Skip to content

feat(spark): support bucket index for LSM tables - #19727

Merged
danny0405 merged 4 commits into
apache:masterfrom
cshuo:spark-bucket-index-lsm-support
Aug 28, 2026
Merged

feat(spark): support bucket index for LSM tables#19727
danny0405 merged 4 commits into
apache:masterfrom
cshuo:spark-bucket-index-lsm-support

Conversation

@cshuo

@cshuo cshuo commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Describe the issue this Pull Request addresses

Closes #19439.

Spark bucket-index partitioners own file-group routing and file-id assignment, so LSM tables cannot replace them with the generic LSM bulk-insert partitioners. The existing bucket paths therefore need to preserve their routing behavior while guaranteeing UTF-8 record-key ordering inside every output bucket.

Summary and Changelog

  • Sort records within each LSM bucket by the full record key using UTF-8 ordering while preserving existing simple and consistent-hashing bucket routing and file-id assignment.
  • Enable the ordering for both RDD and Dataset Row bulk-insert paths, including the Dataset bucket-rescale path, without changing non-LSM ordering behavior or the Row schema.
  • Reject custom bucket sort columns for LSM tables before writes reach the inflight state.
  • Add unit and functional coverage for non-ASCII record keys, COW and MOR tables, simple and consistent-hashing bucket indexes, RDD and Row writers, subsequent upserts, and file-id stability.

Impact

This enables Spark LSM tables to use simple and consistent-hashing bucket indexes. It introduces no new public API or configuration. LSM Dataset Row simple-bucket writes now include the record key in the shuffle sort key, adding the key copy and comparisons required to maintain the LSM physical ordering invariant; other table layouts retain their existing behavior.

Risk Level

Medium. The change affects Spark bucket-index write partitioning and ordering. The risk is mitigated by 14 passing partitioner tests and 7 passing targeted functional cases covering routing, UTF-8 ordering, schema preservation, custom-sort rejection, file-id stability, and snapshot correctness after upsert. Compilation, the relevant Checkstyle and Scalastyle checks, and git diff --check also completed without violations.

Documentation Update

None.

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable

@github-actions github-actions Bot added the size:M PR with lines of changes in (100, 300] label Aug 25, 2026

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR enables Spark bucket indexes (simple and consistent-hashing) for LSM tables by sorting records within each bucket by full record key using UTF-8 byte ordering, across both the RDD and Dataset Row bulk-insert paths, while preserving existing bucket routing and file-id assignment. I traced the ordering consistency across all four partitioner paths and the base-file writer (all use UTF-8 binary ordering), confirmed bucket grouping is preserved in the new shuffle-key scheme, and verified custom-sort-column rejection is reached for every RDD/Row × simple/consistent combination. No correctness issues found. A few style/readability suggestions in the inline comments. Please take a look, and this should be ready for a Hudi committer or PMC member to take it from here. One naming/design nit on the null-sentinel call pattern.

cc @yihua

@codecov-commenter

codecov-commenter commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.48718% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.98%. Comparing base (e0fe585) to head (43c8b46).
⚠️ Report is 31 commits behind head on master.

Files with missing lines Patch % Lines
...sert/BucketIndexBulkInsertPartitionerWithRows.java 70.00% 3 Missing ⚠️
...he/hudi/table/BucketSortBulkInsertPartitioner.java 33.33% 1 Missing and 1 partial ⚠️
...la/org/apache/spark/sql/BucketPartitionUtils.scala 88.23% 1 Missing and 1 partial ⚠️
...xecution/bulkinsert/RDDBucketIndexPartitioner.java 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19727      +/-   ##
============================================
+ Coverage     77.88%   77.98%   +0.09%     
- Complexity    33264    33474     +210     
============================================
  Files          2533     2539       +6     
  Lines        140342   141010     +668     
  Branches      16912    17162     +250     
============================================
+ Hits         109310   109961     +651     
+ Misses        23401    23393       -8     
- Partials       7631     7656      +25     
Components Coverage Δ
hudi-common 83.50% <ø> (+0.14%) ⬆️
hudi-client 83.06% <75.75%> (+0.08%) ⬆️
hudi-flink 85.62% <ø> (+0.02%) ⬆️
hudi-spark-datasource 72.39% <100.00%> (+0.02%) ⬆️
hudi-utilities 74.39% <ø> (+0.06%) ⬆️
hudi-cli 15.06% <ø> (ø)
hudi-hadoop 69.23% <ø> (+0.14%) ⬆️
hudi-sync 75.54% <ø> (+0.04%) ⬆️
hudi-io 79.85% <ø> (+0.09%) ⬆️
hudi-timeline-service 83.44% <ø> (ø)
hudi-cloud 64.27% <ø> (-0.06%) ⬇️
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 51.05% <0.00%> (+0.10%) ⬆️
flink-integration-tests 48.98% <0.00%> (-0.13%) ⬇️
hadoop-mr-java-client 43.72% <0.00%> (-0.18%) ⬇️
integration-tests 13.56% <0.00%> (-0.05%) ⬇️
spark-client-hadoop-common 50.52% <0.00%> (-0.09%) ⬇️
spark-java-tests 52.10% <66.66%> (+0.14%) ⬆️
spark-scala-tests 46.65% <43.58%> (+0.14%) ⬆️
utilities 36.39% <0.00%> (-0.15%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...mit/BaseDatasetBulkInsertCommitActionExecutor.java 69.86% <100.00%> (+0.84%) ⬆️
...mmit/DatasetBucketRescaleCommitActionExecutor.java 100.00% <100.00%> (ø)
...xecution/bulkinsert/RDDBucketIndexPartitioner.java 90.32% <66.66%> (-2.79%) ⬇️
...he/hudi/table/BucketSortBulkInsertPartitioner.java 66.66% <33.33%> (-11.12%) ⬇️
...la/org/apache/spark/sql/BucketPartitionUtils.scala 87.50% <88.23%> (-0.97%) ⬇️
...sert/BucketIndexBulkInsertPartitionerWithRows.java 77.27% <70.00%> (-16.48%) ⬇️

... and 62 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cshuo
cshuo force-pushed the spark-bucket-index-lsm-support branch from d17fb84 to bd62bcb Compare August 25, 2026 07:17

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR extends Spark bucket-index bulk-insert to LSM tables by adding full-record-key UTF-8 ordering within each bucket across the RDD, simple-bucket Row, and rescale paths while preserving existing routing and file-id assignment. I traced the ordering logic and confirmed the three write paths use mutually consistent UTF-8 byte ordering (compareUtf8Bytes / UTF8String binaryCompare / Catalyst binary string sort), that the consistent-hashing LSM path still sorts via requireSortedRecords(), and that the InternalRow key is copied to avoid buffer aliasing. No new issues flagged from this automated pass beyond what other reviewers already raised (per-record comparator branch, rescale-path validation bypass, double validation, null sentinel). Please take a look at any inline comments, and this should be ready for a Hudi committer or PMC member to take it from here. A couple of small naming and API-clarity nits below.

cc @yihua

}

Comparator<HoodieKey> comparator = (Comparator<HoodieKey> & Serializable) (t1, t2) -> t1.getRecordKey().compareTo(t2.getRecordKey());
boolean lsmTable = table.getMetaClient().getTableConfig().isLSMTreeStorageLayout();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: since this is a boolean, could you prefix it with isisLsmTable — so it reads as a predicate consistently with the rest of the codebase?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

protected BulkInsertPartitioner<Dataset<Row>> getPartitioner(boolean populateMetaFields, boolean isTablePartitioned) {
if (populateMetaFields) {
if (writeConfig.getIndexType() == HoodieIndex.IndexType.BUCKET) {
BucketSortBulkInsertPartitioner.validateCustomSortColumns(table, null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: passing null explicitly here is a bit surprising — the method has a sortString parameter, but at this call site there's no sort string at all. Would an overload validateCustomSortColumns(HoodieTable table) (delegating to the existing one with null) make the intent clearer?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR adds simple and consistent-hashing bucket-index support for Spark LSM tables by sorting records within each bucket by UTF-8 record key while preserving existing bucket routing and file-id assignment. No new issues flagged from this automated pass — a Hudi committer or PMC member can take it from here for a final review. One minor simplification suggestion below; otherwise the code is clean and readable.

cc @yihua

}

Comparator<HoodieKey> comparator = (Comparator<HoodieKey> & Serializable) (t1, t2) -> t1.getRecordKey().compareTo(t2.getRecordKey());
Comparator<HoodieKey> comparator = table.getMetaClient().getTableConfig().isLSMTreeStorageLayout()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: the two ternary branches only differ in the comparison expression — could you extract a single Comparator<HoodieKey> that picks between StringUtils.compareUtf8Bytes and String.compareTo on the record keys to avoid duplicating the cast/lambda boilerplate?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Restore the pre-LSM public constructor overloads and add multi-bucket routing coverage for row and RDD writers.

Addresses review comments 3861784555 and 3861784758.

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR enables simple and consistent-hashing bucket indexes for Spark LSM tables by preserving existing bucket routing/file-id assignment while enforcing UTF-8 record-key ordering within each output bucket across the RDD, Dataset Row, and bucket-rescale paths. I traced the three LSM sort paths and confirmed they produce consistent binary-UTF-8 ordering and preserve file-group routing. No new critical correctness issues flagged from this automated pass — a Hudi committer or PMC member can take it from here for a final review.

cc @yihua

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR adds simple and consistent-hashing bucket-index support for Spark LSM tables by sorting records within each bucket by the full record key using binary UTF-8 ordering, across both the RDD and Dataset Row bulk-insert paths (including the rescale path), while preserving existing bucket routing and file-id assignment. I traced the three ordering paths (RDD compareUtf8Bytes, simple-bucket Row tuple ordering via HoodieUTF8String, and consistent-hashing Row sortWithinPartitions) and they agree on binary UTF-8 semantics; routing and file-id stability look preserved. No new correctness, concurrency, or resource issues flagged from this automated pass — a Hudi committer or PMC member can take it from here for a final review. A minor simplification suggestion below, otherwise clean.

cc @yihua

}

Comparator<HoodieKey> comparator = (Comparator<HoodieKey> & Serializable) (t1, t2) -> t1.getRecordKey().compareTo(t2.getRecordKey());
Comparator<HoodieKey> comparator = table.getMetaClient().getTableConfig().isLSMTreeStorageLayout()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: the two ternary branches differ only in the string-compare call — could you pull the boolean out and build a single comparator (e.g. select a BiFunction<String,String,Integer> and wrap it once) to avoid duplicating the lambda?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

@hudi-bot

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands @hudi-bot supports the following commands:
  • @hudi-bot run azure re-run the last Azure build

@danny0405
danny0405 merged commit 2d73fe4 into apache:master Aug 28, 2026
39 of 40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M PR with lines of changes in (100, 300]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Spark][LSM] Support bucket index write paths

5 participants