This is an automated email from the ASF dual-hosted git repository.
danny0405 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 2d73fe40dd8a feat(spark): support bucket index for LSM tables (#19727)
2d73fe40dd8a is described below
commit 2d73fe40dd8a106af58454139484cbff64aca899
Author: Shuo Cheng <[email protected]>
AuthorDate: Fri Aug 28 11:24:46 2026 +0800
feat(spark): support bucket index for LSM tables (#19727)
* feat(spark): support bucket index for LSM tables
---
.../table/BucketSortBulkInsertPartitioner.java | 6 ++
.../BucketIndexBulkInsertPartitionerWithRows.java | 37 +++++--
.../bulkinsert/RDDBucketIndexPartitioner.java | 5 +-
.../apache/spark/sql/BucketPartitionUtils.scala | 42 +++++++-
.../BaseDatasetBulkInsertCommitActionExecutor.java | 4 +-
.../DatasetBucketRescaleCommitActionExecutor.java | 4 +-
.../bulkinsert/TestLSMBulkInsertPartitioner.java | 40 +++++++
.../apache/hudi/functional/TestLSMDataSource.scala | 120 ++++++++++++++++++++-
8 files changed, 242 insertions(+), 16 deletions(-)
diff --git
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/BucketSortBulkInsertPartitioner.java
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/BucketSortBulkInsertPartitioner.java
index 9dbb2e96be36..d9b4cee7bfe8 100644
---
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/BucketSortBulkInsertPartitioner.java
+++
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/BucketSortBulkInsertPartitioner.java
@@ -19,6 +19,7 @@
package org.apache.hudi.table;
import org.apache.hudi.common.util.StringUtils;
+import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.execution.bulkinsert.BulkInsertSortMode;
/**
@@ -32,6 +33,11 @@ public abstract class BucketSortBulkInsertPartitioner<T>
implements BulkInsertPa
public BucketSortBulkInsertPartitioner(HoodieTable table, String sortString)
{
this.table = table;
+ if (table.getMetaClient().getTableConfig().isLSMTreeStorageLayout()
+ && !StringUtils.isNullOrEmpty(sortString)) {
+ throw new HoodieException("Custom sort columns are not supported for
bucket index on LSM tables because "
+ + "LSM files must be ordered by record key");
+ }
if (!StringUtils.isNullOrEmpty(sortString)) {
this.sortColumnNames = sortString.split(",");
} else {
diff --git
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/execution/bulkinsert/BucketIndexBulkInsertPartitionerWithRows.java
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/execution/bulkinsert/BucketIndexBulkInsertPartitionerWithRows.java
index 520427dbd458..39e8b0e768ba 100644
---
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/execution/bulkinsert/BucketIndexBulkInsertPartitionerWithRows.java
+++
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/execution/bulkinsert/BucketIndexBulkInsertPartitionerWithRows.java
@@ -36,20 +36,44 @@ public class BucketIndexBulkInsertPartitionerWithRows
implements BulkInsertParti
private final String indexKeyFields;
private final NumBucketsFunction numBucketsFunction;
private final HoodieWriteConfig writeConfig;
+ private final boolean sortByRecordKey;
private FileSystemViewStorageConfig viewConfig;
- public BucketIndexBulkInsertPartitionerWithRows(String indexKeyFields,
HoodieWriteConfig writeConfig) {
- this(writeConfig, NumBucketsFunction.fromWriteConfig(writeConfig),
indexKeyFields);
+ public BucketIndexBulkInsertPartitionerWithRows(String indexKeyFields,
+ HoodieWriteConfig
writeConfig) {
+ this(indexKeyFields, writeConfig, false);
}
- public BucketIndexBulkInsertPartitionerWithRows(HoodieWriteConfig
writeConfig, String expressions, String rule, int bucketNumber) {
- this(writeConfig, new NumBucketsFunction(expressions, rule, bucketNumber),
writeConfig.getBucketIndexHashFieldWithDefault());
+ public BucketIndexBulkInsertPartitionerWithRows(String indexKeyFields,
+ HoodieWriteConfig
writeConfig,
+ boolean sortByRecordKey) {
+ this(writeConfig, NumBucketsFunction.fromWriteConfig(writeConfig),
indexKeyFields, sortByRecordKey);
}
- private BucketIndexBulkInsertPartitionerWithRows(HoodieWriteConfig
writeConfig, NumBucketsFunction numBucketsFunction, String indexKeyFields) {
+ public BucketIndexBulkInsertPartitionerWithRows(HoodieWriteConfig
writeConfig,
+ String expressions,
+ String rule,
+ int bucketNumber) {
+ this(writeConfig, expressions, rule, bucketNumber, false);
+ }
+
+ public BucketIndexBulkInsertPartitionerWithRows(HoodieWriteConfig
writeConfig,
+ String expressions,
+ String rule,
+ int bucketNumber,
+ boolean sortByRecordKey) {
+ this(writeConfig, new NumBucketsFunction(expressions, rule, bucketNumber),
+ writeConfig.getBucketIndexHashFieldWithDefault(), sortByRecordKey);
+ }
+
+ private BucketIndexBulkInsertPartitionerWithRows(HoodieWriteConfig
writeConfig,
+ NumBucketsFunction
numBucketsFunction,
+ String indexKeyFields,
+ boolean sortByRecordKey) {
this.indexKeyFields = indexKeyFields;
this.numBucketsFunction = numBucketsFunction;
this.writeConfig = writeConfig;
+ this.sortByRecordKey = sortByRecordKey;
if (writeConfig.isUsingRemotePartitioner()) {
this.viewConfig = writeConfig.getViewStorageConfig();
}
@@ -60,7 +84,8 @@ public class BucketIndexBulkInsertPartitionerWithRows
implements BulkInsertParti
Partitioner partitioner = writeConfig.isUsingRemotePartitioner() &&
writeConfig.isEmbeddedTimelineServerEnabled()
? BucketPartitionUtils$.MODULE$.getRemotePartitioner(viewConfig,
numBucketsFunction, outputPartitions)
:
BucketPartitionUtils$.MODULE$.getLocalePartitioner(numBucketsFunction,
outputPartitions);
- return BucketPartitionUtils$.MODULE$.createDataFrame(rows, indexKeyFields,
numBucketsFunction, partitioner);
+ return BucketPartitionUtils$.MODULE$.createDataFrame(
+ rows, indexKeyFields, numBucketsFunction, partitioner,
sortByRecordKey);
}
@Override
diff --git
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/execution/bulkinsert/RDDBucketIndexPartitioner.java
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/execution/bulkinsert/RDDBucketIndexPartitioner.java
index 5239522cc6aa..9eef249d6738 100644
---
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/execution/bulkinsert/RDDBucketIndexPartitioner.java
+++
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/execution/bulkinsert/RDDBucketIndexPartitioner.java
@@ -23,6 +23,7 @@ import org.apache.hudi.common.model.HoodieKey;
import org.apache.hudi.common.model.HoodieRecord;
import org.apache.hudi.common.schema.HoodieSchema;
import org.apache.hudi.common.schema.HoodieSchemaUtils;
+import org.apache.hudi.common.util.StringUtils;
import org.apache.hudi.common.util.collection.FlatLists;
import org.apache.hudi.table.BucketIndexBulkInsertPartitioner;
import org.apache.hudi.table.HoodieTable;
@@ -121,7 +122,9 @@ public abstract class RDDBucketIndexPartitioner<T> extends
BucketIndexBulkInsert
LOG.warn("Bucket index does not support global sort mode, the sort will
only be done within each data partition");
}
- Comparator<HoodieKey> comparator = (Comparator<HoodieKey> & Serializable)
(t1, t2) -> t1.getRecordKey().compareTo(t2.getRecordKey());
+ Comparator<HoodieKey> comparator =
table.getMetaClient().getTableConfig().isLSMTreeStorageLayout()
+ ? (Comparator<HoodieKey> & Serializable) (t1, t2) ->
StringUtils.compareUtf8Bytes(t1.getRecordKey(), t2.getRecordKey())
+ : (Comparator<HoodieKey> & Serializable) (t1, t2) ->
t1.getRecordKey().compareTo(t2.getRecordKey());
return records.mapToPair(record -> new Tuple2<>(record.getKey(), record))
.repartitionAndSortWithinPartitions(partitioner, comparator)
diff --git
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/BucketPartitionUtils.scala
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/BucketPartitionUtils.scala
index 14aff571c238..805dfe8ed566 100644
---
a/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/BucketPartitionUtils.scala
+++
b/hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/BucketPartitionUtils.scala
@@ -18,7 +18,7 @@
package org.apache.spark.sql
-import org.apache.hudi.SparkAdapterSupport
+import org.apache.hudi.{HoodieUTF8String, SparkAdapterSupport}
import org.apache.hudi.common.model.HoodieRecord
import org.apache.hudi.common.table.view.FileSystemViewStorageConfig
import org.apache.hudi.common.util.{Functions, RemotePartitionHelper}
@@ -32,6 +32,14 @@ import org.apache.spark.sql.catalyst.InternalRow
object BucketPartitionUtils extends SparkAdapterSupport {
def createDataFrame(df: DataFrame, indexKeyFields: String,
numBucketsFunction: NumBucketsFunction, partitioner: Partitioner): DataFrame = {
+ createDataFrame(df, indexKeyFields, numBucketsFunction, partitioner,
sortByRecordKey = false)
+ }
+
+ def createDataFrame(df: DataFrame,
+ indexKeyFields: String,
+ numBucketsFunction: NumBucketsFunction,
+ partitioner: Partitioner,
+ sortByRecordKey: Boolean): DataFrame = {
// parse the comma-separated config once outside the per-row closure; the
list is a
// serializable java.util.List, safe to capture
val indexKeyFieldList = KeyGenUtils.getIndexKeyFields(indexKeyFields)
@@ -49,10 +57,34 @@ object BucketPartitionUtils extends SparkAdapterSupport {
val getPartitionKey = getPartitionKeyExtractor()
// use internalRow to avoid extra convert.
- val reRdd = df.queryExecution.toRdd
- .keyBy(row => getPartitionKey(row))
- .repartitionAndSortWithinPartitions(partitioner)
- .values
+ val internalRows = df.queryExecution.toRdd
+ val reRdd = if (sortByRecordKey) {
+ val utf8StringFactory = sparkAdapter.getUTF8StringFactory
+ // Use (bucket route, record key) as the shuffle key. Tuple ordering
groups rows by
+ // (partition path, bucket id) first, then orders each bucket by the
full record key.
+ // Scala derives the record-key ordering from HoodieUTF8String's
Comparable implementation,
+ // which uses binary UTF-8 ordering for both Spark 3 and Spark 4.
+ val keyedRows = internalRows.keyBy(row => {
+ val recordKey = utf8StringFactory.wrapUTF8String(
+ row.getUTF8String(HoodieRecord.RECORD_KEY_META_FIELD_ORD))
+ (getPartitionKey(row), recordKey)
+ })
+ // The record key participates only in sorting; bucket routing remains
unchanged.
+ val bucketRoutePartitioner = new Partitioner {
+ override def numPartitions: Int = partitioner.numPartitions
+
+ override def getPartition(key: Any): Int = {
+ val bucketRoute = key.asInstanceOf[((String, Int),
HoodieUTF8String)]._1
+ partitioner.getPartition(bucketRoute)
+ }
+ }
+
keyedRows.repartitionAndSortWithinPartitions(bucketRoutePartitioner).values
+ } else {
+ internalRows
+ .keyBy(row => getPartitionKey(row))
+ .repartitionAndSortWithinPartitions(partitioner)
+ .values
+ }
sparkAdapter.internalCreateDataFrame(df.sparkSession, reRdd, df.schema)
}
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/main/java/org/apache/hudi/commit/BaseDatasetBulkInsertCommitActionExecutor.java
b/hudi-spark-datasource/hudi-spark-common/src/main/java/org/apache/hudi/commit/BaseDatasetBulkInsertCommitActionExecutor.java
index e591eb272768..bdc1d2bc7380 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/main/java/org/apache/hudi/commit/BaseDatasetBulkInsertCommitActionExecutor.java
+++
b/hudi-spark-datasource/hudi-spark-common/src/main/java/org/apache/hudi/commit/BaseDatasetBulkInsertCommitActionExecutor.java
@@ -141,7 +141,9 @@ public abstract class
BaseDatasetBulkInsertCommitActionExecutor implements Seria
if (populateMetaFields) {
if (writeConfig.getIndexType() == HoodieIndex.IndexType.BUCKET) {
if (writeConfig.getBucketIndexEngineType() ==
HoodieIndex.BucketIndexEngineType.SIMPLE) {
- return new
BucketIndexBulkInsertPartitionerWithRows(writeConfig.getBucketIndexHashFieldWithDefault(),
table.getConfig());
+ return new BucketIndexBulkInsertPartitionerWithRows(
+ writeConfig.getBucketIndexHashFieldWithDefault(),
table.getConfig(),
+ table.getMetaClient().getTableConfig().isLSMTreeStorageLayout());
} else {
return new ConsistentBucketIndexBulkInsertPartitionerWithRows(table,
Collections.emptyMap(), true);
}
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/main/java/org/apache/hudi/commit/DatasetBucketRescaleCommitActionExecutor.java
b/hudi-spark-datasource/hudi-spark-common/src/main/java/org/apache/hudi/commit/DatasetBucketRescaleCommitActionExecutor.java
index 9f9e9ae9288f..34040e1f26c0 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/main/java/org/apache/hudi/commit/DatasetBucketRescaleCommitActionExecutor.java
+++
b/hudi-spark-datasource/hudi-spark-common/src/main/java/org/apache/hudi/commit/DatasetBucketRescaleCommitActionExecutor.java
@@ -57,7 +57,9 @@ public class DatasetBucketRescaleCommitActionExecutor extends
DatasetBulkInsertO
*/
@Override
protected BulkInsertPartitioner<Dataset<Row>> getPartitioner(boolean
populateMetaFields, boolean isTablePartitioned) {
- return new
BucketIndexBulkInsertPartitionerWithRows(writeClient.getConfig(), expression,
rule, bucketNumber);
+ return new BucketIndexBulkInsertPartitionerWithRows(
+ writeClient.getConfig(), expression, rule, bucketNumber,
+ table.getMetaClient().getTableConfig().isLSMTreeStorageLayout());
}
/**
diff --git
a/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/execution/bulkinsert/TestLSMBulkInsertPartitioner.java
b/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/execution/bulkinsert/TestLSMBulkInsertPartitioner.java
index 4c56005b4e31..65661f813c07 100644
---
a/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/execution/bulkinsert/TestLSMBulkInsertPartitioner.java
+++
b/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/execution/bulkinsert/TestLSMBulkInsertPartitioner.java
@@ -24,6 +24,7 @@ import org.apache.hudi.common.model.HoodieRecord;
import org.apache.hudi.common.table.HoodieTableConfig;
import org.apache.hudi.common.table.HoodieTableMetaClient;
import org.apache.hudi.common.util.StringUtils;
+import org.apache.hudi.config.HoodieIndexConfig;
import org.apache.hudi.config.HoodieWriteConfig;
import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.table.BulkInsertPartitioner;
@@ -132,6 +133,36 @@ public class TestLSMBulkInsertPartitioner extends
HoodieSparkClientTestHarness {
assertTrue(partitioner.arePartitionRecordsSorted());
}
+ @Test
+ void
testSimpleBucketRowPartitionerSortsByUtf8RecordKeyWithoutChangingSchema() {
+ StructType schema = new StructType()
+ .add(HoodieRecord.COMMIT_TIME_METADATA_FIELD, DataTypes.StringType,
false)
+ .add(HoodieRecord.COMMIT_SEQNO_METADATA_FIELD, DataTypes.StringType,
false)
+ .add(HoodieRecord.RECORD_KEY_METADATA_FIELD, DataTypes.StringType,
false)
+ .add(HoodieRecord.PARTITION_PATH_METADATA_FIELD, DataTypes.StringType,
false)
+ .add(HoodieRecord.FILENAME_METADATA_FIELD, DataTypes.StringType, false)
+ .add("value", DataTypes.IntegerType, false);
+ Dataset<Row> input = sqlContext.createDataFrame(
+ jsc.parallelize(createRowsWithMetaFields(), 3), schema);
+ HoodieWriteConfig config = createWriteConfig(BulkInsertSortMode.NONE,
true);
+ config.setValue(HoodieIndexConfig.BUCKET_INDEX_HASH_FIELD, "id");
+ config.setValue(HoodieIndexConfig.BUCKET_INDEX_NUM_BUCKETS, "1");
+
+ List<BulkInsertPartitioner<Dataset<Row>>> partitioners = Arrays.asList(
+ new BucketIndexBulkInsertPartitionerWithRows("id", config, true),
+ new BucketIndexBulkInsertPartitionerWithRows(config, "p1|p2|p3,1",
"regex", 1, true));
+
+ for (BulkInsertPartitioner<Dataset<Row>> partitioner : partitioners) {
+ Dataset<Row> actual = partitioner.repartitionRecords(input, 1);
+
+ assertEquals(schema, actual.schema(), "Sorting must not add temporary
columns");
+ assertEquals(1, actual.javaRDD().getNumPartitions());
+ assertSortedSparkPartitions(actual.javaRDD().glom().collect(), row ->
new Tuple2<>(
+ row.getAs(HoodieRecord.PARTITION_PATH_METADATA_FIELD),
+ row.getAs(HoodieRecord.RECORD_KEY_METADATA_FIELD)));
+ }
+ }
+
@ParameterizedTest
@EnumSource(value = BulkInsertSortMode.class, names = {
"GLOBAL_SORT", "PARTITION_SORT", "PARTITION_PATH_REPARTITION_AND_SORT"})
@@ -294,6 +325,15 @@ public class TestLSMBulkInsertPartitioner extends
HoodieSparkClientTestHarness {
return rows;
}
+ private List<Row> createRowsWithMetaFields() {
+ List<Row> rows = new ArrayList<>();
+ int value = 0;
+ for (Tuple2<String, String> key : createKeys()) {
+ rows.add(RowFactory.create("001", "001_0", key._2, key._1, "", value++));
+ }
+ return rows;
+ }
+
private List<Tuple2<String, String>> createKeys() {
String bmpPrivateUse = new String(Character.toChars(0xE000));
String supplementary = new String(Character.toChars(0x20000));
diff --git
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestLSMDataSource.scala
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestLSMDataSource.scala
index cac27b42c61f..92382c9a165b 100644
---
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestLSMDataSource.scala
+++
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestLSMDataSource.scala
@@ -20,14 +20,17 @@ package org.apache.hudi.functional
import org.apache.hudi.{DataSourceUtils, DataSourceWriteOptions}
import org.apache.hudi.client.SparkRDDWriteClient
import org.apache.hudi.common.config.HoodieStorageConfig
-import org.apache.hudi.common.model.{HoodieBaseFile, HoodieRecord,
HoodieRecordPayload, HoodieTableType, WriteOperationType}
+import org.apache.hudi.common.fs.{FileNameParser, FSUtils}
+import org.apache.hudi.common.model.{HoodieBaseFile,
HoodieConsistentHashingMetadata, HoodieRecord, HoodieRecordPayload,
HoodieTableType, WriteOperationType}
import org.apache.hudi.common.table.{HoodieTableConfig, HoodieTableMetaClient}
import org.apache.hudi.common.testutils.HoodieTestUtils
import org.apache.hudi.common.util.Option
import org.apache.hudi.common.util.StringUtils
-import org.apache.hudi.config.{HoodieCompactionConfig, HoodieWriteConfig}
+import org.apache.hudi.config.{HoodieCompactionConfig, HoodieIndexConfig,
HoodieWriteConfig}
import org.apache.hudi.exception.HoodieException
import org.apache.hudi.execution.bulkinsert.{BulkInsertSortMode,
RowCustomColumnsSortPartitioner}
+import org.apache.hudi.index.HoodieIndex
+import org.apache.hudi.index.bucket.{BucketIdentifier,
ConsistentBucketIdentifier}
import org.apache.hudi.testutils.{HoodieClientTestUtils,
SparkClientFunctionalTestHarness}
import
org.apache.hudi.testutils.SparkClientFunctionalTestHarness.getSparkSqlConf
@@ -175,6 +178,53 @@ class TestLSMDataSource extends
SparkClientFunctionalTestHarness {
"A-row-p2" -> "v1"))
}
+ @ParameterizedTest
+ @MethodSource(Array("bucketBulkInsertParams"))
+ def testBucketIndexBulkInsert(
+ tableType: HoodieTableType,
+ bucketEngineType: HoodieIndex.BucketIndexEngineType,
+ enableRowWriter: Boolean): Unit = {
+ val numBuckets = 2
+ val tablePath =
s"${basePath}_${tableType.name.toLowerCase}_${bucketEngineType.name.toLowerCase}_$enableRowWriter"
+ val options = baseOptions(tableType) ++ Map(
+ DataSourceWriteOptions.ENABLE_ROW_WRITER.key -> enableRowWriter.toString,
+ HoodieWriteConfig.BULK_INSERT_SORT_MODE.key ->
BulkInsertSortMode.NONE.name,
+ HoodieIndexConfig.INDEX_TYPE.key -> HoodieIndex.IndexType.BUCKET.name,
+ HoodieIndexConfig.BUCKET_INDEX_ENGINE_TYPE.key -> bucketEngineType.name,
+ HoodieIndexConfig.BUCKET_INDEX_HASH_FIELD.key -> "id",
+ HoodieIndexConfig.BUCKET_INDEX_NUM_BUCKETS.key -> numBuckets.toString)
+ val insertValues = Seq(
+ ("😀-bucket-p1", "v1", 1L, FirstPartition),
+ ("A-bucket-p1", "v1", 1L, FirstPartition),
+ ("middle-bucket-p1", "v1", 1L, FirstPartition),
+ ("😀-bucket-p2", "v1", 1L, SecondPartition),
+ ("A-bucket-p2", "v1", 1L, SecondPartition)) ++
+ routingRecords(bucketEngineType, numBuckets)
+ val inserts = rows(insertValues)
+
+ write(inserts, tablePath, options, WriteOperationType.BULK_INSERT,
SaveMode.Overwrite)
+
+ assertLatestBaseFilesSorted(tablePath, WriteOperationType.BULK_INSERT)
+ assertBucketFileGroupRouting(tablePath, bucketEngineType, numBuckets)
+ val initialFileIds = latestBaseFiles(tablePath).map(_.getFileId).toSet
+ assertEquals(numBuckets * 2, initialFileIds.size)
+ assertSnapshot(tablePath, insertValues.map(value => value._1 ->
value._2).toMap)
+
+ val upsertValues = Seq(
+ ("😀-bucket-p1", "v2", 2L, FirstPartition),
+ ("A-bucket-p1", "v2", 2L, FirstPartition),
+ ("middle-bucket-p1", "v2", 2L, FirstPartition),
+ ("ascii-new-p1", "new", 2L, FirstPartition))
+ val upserts = rows(upsertValues)
+ write(upserts, tablePath, options, WriteOperationType.UPSERT)
+
+ assertChangedFilesSorted(tablePath, tableType, WriteOperationType.UPSERT,
"log")
+ assertEquals(initialFileIds,
latestBaseFiles(tablePath).map(_.getFileId).toSet)
+ assertSnapshot(tablePath,
+ insertValues.map(value => value._1 -> value._2).toMap ++
+ upsertValues.map(value => value._1 -> value._2).toMap)
+ }
+
@ParameterizedTest
@EnumSource(value = classOf[BulkInsertSortMode], names = Array(
"NONE", "PARTITION_PATH_REPARTITION"))
@@ -412,6 +462,63 @@ class TestLSMDataSource extends
SparkClientFunctionalTestHarness {
}
}
+ private def assertBucketFileGroupRouting(
+ tablePath: String,
+ bucketEngineType: HoodieIndex.BucketIndexEngineType,
+ numBuckets: Int): Unit = {
+ val actualFileIdsByPartition = spark.read.format("hudi").load(tablePath)
+ .select("id", HoodieRecord.PARTITION_PATH_METADATA_FIELD,
HoodieRecord.FILENAME_METADATA_FIELD)
+ .collect()
+ .map { row =>
+ val recordKey = row.getString(0)
+ val partitionPath = row.getString(1)
+ val fileId = FSUtils.getFileId(row.getString(2))
+ val actualFileGroup = bucketEngineType match {
+ case HoodieIndex.BucketIndexEngineType.SIMPLE =>
+
BucketIdentifier.bucketIdStr(BucketIdentifier.bucketIdFromFileId(fileId))
+ case HoodieIndex.BucketIndexEngineType.CONSISTENT_HASHING =>
+ FileNameParser.getFileIdPfxFromFileId(fileId)
+ }
+ assertEquals(expectedFileGroup(recordKey, partitionPath,
bucketEngineType, numBuckets), actualFileGroup,
+ s"Unexpected file group for record $recordKey in partition
$partitionPath")
+ partitionPath -> actualFileGroup
+ }
+ .groupBy(_._1)
+
+ Seq(FirstPartition, SecondPartition).foreach { partitionPath =>
+ assertEquals(numBuckets,
actualFileIdsByPartition(partitionPath).map(_._2).distinct.length,
+ s"Expected every bucket to be exercised in partition $partitionPath")
+ }
+ }
+
+ private def routingRecords(
+ bucketEngineType: HoodieIndex.BucketIndexEngineType,
+ numBuckets: Int): Seq[(String, String, Long, String)] = {
+ Seq(FirstPartition, SecondPartition).flatMap { partitionPath =>
+ val keysByFileGroup = (0 until 100)
+ .map(index => s"route-$index-$partitionPath")
+ .groupBy(recordKey => expectedFileGroup(recordKey, partitionPath,
bucketEngineType, numBuckets))
+ assertEquals(numBuckets, keysByFileGroup.size,
+ s"Could not generate a routing key for every bucket in partition
$partitionPath")
+ keysByFileGroup.values.map(_.head).toSeq
+ .map(recordKey => (recordKey, "v1", 1L, partitionPath))
+ }
+ }
+
+ private def expectedFileGroup(
+ recordKey: String,
+ partitionPath: String,
+ bucketEngineType: HoodieIndex.BucketIndexEngineType,
+ numBuckets: Int): String = {
+ bucketEngineType match {
+ case HoodieIndex.BucketIndexEngineType.SIMPLE =>
+ BucketIdentifier.bucketIdStr(BucketIdentifier.getBucketId(recordKey,
"id", numBuckets))
+ case HoodieIndex.BucketIndexEngineType.CONSISTENT_HASHING =>
+ new ConsistentBucketIdentifier(new
HoodieConsistentHashingMetadata(partitionPath, numBuckets))
+ .getBucket(recordKey, "id").getFileIdPrefix
+ }
+ }
+
private def assertSnapshot(tablePath: String, expected: Map[String,
String]): Unit = {
val actual = spark.read.format("hudi").load(tablePath)
.select("id", "value")
@@ -430,6 +537,15 @@ class TestLSMDataSource extends
SparkClientFunctionalTestHarness {
object TestLSMDataSource {
+ def bucketBulkInsertParams(): java.util.stream.Stream[Arguments] =
+ java.util.stream.Stream.of(
+ Arguments.of(HoodieTableType.COPY_ON_WRITE,
HoodieIndex.BucketIndexEngineType.SIMPLE, Boolean.box(false)),
+ Arguments.of(HoodieTableType.COPY_ON_WRITE,
HoodieIndex.BucketIndexEngineType.SIMPLE, Boolean.box(true)),
+ Arguments.of(HoodieTableType.MERGE_ON_READ,
HoodieIndex.BucketIndexEngineType.SIMPLE, Boolean.box(false)),
+ Arguments.of(HoodieTableType.MERGE_ON_READ,
HoodieIndex.BucketIndexEngineType.SIMPLE, Boolean.box(true)),
+ Arguments.of(HoodieTableType.MERGE_ON_READ,
HoodieIndex.BucketIndexEngineType.CONSISTENT_HASHING, Boolean.box(false)),
+ Arguments.of(HoodieTableType.MERGE_ON_READ,
HoodieIndex.BucketIndexEngineType.CONSISTENT_HASHING, Boolean.box(true)))
+
def bulkInsertWithHoodieRecordPathParams():
java.util.stream.Stream[Arguments] =
java.util.stream.Stream.of(
Arguments.of(HoodieTableType.COPY_ON_WRITE,
BulkInsertSortMode.GLOBAL_SORT),