This is an automated email from the ASF dual-hosted git repository.
yihua 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 211ccba18c0f fix(spark): avoid full partition listing in incremental
reads (#19898)
211ccba18c0f is described below
commit 211ccba18c0f4f1041bd2016e725ab4a9106accd
Author: Prashant Wason <[email protected]>
AuthorDate: Tue Oct 6 18:49:00 2026 -0700
fix(spark): avoid full partition listing in incremental reads (#19898)
---
.../apache/hudi/HoodieIncrementalFileIndex.scala | 21 ++-
.../hudi/MergeOnReadIncrementalRelationV1.scala | 6 +-
.../hudi/MergeOnReadIncrementalRelationV2.scala | 6 +-
.../apache/hudi/SparkHoodieTableFileIndex.scala | 173 +++++++++++++--------
.../TestIncrementalReadWithPartitionFilter.scala | 131 ++++++++++++++++
5 files changed, 267 insertions(+), 70 deletions(-)
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieIncrementalFileIndex.scala
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieIncrementalFileIndex.scala
index 5b5312643aac..ba03764aa4c1 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieIncrementalFileIndex.scala
+++
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieIncrementalFileIndex.scala
@@ -17,8 +17,9 @@
package org.apache.hudi
-import org.apache.hudi.common.model.HoodieLogFile
+import org.apache.hudi.common.model.{FileSlice, HoodieLogFile}
import org.apache.hudi.common.table.HoodieTableMetaClient
+import org.apache.hudi.core.read.BaseHoodieTableFileIndex
import org.apache.hudi.storage.StoragePathInfo
import org.apache.hudi.util.{JFunction, SparkConfigUtils}
@@ -43,6 +44,24 @@ class HoodieIncrementalFileIndex(override val spark:
SparkSession,
spark, metaClient, schemaSpec, options, fileStatusCache, includeLogFiles,
shouldEmbedFileSlices = true
) with FileIndex {
+ // Skip the Spark optimizer's partition pruning rule (e.g.
Spark33HoodiePruneFileSourcePartitions)
+ // which would trigger a full-table partition listing via the base class.
The incremental file
+ // index already selects only modified file groups, and prunes them by the
partition filters, in
+ // listFileSplits(). Overridden rather than set so that it also holds after
refresh().
+ override def hasPredicatesPushedDown: Boolean = true
+
+ // Serve file slices from the incremental relation rather than the base
class listing, in case
+ // anything still calls this.
+ override def filterFileSlices(dataFilters: Seq[Expression],
partitionFilters: Seq[Expression],
+ isPartitionPruneOnly: Boolean)
+ : Seq[(Option[BaseHoodieTableFileIndex.PartitionPath], Seq[FileSlice])] = {
+ mergeOnReadIncrementalRelation.listFileSplits(partitionFilters,
dataFilters).values.flatten
+ .filter(!_.isEmpty)
+ .groupBy(_.getPartitionPath)
+ .map { case (partitionPath, fileSlices) =>
(Option(convertToPartitionPath(partitionPath)), fileSlices.toSeq) }
+ .toSeq
+ }
+
override def listFiles(partitionFilters: Seq[Expression], dataFilters:
Seq[Expression]): Seq[PartitionDirectory] = {
val fileSlices =
mergeOnReadIncrementalRelation.listFileSplits(partitionFilters,
dataFilters).toSeq.map {
case (partitionValues, fileSlices) =>
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV1.scala
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV1.scala
index fe0996e49e63..8c78bf68db54 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV1.scala
+++
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV1.scala
@@ -130,8 +130,10 @@ case class MergeOnReadIncrementalRelationV1(override val
sqlContext: SQLContext,
val fsView = new HoodieTableFileSystemView(metaClient, timeline,
affectedFilesInCommits)
val modifiedPartitions = getWritePartitionPaths(commitsMetadata)
-
fileIndex.listMatchingPartitionPaths(HoodieFileIndex.convertFilterForTimestampKeyGenerator(metaClient,
partitionFilters))
- .map(p => p.getPath).filter(p => modifiedPartitions.contains(p))
+ // Evaluate the partition predicates against the modified partitions
only, rather than listing
+ // every partition of the table.
+ fileIndex.filterPartitionPaths(modifiedPartitions.asScala.toSeq,
+ HoodieFileIndex.convertFilterForTimestampKeyGenerator(metaClient,
partitionFilters))
.flatMap { relativePartitionPath =>
fsView.getLatestMergedFileSlicesBeforeOrOn(relativePartitionPath,
latestCommit).iterator().asScala
}
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV2.scala
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV2.scala
index 796e43ae8c61..501e619fa439 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV2.scala
+++
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/MergeOnReadIncrementalRelationV2.scala
@@ -128,8 +128,10 @@ case class MergeOnReadIncrementalRelationV2(override val
sqlContext: SQLContext,
val fsView = new HoodieTableFileSystemView(metaClient, timeline,
affectedFilesInCommits)
val modifiedPartitions = getWritePartitionPaths(commitsMetadata)
-
fileIndex.listMatchingPartitionPaths(HoodieFileIndex.convertFilterForTimestampKeyGenerator(metaClient,
partitionFilters))
- .map(p => p.getPath).filter(p => modifiedPartitions.contains(p))
+ // Evaluate the partition predicates against the modified partitions
only, rather than listing
+ // every partition of the table.
+ fileIndex.filterPartitionPaths(modifiedPartitions.asScala.toSeq,
+ HoodieFileIndex.convertFilterForTimestampKeyGenerator(metaClient,
partitionFilters))
.flatMap { relativePartitionPath =>
fsView.getLatestMergedFileSlicesBeforeOrOn(relativePartitionPath,
latestCommit).iterator().asScala
}
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/SparkHoodieTableFileIndex.scala
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/SparkHoodieTableFileIndex.scala
index 8187d5c54e86..7566bf148125 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/SparkHoodieTableFileIndex.scala
+++
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/SparkHoodieTableFileIndex.scala
@@ -250,44 +250,33 @@ class SparkHoodieTableFileIndex(spark: SparkSession,
}
/**
- * Prune the partition by the filter.This implementation is fork from
+ * Lists the partitions of the table matching the partition predicates. The
pruning is forked from
*
org.apache.spark.sql.execution.datasources.PartitioningAwareFileIndex#prunePartitions.
*
+ * Whether this lists the table depends on the predicates and the query:
+ * - No partition predicate: returns [[getAllQueryPartitionPaths]]. For an
incremental query whose start
+ * is inside the active timeline these are the partitions written by the
instants in range, read from
+ * the commit metadata. Otherwise (snapshot query, or an incremental
query starting from `earliest` or
+ * before the active timeline) every partition of the table is listed.
+ * - Equality predicates on all partition columns: a single existence
check, no listing.
+ * - Equality predicates on the leading partition columns: only the
partitions under that prefix are listed.
+ * - No bound leading prefix: hive-style, URL-encoded tables list from the
table root, evaluating the
+ * predicates through the metadata table, with no incremental shortcut.
Other tables fall back to
+ * [[getAllQueryPartitionPaths]] as above.
+ *
+ * Callers that already hold the candidate partitions (e.g. incremental
reads, which know the partitions
+ * modified by the commits in range) should use [[filterPartitionPaths]]
instead, which never lists the table.
+ *
* @param predicates The filter condition.
* @return The pruned partition paths.
*/
def listMatchingPartitionPaths(predicates: Seq[Expression]):
Seq[PartitionPath] = {
- val resolve = spark.sessionState.analyzer.resolver
val partitionColumnNames = getPartitionColumns
-
- // Resolves GetStructField chain to full dot-path:
GetStructField(attr("a"), _, "b") → "a.b"
- def getFieldPath(expr: Expression): Option[String] = expr match {
- case a: AttributeReference => Some(a.name)
- case GetStructField(child, _, Some(fieldName)) =>
- getFieldPath(child).map(_ + "." + fieldName)
- case _ => None
- }
-
- // True if every column reference in expr resolves to a partition column.
- // For nested columns, walks GetStructField chains to match the full
dot-path.
- // Example: partition = "nested_record.level"
- // nested_record.level = 'INFO' → GetStructField path
"nested_record.level" → true
- // nested_record.nested_int = 10 → GetStructField path
"nested_record.nested_int" → false
- // IsNotNull(nested_record) → AttributeReference "nested_record"
not in partitionColumnNames → false
- def referencesOnlyPartitionColumns(expr: Expression): Boolean = expr match
{
- case g: GetStructField =>
- getFieldPath(g).exists(path => partitionColumnNames.exists(pc =>
resolve(path, pc)))
- case a: AttributeReference =>
- partitionColumnNames.exists(pc => resolve(a.name, pc))
- case _ =>
- expr.children.forall(referencesOnlyPartitionColumns)
- }
-
val partitionPruningPredicates =
predicates.filter(referencesOnlyPartitionColumns)
if (partitionPruningPredicates.isEmpty) {
val queryPartitionPaths = getAllQueryPartitionPaths.asScala.toSeq
- logInfo(s"No partition predicates provided, listing full table
(${queryPartitionPaths.size} partitions)")
+ logInfo(s"No partition predicates provided, using all query partitions
(${queryPartitionPaths.size} partitions)")
queryPartitionPaths
} else {
// NOTE: We fallback to already cached partition-paths only in cases
when we can subsequently
@@ -307,48 +296,102 @@ class SparkHoodieTableFileIndex(spark: SparkSession,
// we might not be able to properly parse partition-values from
the listed partition-paths.
// In that case, we simply could not apply partition pruning and
will have to regress to scanning
// the whole table
- if (haveProperPartitionValues(partitionPaths.toSeq) &&
partitionSchema.nonEmpty) {
- val predicate = partitionPruningPredicates.reduce(expressions.And)
- val partitionFieldNames = partitionSchema.fieldNames
- val transformedPredicate = predicate.transform {
- case g @ GetStructField(_, _, Some(_)) =>
- getFieldPath(g).flatMap { path =>
- val idx = partitionFieldNames.indexWhere(name => resolve(path,
name))
- if (idx >= 0) Some(BoundReference(idx,
partitionSchema(idx).dataType, nullable = true))
- else None
- }.getOrElse(g)
- case a: AttributeReference =>
- val index = partitionSchema.indexWhere(sf => resolve(a.name,
sf.name))
- if (index >= 0) BoundReference(index,
partitionSchema(index).dataType, nullable = true)
- else a
- }
- val boundPredicate: BasePredicate = try {
- // Try using 1-arg constructor via reflection
- val clazz =
Class.forName("org.apache.spark.sql.catalyst.expressions.InterpretedPredicate")
- val ctor = clazz.getConstructor(classOf[Expression])
- ctor.newInstance(transformedPredicate).asInstanceOf[BasePredicate]
- } catch {
- case _: NoSuchMethodException | _: IllegalArgumentException =>
- // Fallback: Try using 2-arg constructor for certain Spark runtime
- val clazz =
Class.forName("org.apache.spark.sql.catalyst.expressions.InterpretedPredicate")
- val ctor = clazz.getConstructor(classOf[Expression],
classOf[Boolean])
- ctor.newInstance(transformedPredicate, java.lang.Boolean.FALSE)
- .asInstanceOf[BasePredicate]
- }
- val prunedPartitionPaths = partitionPaths.filter {
- partitionPath =>
boundPredicate.eval(InternalRow.fromSeq(partitionPath.getValues))
- }.toSeq
+ prunePartitionPaths(partitionPaths.toSeq, partitionPruningPredicates)
+ }
+ }
- logInfo(s"Using provided predicates to prune number of target table's
partitions scanned from" +
- s" ${partitionPaths.size} to ${prunedPartitionPaths.size}")
+ /**
+ * Prunes the given relative partition paths by the partition predicates,
without listing the table.
+ * Used by incremental reads, which already know the partitions modified by
the commits in range.
+ *
+ * @param relativePartitionPaths The candidate relative partition paths.
+ * @param predicates The filter condition.
+ * @return The relative partition paths matching the partition predicates.
+ */
+ def filterPartitionPaths(relativePartitionPaths: Seq[String], predicates:
Seq[Expression]): Seq[String] = {
+ val partitionPruningPredicates =
predicates.filter(referencesOnlyPartitionColumns)
+ if (partitionPruningPredicates.isEmpty) {
+ relativePartitionPaths
+ } else {
+ prunePartitionPaths(relativePartitionPaths.map(convertToPartitionPath),
partitionPruningPredicates).map(_.getPath)
+ }
+ }
- prunedPartitionPaths
- } else {
- logWarning(s"Unable to apply partition pruning, due to failure to
parse partition values from the" +
- s" following path(s): ${partitionPaths.find(_.getValues.length ==
0).map(e => e.getPath)}")
+ // Resolves GetStructField chain to full dot-path: GetStructField(attr("a"),
_, "b") → "a.b"
+ private def getFieldPath(expr: Expression): Option[String] = expr match {
+ case a: AttributeReference => Some(a.name)
+ case GetStructField(child, _, Some(fieldName)) =>
+ getFieldPath(child).map(_ + "." + fieldName)
+ case _ => None
+ }
+
+ // True if every column reference in expr resolves to a partition column.
+ // For nested columns, walks GetStructField chains to match the full
dot-path.
+ // Example: partition = "nested_record.level"
+ // nested_record.level = 'INFO' → GetStructField path
"nested_record.level" → true
+ // nested_record.nested_int = 10 → GetStructField path
"nested_record.nested_int" → false
+ // IsNotNull(nested_record) → AttributeReference "nested_record" not
in partitionColumnNames → false
+ private def referencesOnlyPartitionColumns(expr: Expression): Boolean = {
+ val resolve = spark.sessionState.analyzer.resolver
+ val partitionColumnNames = getPartitionColumns
+ expr match {
+ case g: GetStructField =>
+ getFieldPath(g).exists(path => partitionColumnNames.exists(pc =>
resolve(path, pc)))
+ case a: AttributeReference =>
+ partitionColumnNames.exists(pc => resolve(a.name, pc))
+ case _ =>
+ expr.children.forall(referencesOnlyPartitionColumns)
+ }
+ }
- partitionPaths.toSeq
+ /**
+ * Evaluates the (non-empty) partition pruning predicates against the
partition values of the given
+ * partition paths.
+ */
+ private def prunePartitionPaths(partitionPaths: Seq[PartitionPath],
+ partitionPruningPredicates:
Seq[Expression]): Seq[PartitionPath] = {
+ val resolve = spark.sessionState.analyzer.resolver
+ if (haveProperPartitionValues(partitionPaths.toSeq) &&
partitionSchema.nonEmpty) {
+ val predicate = partitionPruningPredicates.reduce(expressions.And)
+ val partitionFieldNames = partitionSchema.fieldNames
+ val transformedPredicate = predicate.transform {
+ case g @ GetStructField(_, _, Some(_)) =>
+ getFieldPath(g).flatMap { path =>
+ val idx = partitionFieldNames.indexWhere(name => resolve(path,
name))
+ if (idx >= 0) Some(BoundReference(idx,
partitionSchema(idx).dataType, nullable = true))
+ else None
+ }.getOrElse(g)
+ case a: AttributeReference =>
+ val index = partitionSchema.indexWhere(sf => resolve(a.name,
sf.name))
+ if (index >= 0) BoundReference(index,
partitionSchema(index).dataType, nullable = true)
+ else a
}
+ val boundPredicate: BasePredicate = try {
+ // Try using 1-arg constructor via reflection
+ val clazz =
Class.forName("org.apache.spark.sql.catalyst.expressions.InterpretedPredicate")
+ val ctor = clazz.getConstructor(classOf[Expression])
+ ctor.newInstance(transformedPredicate).asInstanceOf[BasePredicate]
+ } catch {
+ case _: NoSuchMethodException | _: IllegalArgumentException =>
+ // Fallback: Try using 2-arg constructor for certain Spark runtime
+ val clazz =
Class.forName("org.apache.spark.sql.catalyst.expressions.InterpretedPredicate")
+ val ctor = clazz.getConstructor(classOf[Expression],
classOf[Boolean])
+ ctor.newInstance(transformedPredicate, java.lang.Boolean.FALSE)
+ .asInstanceOf[BasePredicate]
+ }
+ val prunedPartitionPaths = partitionPaths.filter {
+ partitionPath =>
boundPredicate.eval(InternalRow.fromSeq(partitionPath.getValues))
+ }.toSeq
+
+ logInfo(s"Using provided predicates to prune number of target table's
partitions scanned from" +
+ s" ${partitionPaths.size} to ${prunedPartitionPaths.size}")
+
+ prunedPartitionPaths
+ } else {
+ logWarning(s"Unable to apply partition pruning, due to failure to parse
partition values from the" +
+ s" following path(s): ${partitionPaths.find(_.getValues.length ==
0).map(e => e.getPath)}")
+
+ partitionPaths.toSeq
}
}
diff --git
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestIncrementalReadWithPartitionFilter.scala
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestIncrementalReadWithPartitionFilter.scala
new file mode 100644
index 000000000000..564b49d2112f
--- /dev/null
+++
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestIncrementalReadWithPartitionFilter.scala
@@ -0,0 +1,131 @@
+/*
+ * 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.config.HoodieMetadataConfig
+import org.apache.hudi.common.model.HoodieTableType
+import org.apache.hudi.common.table.HoodieTableConfig
+import
org.apache.hudi.common.testutils.HoodieTestDataGenerator.{DEFAULT_FIRST_PARTITION_PATH,
DEFAULT_SECOND_PARTITION_PATH}
+import
org.apache.hudi.common.testutils.HoodieTestDataGenerator.recordsToStrings
+import org.apache.hudi.config.HoodieWriteConfig
+import org.apache.hudi.hadoop.fs.RecordingLocalFileSystem
+import org.apache.hudi.hadoop.fs.RecordingLocalFileSystem.Call
+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.EnumSource
+
+import scala.collection.JavaConverters._
+
+/**
+ * Verifies that incremental reads apply partition filters when the commits in
range touch several
+ * partitions. Spark removes partition-only predicates from the post-scan
filters and relies on the
+ * file index to prune, so the incremental file index must evaluate them
against the modified partitions.
+ * Also verifies that resolving the file slices of an incremental read does
not list the table.
+ */
+class TestIncrementalReadWithPartitionFilter extends HoodieSparkClientTestBase
{
+
+ var spark: SparkSession = null
+
+ 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"
+ )
+
+ @BeforeEach
+ override def setUp(): Unit = {
+ setTableName("hoodie_test")
+ initPath()
+ initSparkContexts()
+ spark = sqlContext.sparkSession
+ initTestDataGenerator()
+ initHoodieStorage()
+ }
+
+ @AfterEach
+ override def tearDown(): Unit = {
+ if (spark != null) {
+
RecordingLocalFileSystem.unregister(spark.sparkContext.hadoopConfiguration)
+ }
+ cleanupSparkContexts()
+ cleanupTestDataGenerator()
+ cleanupFileSystem()
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = classOf[HoodieTableType])
+ def testIncrementalReadWithPartitionFilter(tableType: HoodieTableType): Unit
= {
+ writeOneCommitPerPartition(tableType)
+ val incrementalDF = readIncrementally()
+
+ assertEquals(50, incrementalDF.count())
+ assertEquals(30, incrementalDF.where(s"partition =
'$DEFAULT_FIRST_PARTITION_PATH'").count())
+ assertEquals(20, incrementalDF.where(s"partition =
'$DEFAULT_SECOND_PARTITION_PATH'").count())
+ assertEquals(0, incrementalDF.where("partition = 'nonexistent'").count())
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = classOf[HoodieTableType])
+ def testIncrementalReadDoesNotListTable(tableType: HoodieTableType): Unit = {
+ writeOneCommitPerPartition(tableType)
+
+ // Record the file system calls of the read. With the metadata table
disabled, any partition
+ // listing shows up as a listStatus call on storage outside the .hoodie
folder.
+ RecordingLocalFileSystem.register(spark.sparkContext.hadoopConfiguration)
+ RecordingLocalFileSystem.reset()
+ assertEquals(50, readIncrementally(HoodieMetadataConfig.ENABLE.key ->
"false").count())
+
+ // The data files are opened through the recorder, so it is known to see
the calls under test.
+
assertTrue(RecordingLocalFileSystem.count(Call.operation("open").and(Call.pathEndsWith(".parquet")))
> 0)
+ val listings = Call.operation("listStatus",
"listLocatedStatus").and(Call.underMetaFolder().negate())
+ assertEquals(0, RecordingLocalFileSystem.count(listings),
RecordingLocalFileSystem.describe(listings))
+ }
+
+ // One commit per partition: 30 records in the first partition, 20 in the
second.
+ private def writeOneCommitPerPartition(tableType: HoodieTableType): Unit = {
+ Seq(("001", DEFAULT_FIRST_PARTITION_PATH, 30), ("002",
DEFAULT_SECOND_PARTITION_PATH, 20)).foreach {
+ case (instantTime, partition, numRecords) =>
+ val records =
recordsToStrings(dataGen.generateInsertsForPartition(instantTime, numRecords,
partition)).asScala.toList
+ spark.read.json(spark.sparkContext.parallelize(records,
2)).write.format("hudi")
+ .options(commonOpts)
+ .option(DataSourceWriteOptions.TABLE_TYPE.key, tableType.name())
+ .option(DataSourceWriteOptions.OPERATION.key,
DataSourceWriteOptions.INSERT_OPERATION_OPT_VAL)
+ .mode(SaveMode.Append)
+ .save(basePath)
+ }
+ }
+
+ // Reads from before the first commit, so every commit of the table is in
range.
+ private def readIncrementally(extraOpts: (String, String)*): DataFrame = {
+ spark.read.format("hudi")
+ .option(DataSourceReadOptions.QUERY_TYPE.key,
DataSourceReadOptions.QUERY_TYPE_INCREMENTAL_OPT_VAL)
+ .option(DataSourceReadOptions.START_COMMIT.key, "000")
+ // Keep the incremental listing path under test rather than the full
table scan fallback.
+
.option(DataSourceReadOptions.INCREMENTAL_FALLBACK_TO_FULL_TABLE_SCAN.key,
"false")
+ .options(extraOpts.toMap)
+ .load(basePath)
+ }
+}