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

voonhous 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 c9dbde0f4553 test(spark): add incremental read-path relation coverage 
(#19404)
c9dbde0f4553 is described below

commit c9dbde0f455314c29aacf6e1acbd80a7a8694800
Author: Y Ethan Guo <[email protected]>
AuthorDate: Fri Jul 31 05:08:52 2026 -0700

    test(spark): add incremental read-path relation coverage (#19404)
    
    * test(spark): add incremental read-path relation coverage
    
    * Drop redundant per-partition sum assertion
    
    The assertEquals map comparison already implies the per-partition sums are 
equal, so the follow-up assertTrue can never catch a distinct failure. Remove 
it and the now-unused assertTrue import.
    
    * test(spark): address review comments on incremental relation coverage
    
    - MOR glob arms write log-only file slices via the in-memory index and
      assert merged upsert values plus inputFiles paths per glob
    - thread enableInlineCluster through addDataToQuery and cluster in the
      legacy streaming test to reach the replaced-file filtering of the
      legacy incremental relations
    - pin the legacy (non file-group-reader) plan shape in the streaming test
    - add a TestIncrementalReadWithFullTableScan case pinning the divergent
      v6/v8 default of incr.fallback.fulltablescan.enable on a cleaned span
    
    ---------
    
    Co-authored-by: voon <[email protected]>
---
 .../TestIncrementalReadWithFullTableScan.scala     | 105 ++++++++++-
 .../TestIncrementalReadWithPathGlob.scala          | 205 +++++++++++++++++++++
 .../hudi/functional/TestStreamingSource.scala      |  57 ++++--
 3 files changed, 351 insertions(+), 16 deletions(-)

diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestIncrementalReadWithFullTableScan.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestIncrementalReadWithFullTableScan.scala
index dc6a8b5d760d..6c26e4fe1062 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestIncrementalReadWithFullTableScan.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestIncrementalReadWithFullTableScan.scala
@@ -34,7 +34,7 @@ import org.junit.jupiter.api.{AfterEach, BeforeEach}
 import org.junit.jupiter.api.Assertions.{assertEquals, assertThrows, 
assertTrue}
 import org.junit.jupiter.api.function.Executable
 import org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.EnumSource
+import org.junit.jupiter.params.provider.{CsvSource, EnumSource}
 
 import java.time.Instant
 import java.util.Date
@@ -158,6 +158,109 @@ class TestIncrementalReadWithFullTableScan extends 
HoodieSparkClientTestBase {
     runIncrementalQueryAndCompare(startUncleanedCompletionTs, 
endUncleanedCompletionTs, 1, false)
   }
 
+  /**
+   * Pins the divergent DEFAULT of {@code 
hoodie.datasource.read.incr.fallback.fulltablescan.enable}
+   * across table versions. {@code MergeOnReadIncrementalRelationV1} (selected 
for table version
+   * below 8) hardcodes "false" when the option is absent, while
+   * {@code MergeOnReadIncrementalRelationV2} (table version 8 and above) 
reads the ConfigProperty
+   * default, which is "true" since HUDI-8624. Batch incremental reads route 
through these two
+   * relations for both COW and MOR (see the {@code 
HoodieCopyOnWriteIncrementalHadoopFsRelationFactory}
+   * and {@code HoodieMergeOnReadIncrementalHadoopFsRelationFactory} V1/V2 
subclasses), so the
+   * divergence is table-version driven, not table-type driven. Note that
+   * {@code INCREMENTAL_FALLBACK_TO_FULL_TABLE_SCAN_FOR_NON_EXISTING_FILES} is 
an
+   * alias for the very same ConfigProperty (same key, same default), so a 
single key governs both
+   * versions and only the in-code default differs.
+   *
+   * The timeline shape here is the one proven by {@code 
testFailEarlyForIncrViewQueryForNonExistingFiles}:
+   * 10 insert commits with cleaning and archival configured so that the 
earliest commits are both
+   * cleaned and archived. The discriminating query is a narrow span over the 
two oldest commits that
+   * are still on the active timeline but whose data files have already been 
cleaned; there the
+   * default decides between failing and silently degrading into a full table 
scan.
+   *
+   * A span that starts at "000" does NOT discriminate: both versions return 
every record. With an
+   * archived start instant V1 keeps only the latest merged file slice of each 
affected file group
+   * (which still exists) and its record filter degenerates to {@code 
_hoodie_commit_time > "000"},
+   * so no file is missing and no fallback is needed. That case is asserted 
too, so nobody
+   * "simplifies" the discriminating span away and silently loses the pin.
+   */
+  @ParameterizedTest
+  @CsvSource(value = Array(
+    "COPY_ON_WRITE,6",
+    "COPY_ON_WRITE,8",
+    "MERGE_ON_READ,6",
+    "MERGE_ON_READ,8"))
+  def testDefaultFallbackBehaviorAcrossTableVersions(tableType: String, 
tableVersion: Int): Unit = {
+    val commonOpts = Map(
+      "hoodie.insert.shuffle.parallelism" -> "4",
+      "hoodie.upsert.shuffle.parallelism" -> "4",
+      DataSourceWriteOptions.RECORDKEY_FIELD.key -> "_row_key",
+      DataSourceWriteOptions.PARTITIONPATH_FIELD.key -> "partition",
+      HoodieTableConfig.ORDERING_FIELDS.key -> "timestamp",
+      HoodieWriteConfig.TBL_NAME.key -> "hoodie_test",
+      HoodieMetadataConfig.COMPACT_NUM_DELTA_COMMITS.key -> "1",
+      DataSourceWriteOptions.TABLE_TYPE.key -> tableType,
+      HoodieWriteConfig.WRITE_TABLE_VERSION.key -> tableVersion.toString,
+      HoodieWriteConfig.AUTO_UPGRADE_VERSION.key -> "false"
+    )
+
+    val numCommits = 10
+    for (i <- 1 to numCommits) {
+      val records = recordsToStrings(dataGen.generateInserts("%05d".format(i), 
perBatchSize)).asScala.toList
+      val inputDF = spark.read.json(spark.sparkContext.parallelize(records, 2))
+      inputDF.write.format("org.apache.hudi")
+        .options(commonOpts)
+        .option("hoodie.clean.commits.retained", "3")
+        .option("hoodie.keep.min.commits", "4")
+        .option("hoodie.keep.max.commits", "7")
+        .option(DataSourceWriteOptions.OPERATION.key(), 
DataSourceWriteOptions.INSERT_OPERATION_OPT_VAL)
+        .mode(SaveMode.Append)
+        .save(basePath)
+    }
+
+    val metaClient = createMetaClient(spark, basePath)
+    assertEquals(tableVersion, 
metaClient.getTableConfig.getTableVersion.versionCode(),
+      "table should be written at the requested version to select the V1/V2 
relation")
+    
assertTrue(metaClient.getArchivedTimeline.filterCompletedInstants().countInstants()
 > 0,
+      "early commits must be archived so the timeline matches 
testFailEarlyForIncrViewQueryForNonExistingFiles")
+
+    // The two oldest commits still on the active timeline: not archived, but 
already cleaned.
+    val completedCommits = 
metaClient.getCommitsTimeline.filterCompletedInstants()
+    val cleanedStart = completedCommits.nthInstant(0).get()
+    val cleanedEnd = completedCommits.nthInstant(1).get()
+    // V1 ranges over requested time, V2 over completion time.
+    val (startTs, endTs) = if (tableVersion < 8) {
+      (cleanedStart.requestedTime, cleanedEnd.requestedTime)
+    } else {
+      (cleanedStart.getCompletionTime, cleanedEnd.getCompletionTime)
+    }
+
+    // No fallback option is set on any of these queries: what happens is 
purely the in-code default
+    // of the relation that the table version selects.
+    def readSpan(start: String, end: Option[String]): Long = {
+      var reader = spark.read.format("org.apache.hudi")
+        .option(DataSourceReadOptions.QUERY_TYPE.key(), 
DataSourceReadOptions.QUERY_TYPE_INCREMENTAL_OPT_VAL)
+        .option(DataSourceReadOptions.START_COMMIT.key(), start)
+      end.foreach(e => reader = 
reader.option(DataSourceReadOptions.END_COMMIT.key(), e))
+      reader.load(basePath).count()
+    }
+
+    if (tableVersion < 8) {
+      // V1 hardcodes the default to false, so nothing falls back and the 
query fails on the cleaned
+      // data files of the span.
+      shouldThrowSparkExceptionIfFallbackIsFalse(() => readSpan(startTs, 
Some(endTs)))
+    } else {
+      // V2 takes the config default of true, so the same span silently 
degrades into a full table
+      // scan and still returns the single batch the span covers.
+      assertEquals(perBatchSize, readSpan(startTs, Some(endTs)),
+        "table version 8 must silently fall back to a full table scan for a 
cleaned span")
+    }
+
+    // Both versions agree once the span starts before the timeline: see the 
class comment on why
+    // this query needs no fallback at all.
+    assertEquals(numCommits * perBatchSize, readSpan("000", None),
+      "a span starting at 000 returns every record on both table versions")
+  }
+
   private def runIncrementalQueryAndCompare(
       startTs: String,
       endTs: String,
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestIncrementalReadWithPathGlob.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestIncrementalReadWithPathGlob.scala
new file mode 100644
index 000000000000..8a63ed7221bb
--- /dev/null
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestIncrementalReadWithPathGlob.scala
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.functional
+
+import org.apache.hudi.{DataSourceReadOptions, DataSourceWriteOptions}
+import org.apache.hudi.common.model.HoodieRecord
+import org.apache.hudi.common.table.HoodieTableConfig
+import 
org.apache.hudi.common.testutils.HoodieTestDataGenerator.{recordsToStrings, 
DEFAULT_FIRST_PARTITION_PATH, DEFAULT_SECOND_PARTITION_PATH}
+import org.apache.hudi.config.{HoodieIndexConfig, HoodieWriteConfig}
+import org.apache.hudi.index.HoodieIndex.IndexType
+import org.apache.hudi.testutils.HoodieSparkClientTestBase
+
+import org.apache.spark.sql.{DataFrame, SaveMode, SparkSession}
+import org.junit.jupiter.api.{AfterEach, BeforeEach}
+import org.junit.jupiter.api.Assertions.{assertEquals, assertTrue}
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.CsvSource
+
+import java.util.{List => JList}
+
+import scala.collection.JavaConverters._
+
+/**
+ * Coverage for the {@code hoodie.datasource.read.incr.path.glob} file-slice 
filtering branch of the
+ * batch incremental relations {@code MergeOnReadIncrementalRelationV1} (table 
version 6) and
+ * {@code MergeOnReadIncrementalRelationV2} (table version 8). Both the COW 
and the MOR batch
+ * incremental read routes build a {@code HoodieIncrementalFileIndex} backed 
by these relations (see
+ * {@code HoodieCopyOnWriteIncrementalHadoopFsRelationFactoryV1/V2} and the 
MOR factories), and the
+ * table version selects V1 vs V2 in {@code DefaultSource}. The COW arms cover 
the base-file path of
+ * {@code filterFileSlices}, since the COW factory builds the file index with
+ * {@code includeLogFiles = false}. The MOR arms write with the in-memory 
index, which can index log
+ * files, so their inserts land in log-only file slices; those arms therefore 
exercise the
+ * {@code getLatestLogFile} fallback of {@code filterFileSlices} and, because 
the MOR factory builds
+ * the file index with {@code includeLogFiles = true}, the log-file branch of
+ * {@code HoodieIncrementalFileIndex.inputFiles}.
+ *
+ * Existing glob coverage ({@code TestCOWDataSourceStorage}, plus {@code 
TestDataSourceForBootstrap}
+ * for bootstrapped COW) exercises only COW at the default table
+ * version and asserts counts alone; this pins the per-partition result of the 
glob (including the
+ * glob-matches-nothing empty branch) across both table versions and both 
table types, the merged
+ * value of an upserted record inside the glob, and the file paths reported by 
{@code inputFiles}.
+ */
+class TestIncrementalReadWithPathGlob extends HoodieSparkClientTestBase {
+
+  private var spark: SparkSession = _
+  // generateInsertsForPartition sets the record's partition_path field to the 
given partition.
+  private val firstPartition = DEFAULT_FIRST_PARTITION_PATH // 2016/03/15
+  private val secondPartition = DEFAULT_SECOND_PARTITION_PATH // 2015/03/16
+  private val numFirst = 8
+  private val numSecond = 5
+  private val numUpdated = 3
+  // generateUpdates stamps the rider field with "rider-" + the seed it is 
given.
+  private val updatedRider = "rider-003"
+
+  @BeforeEach override def setUp(): Unit = {
+    setTableName("hoodie_test")
+    initPath()
+    initSparkContexts()
+    spark = sqlContext.sparkSession
+    initTestDataGenerator()
+    initHoodieStorage()
+  }
+
+  @AfterEach override def tearDown(): Unit = {
+    spark = null
+    cleanupResources()
+  }
+
+  @ParameterizedTest
+  @CsvSource(value = Array(
+    "COPY_ON_WRITE,6",
+    "COPY_ON_WRITE,8",
+    "MERGE_ON_READ,6",
+    "MERGE_ON_READ,8"))
+  def testIncrementalPathGlobPartitionFiltering(tableType: String, 
tableVersion: Int): Unit = {
+    val baseOpts = Map(
+      "hoodie.insert.shuffle.parallelism" -> "2",
+      "hoodie.upsert.shuffle.parallelism" -> "2",
+      DataSourceWriteOptions.RECORDKEY_FIELD.key -> "_row_key",
+      DataSourceWriteOptions.PARTITIONPATH_FIELD.key -> "partition_path",
+      HoodieTableConfig.ORDERING_FIELDS.key -> "timestamp",
+      HoodieWriteConfig.TBL_NAME.key -> "hoodie_test",
+      DataSourceWriteOptions.TABLE_TYPE.key -> tableType,
+      HoodieWriteConfig.WRITE_TABLE_VERSION.key -> tableVersion.toString,
+      HoodieWriteConfig.AUTO_UPGRADE_VERSION.key -> "false")
+    // The default SIMPLE index cannot index log files, so MOR inserts would 
land in base files and
+    // every file slice would carry a base file. The in-memory index can, 
which keeps the MOR arms
+    // on log-only file slices and drives the log-file branches of the 
relation and the file index.
+    val opts = if 
(DataSourceWriteOptions.MOR_TABLE_TYPE_OPT_VAL.equals(tableType)) {
+      baseOpts + (HoodieIndexConfig.INDEX_TYPE.key -> 
IndexType.INMEMORY.toString)
+    } else {
+      baseOpts
+    }
+
+    // First commit writes only the first partition, second commit only the 
second partition, so the
+    // glob boundary lines up with the commit boundary and expected counts are 
deterministic.
+    val firstBatch = writeBatch("001", numFirst, firstPartition, opts, 
SaveMode.Overwrite)
+    writeBatch("002", numSecond, secondPartition, opts, SaveMode.Append)
+    // Third commit upserts a subset of the first batch. Updates keep the 
record key and partition,
+    // so the per-partition counts must stay the same and the updated rows 
must be merged in place.
+    writeUpdateBatch("003", firstBatch.subList(0, numUpdated), opts)
+
+    val metaClient = createMetaClient(spark, basePath)
+    assertEquals(tableVersion, 
metaClient.getTableConfig.getTableVersion.versionCode(),
+      "table should be written at the requested version to select the V1/V2 
relation")
+
+    // Without a glob the incremental span returns every record from both 
partitions.
+    val allDf = incrementalRead(None)
+    assertPartitionCounts(allDf, Map(firstPartition -> numFirst, 
secondPartition -> numSecond))
+    val allInputFiles = allDf.inputFiles
+    if (DataSourceWriteOptions.MOR_TABLE_TYPE_OPT_VAL.equals(tableType)) {
+      // Guards the coverage this test is here for: if MOR inserts ever stop 
landing in log files the
+      // getLatestLogFile fallback of filterFileSlices and the log branch of 
inputFiles go untested.
+      assertTrue(allInputFiles.forall(_.contains(".log.")),
+        "MOR file slices must be log only under the in-memory index: " + 
allInputFiles.mkString(","))
+    }
+    assertTrue(allInputFiles.exists(_.contains(firstPartition)),
+      "without a glob inputFiles must report files from the first partition: " 
+ allInputFiles.mkString(","))
+    assertTrue(allInputFiles.exists(_.contains(secondPartition)),
+      "without a glob inputFiles must report files from the second partition: 
" + allInputFiles.mkString(","))
+
+    // Glob restricted to the first partition returns only its records.
+    val firstDf = incrementalRead(Some("/2016/*/*/*"))
+    assertPartitionCounts(firstDf, Map(firstPartition -> numFirst))
+    assertEquals(numUpdated, firstDf.filter(s"rider = 
'$updatedRider'").count(),
+      "the upserted records must be merged into the glob result, not 
duplicated or dropped")
+    val firstInputFiles = firstDf.inputFiles
+    assertTrue(firstInputFiles.nonEmpty, "a glob matching the first partition 
must report input files")
+    assertTrue(firstInputFiles.forall(_.contains(firstPartition)),
+      "every file reported for the first-partition glob must live under it: " 
+ firstInputFiles.mkString(","))
+
+    // Glob restricted to the second partition returns only its records.
+    val secondDf = incrementalRead(Some("/2015/*/*/*"))
+    assertPartitionCounts(secondDf, Map(secondPartition -> numSecond))
+    val secondInputFiles = secondDf.inputFiles
+    assertTrue(secondInputFiles.nonEmpty, "a glob matching the second 
partition must report input files")
+    assertTrue(secondInputFiles.forall(_.contains(secondPartition)),
+      "every file reported for the second-partition glob must live under it: " 
+ secondInputFiles.mkString(","))
+
+    // Glob that matches no partition exercises the empty-result branch of the 
relation.
+    val emptyDf = incrementalRead(Some("/9999/*/*/*"))
+    assertEquals(0, emptyDf.count(),
+      "a glob matching no partition path must yield an empty result")
+    assertEquals(0, emptyDf.inputFiles.length,
+      "a glob matching no partition path must report no input files")
+  }
+
+  /**
+   * Writes an insert batch and returns the generated records so that later 
batches can update them.
+   * The seed only feeds the generated field values (rider-001, driver-001, 
...); it does not control
+   * the commit time of the write.
+   */
+  private def writeBatch(seed: String, n: Int, partition: String,
+                         opts: Map[String, String], mode: SaveMode): 
JList[HoodieRecord[_]] = {
+    val records = dataGen.generateInsertsForPartition(seed, n, partition)
+    write(records, opts, DataSourceWriteOptions.INSERT_OPERATION_OPT_VAL, mode)
+    records
+  }
+
+  private def writeUpdateBatch(seed: String, baseRecords: 
JList[HoodieRecord[_]],
+                               opts: Map[String, String]): Unit = {
+    write(dataGen.generateUpdates(seed, baseRecords), opts,
+      DataSourceWriteOptions.UPSERT_OPERATION_OPT_VAL, SaveMode.Append)
+  }
+
+  private def write(records: JList[HoodieRecord[_]], opts: Map[String, String],
+                    operation: String, mode: SaveMode): Unit = {
+    val rows = recordsToStrings(records).asScala.toList
+    val df = spark.read.json(spark.sparkContext.parallelize(rows, 2))
+    df.write.format("org.apache.hudi")
+      .options(opts)
+      .option(DataSourceWriteOptions.OPERATION.key, operation)
+      .mode(mode)
+      .save(basePath)
+  }
+
+  private def incrementalRead(pathGlob: Option[String]): DataFrame = {
+    var reader = spark.read.format("org.apache.hudi")
+      .option(DataSourceReadOptions.QUERY_TYPE.key, 
DataSourceReadOptions.QUERY_TYPE_INCREMENTAL_OPT_VAL)
+      .option(DataSourceReadOptions.START_COMMIT.key, "000")
+    pathGlob.foreach(g => reader = 
reader.option(DataSourceReadOptions.INCR_PATH_GLOB.key, g))
+    reader.load(basePath)
+  }
+
+  private def assertPartitionCounts(df: DataFrame, expected: Map[String, 
Int]): Unit = {
+    val actual = df.groupBy("_hoodie_partition_path").count().collect()
+      .map(row => row.getString(0) -> row.getLong(1).toInt).toMap
+    assertEquals(expected, actual, "incremental path glob returned unexpected 
per-partition counts")
+  }
+}
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestStreamingSource.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestStreamingSource.scala
index ffaae686eb4d..ff0ce57d2b87 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestStreamingSource.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestStreamingSource.scala
@@ -296,27 +296,30 @@ class TestStreamingSource extends StreamTest {
   }
 
   /**
-   * Exercises the legacy incremental streaming path in 
[[HoodieStreamSourceV1]], which is taken
-   * when the streaming read table version is below EIGHT and the file group 
reader is disabled.
-   * This drives the [[IncrementalRelationV1]] (COW) / 
[[MergeOnReadIncrementalRelationV1]] (MOR)
-   * branches of `getBatch` rather than the newer HadoopFsRelation factory 
path.
+   * Exercises the legacy incremental streaming path in 
[[HoodieStreamSourceV1]] (table version
+   * below EIGHT) and [[HoodieStreamSourceV2]] (table version EIGHT and 
above), taken when the file
+   * group reader is disabled. This drives the standalone incremental relations
+   * ([[IncrementalRelationV1]] / [[MergeOnReadIncrementalRelationV1]] for 
version 6,
+   * [[IncrementalRelationV2]] / [[MergeOnReadIncrementalRelationV2]] for 
version 8) via `getBatch`,
+   * rather than the newer HadoopFsRelation factory path.
    */
-  private def testLegacyIncrementalStreamSource(tableType: HoodieTableType): 
Unit = {
+  private def testLegacyIncrementalStreamSource(tableType: HoodieTableType,
+                                                tableVersion: 
HoodieTableVersion): Unit = {
     withTempDir { inputDir =>
-      val tablePath = 
s"${inputDir.getCanonicalPath}/test_${tableType.name}_legacy_stream"
+      val tablePath = 
s"${inputDir.getCanonicalPath}/test_${tableType.name}_v${tableVersion.versionCode}_legacy_stream"
       HoodieTableMetaClient.newTableBuilder()
         .setTableType(tableType)
         .setTableName(getTableName(tablePath))
-        .setTableVersion(HoodieTableVersion.SIX)
+        .setTableVersion(tableVersion)
         .setRecordKeyFields("id")
         .setOrderingFields("ts")
         
.initTable(HadoopFSUtils.getStorageConf(spark.sessionState.newHadoopConf()), 
tablePath)
 
-      addData(tablePath, Seq(("1", "a1", "10", "000")), tableVersion = 
HoodieTableVersion.SIX)
+      addData(tablePath, Seq(("1", "a1", "10", "000")), tableVersion = 
tableVersion)
       val df = spark.readStream
         .format("org.apache.hudi")
-        .option(WRITE_TABLE_VERSION.key, 
HoodieTableVersion.SIX.versionCode().toString)
-        .option(STREAMING_READ_TABLE_VERSION.key, 
HoodieTableVersion.SIX.versionCode().toString)
+        .option(WRITE_TABLE_VERSION.key, tableVersion.versionCode().toString)
+        .option(STREAMING_READ_TABLE_VERSION.key, 
tableVersion.versionCode().toString)
         // force the legacy (non file-group-reader) incremental relation path
         .option(HoodieReaderConfig.FILE_GROUP_READER_ENABLED.key, "false")
         .load(tablePath)
@@ -330,16 +333,31 @@ class TestStreamingSource extends StreamTest {
         addDataToQuery(tablePath,
           Seq(("2", "a2", "12", "000"),
             ("3", "a3", "12", "000")),
-          tableVersion = HoodieTableVersion.SIX),
+          tableVersion = tableVersion),
         StartStream(),
         AssertOnQuery { q => q.processAllAvailable(); true },
+        // The legacy branch of getBatch materializes the micro batch from an 
RDD via
+        // internalCreateDataFrame, so the physical plan is a "Scan 
ExistingRDD"; this fails if the
+        // legacy branch is dropped and the source silently falls back to the 
file group reader
+        // path, which scans a HadoopFsRelation ("FileScan" / "Scan parquet") 
instead.
+        AssertOnQuery { q =>
+          val plan = q.lastExecution.executedPlan.toString
+          assertTrue(plan.contains("Scan ExistingRDD"),
+            "expected the legacy RDD-backed incremental batch, but got plan: " 
+ plan)
+          assertTrue(!plan.contains("FileScan"),
+            "expected no file-group-reader HadoopFsRelation scan, but got 
plan: " + plan)
+          true
+        },
         CheckAnswerRows(
           Seq(Row("2", "a2", "12", "000"),
             Row("3", "a3", "12", "000")),
           lastOnly = true, isSorted = false),
         StopStream,
 
-        addDataToQuery(tablePath, Seq(("4", "a4", "13", "000")), tableVersion 
= HoodieTableVersion.SIX),
+        // inline clustering on this write lands a replacecommit inside the 
next getBatch span, which
+        // reaches the replaced-file-group filtering of the legacy incremental 
relations
+        addDataToQuery(tablePath, Seq(("4", "a4", "13", "000")), 
enableInlineCluster = true,
+          tableVersion = tableVersion),
         StartStream(),
         AssertOnQuery { q => q.processAllAvailable(); true },
         CheckAnswerRows(Seq(Row("4", "a4", "13", "000")), lastOnly = true, 
isSorted = false)
@@ -348,11 +366,19 @@ class TestStreamingSource extends StreamTest {
   }
 
   test("test cow stream source with legacy file group reader disabled") {
-    testLegacyIncrementalStreamSource(COPY_ON_WRITE)
+    testLegacyIncrementalStreamSource(COPY_ON_WRITE, HoodieTableVersion.SIX)
   }
 
   test("test mor stream source with legacy file group reader disabled") {
-    testLegacyIncrementalStreamSource(MERGE_ON_READ)
+    testLegacyIncrementalStreamSource(MERGE_ON_READ, HoodieTableVersion.SIX)
+  }
+
+  test("test cow stream source with legacy file group reader disabled on table 
version 8") {
+    testLegacyIncrementalStreamSource(COPY_ON_WRITE, HoodieTableVersion.EIGHT)
+  }
+
+  test("test mor stream source with legacy file group reader disabled on table 
version 8") {
+    testLegacyIncrementalStreamSource(MERGE_ON_READ, HoodieTableVersion.EIGHT)
   }
 
   private def testCheckpointTranslation(tableName: String,
@@ -475,9 +501,10 @@ class TestStreamingSource extends StreamTest {
 
   private def addDataToQuery(inputPath: String,
                              rows: Seq[(String, String, String, String)],
+                             enableInlineCluster: Boolean = false,
                              tableVersion: HoodieTableVersion = 
HoodieTableVersion.current): AssertOnQuery = {
     AssertOnQuery { _=>
-      addData(inputPath, rows, tableVersion = tableVersion)
+      addData(inputPath, rows, enableInlineCluster = enableInlineCluster, 
tableVersion = tableVersion)
       true
     }
   }

Reply via email to