This is an automated email from the ASF dual-hosted git repository.

codope pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new aabed4659396 fix(spark): honor 'earliest' begin instant in DataFrame 
CDC reads (#20304)
aabed4659396 is described below

commit aabed46593960ff3553cdc1ace1a491987983702
Author: Sagar Sumit <[email protected]>
AuthorDate: Wed Oct 14 09:24:30 2026 +0530

    fix(spark): honor 'earliest' begin instant in DataFrame CDC reads (#20304)
    
    A DataFrame CDC read with hoodie.datasource.read.begin.instanttime=earliest
    returned no rows: HoodieCDCFileIndex passed the literal into InstantRange,
    and the string comparison against instant times excluded every commit.
    Map it to HoodieTimeline.INIT_INSTANT_TS, matching what hudi_table_changes
    already does for 'earliest'.
    
    Also correct the begin.instanttime doc: the start bound is exclusive
    (completion_time > START_COMMIT), not inclusive.
    
    Fixes #20300
    Fixes #20301
---
 .../scala/org/apache/hudi/DataSourceOptions.scala  |  5 ++--
 .../org/apache/hudi/cdc/HoodieCDCFileIndex.scala   |  7 +++++-
 .../functional/cdc/TestCDCDataFrameSuite.scala     | 28 ++++++++++++++++++++++
 3 files changed, 37 insertions(+), 3 deletions(-)

diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/DataSourceOptions.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/DataSourceOptions.scala
index 899b93a386f9..1d9caf830631 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/DataSourceOptions.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/DataSourceOptions.scala
@@ -116,8 +116,9 @@ object DataSourceReadOptions {
     .sinceVersion("0.9.0")
     .withDocumentation("Required when `" + QUERY_TYPE.key() + "` is set to `" 
+ QUERY_TYPE_INCREMENTAL_OPT_VAL + "`. "
       + "Represents the completion time to start incrementally pulling data 
from. The completion time here need not necessarily "
-      + "correspond to an instant on the timeline. New data written with 
completion_time >= START_COMMIT are fetched out. "
-      + "For e.g: ‘20170901080000’ will get all new data written on or after 
Sep 1, 2017 08:00AM.")
+      + "correspond to an instant on the timeline. New data written with 
completion_time > START_COMMIT are fetched out, "
+      + "i.e. the start is exclusive while 
`hoodie.datasource.read.end.instanttime` is inclusive. "
+      + "For e.g: ‘20170901080000’ will get all new data written after Sep 1, 
2017 08:00AM.")
 
   val END_COMMIT: ConfigProperty[String] = ConfigProperty
     .key("hoodie.datasource.read.end.instanttime")
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/cdc/HoodieCDCFileIndex.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/cdc/HoodieCDCFileIndex.scala
index bf8f0f52c2bd..8c8d6047e2bc 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/cdc/HoodieCDCFileIndex.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/cdc/HoodieCDCFileIndex.scala
@@ -25,6 +25,8 @@ import org.apache.hudi.common.table.HoodieTableMetaClient
 import org.apache.hudi.common.table.cdc.{HoodieCDCExtractor, HoodieCDCUtils}
 import org.apache.hudi.common.table.log.InstantRange
 import org.apache.hudi.common.table.log.InstantRange.RangeType
+import org.apache.hudi.common.table.read.IncrementalQueryAnalyzer
+import org.apache.hudi.common.table.timeline.HoodieTimeline
 import org.apache.hudi.exception.HoodieException
 
 import org.apache.hadoop.fs.{FileStatus, Path}
@@ -57,7 +59,10 @@ class HoodieCDCFileIndex(override val spark: SparkSession,
       .startInstant(options.getOrElse(DataSourceReadOptions.START_COMMIT.key(),
         throw new HoodieException(s"CDC Query should provide the valid start 
completion time "
           + s"through the option ${DataSourceReadOptions.START_COMMIT.key()}")
-      ))
+      ) match {
+        case start if 
start.equalsIgnoreCase(IncrementalQueryAnalyzer.START_COMMIT_EARLIEST) => 
HoodieTimeline.INIT_INSTANT_TS
+        case start => start
+      })
       .endInstant(options.getOrElse(DataSourceReadOptions.END_COMMIT.key(),
         HoodieCDCFileIndex.getTimestampOfLatestInstant(metaClient)
       ))
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/cdc/TestCDCDataFrameSuite.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/cdc/TestCDCDataFrameSuite.scala
index 894b7365ba16..853d82313740 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/cdc/TestCDCDataFrameSuite.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/cdc/TestCDCDataFrameSuite.scala
@@ -1023,4 +1023,32 @@ class TestCDCDataFrameSuite extends HoodieCDCTestBase {
       }
     }
   }
+
+  @ParameterizedTest
+  @CsvSource(Array("COPY_ON_WRITE,earliest", "MERGE_ON_READ,earliest", 
"COPY_ON_WRITE,EARLIEST", "MERGE_ON_READ,EARLIEST"))
+  def testCDCReadFromEarliest(tableType: String, beginInstant: String): Unit = 
{
+    val options = commonOpts ++ Map(DataSourceWriteOptions.TABLE_TYPE.key() -> 
tableType)
+
+    val records1 = recordsToStrings(dataGen.generateInserts("000", 
100)).asScala.toList
+    spark.read.json(spark.sparkContext.parallelize(records1, 2))
+      .write.format("org.apache.hudi")
+      .options(options)
+      .mode(SaveMode.Overwrite)
+      .save(basePath)
+    metaClient = createMetaClient(spark, basePath)
+    val commitTime1 = 
metaClient.reloadActiveTimeline.lastInstant().get().requestedTime
+
+    val records2 = recordsToStrings(dataGen.generateUniqueUpdates("001", 
30)).asScala.toList
+    spark.read.json(spark.sparkContext.parallelize(records2, 2))
+      .write.format("org.apache.hudi")
+      .options(options)
+      .mode(SaveMode.Append)
+      .save(basePath)
+
+    val cdcFromEarliest = cdcDataFrame(beginInstant)
+    assertCDCOpCnt(cdcFromEarliest, 100, 30, 0)
+    assertEquals(
+      
cdcDataFrame(instantBefore(commitTime1)).collect().map(_.toString).sorted.toSeq,
+      cdcFromEarliest.collect().map(_.toString).sorted.toSeq)
+  }
 }

Reply via email to