Skip to content

feat(client): route merge-on-read updates as positional deletes and inserts - #20167

Draft
yihua wants to merge 9 commits into
apache:masterfrom
yihua:mor-update-as-delete-insert
Draft

yihua wants to merge 9 commits into
apache:masterfrom
yihua:mor-update-as-delete-insert

Conversation

@yihua

@yihua yihua commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

issue: #16073, #20175

Opened as a draft to capture the current state of the work. The design is written up in RFC-114 (#20181; number claimed in #20174).

An external table format (RFC-93) whose storage model is immutable data files plus positional delete files can only represent a merge-on-read table exactly if every live record sits in a base file and log files carry positional deletes alone. Hudi's upsert appends the new version of an updated record to the current file group's log, which such a format cannot express between compactions.

Summary and Changelog

  • New write config hoodie.write.updates.as.deletes.and.inserts (default false). On a merge-on-read table, an upsert routes each tagged update as a positional delete against the record's current file group plus an untagged insert that the insert partitioner routes to a fresh file slice. Log files then hold positional deletes only.
  • Multi-tagged records resolve to their latest location, so a key that already moved once is tombstoned in the right file group on the next update.
  • Small-file routing is kept out of this mode so an insert can never land in the file group that was just tombstoned within the same commit.
  • Fail-fast guards: event-time ordering, log compaction, an index type that yields no record positions (bucket, in-memory, record index for now) and disabled record positions are rejected while the mode is on.
  • HoodieTableFormat is now notified on compaction completion and on merge-on-read rollback, so an external table format plugin can drop the positional deletes of rewritten or rolled-back base files. Table format settings persist in the table config, and the file-system view ignores data files Hudi did not name. The view change is open for discussion: it breaks Apache XTable's Hudi conversion target, which registers source-named files as Hudi base files; RFC-114 records the better shape (hide foreign files through the format's own metadata).
  • Tests: TestHoodieJavaClientUpdatesAsDeletesAndInserts (decomposition, repeated updates of the same keys, default behavior unchanged, compaction after decomposed updates, the rejections), TestSparkUpdatesAsDeletesAndInserts through the Spark write client, and TestHoodieJavaClientMergeOnReadForTestFormat for the table-format hooks.

Impact

No behavior change unless the new config is enabled. Pluggable table format implementations now receive compaction and rollback notifications.

Risk Level

low: the write mode is opt-in and guarded; the table-format notifications are no-ops for the native format.

Documentation Update

The new config carries its description in HoodieWriteConfig. A website page will be needed once the feature is finalized.

Contributor's checklist

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

The table format commit hook fires for delta commits and clustering but not
for compaction or log compaction, so a pluggable table format never learns
that base files were rewritten. Adds the table format completion action to
the compaction and log compaction timeline transitions and invokes the hook
from CompactHelpers. The compaction transitions now also return the completed
instant carrying its completion time, matching the other transitions, since
the completion action consumes it.
…ts on merge-on-read tables

Adds hoodie.write.updates.as.deletes.and.inserts: when enabled on a MOR table,
an upsert rewrites each tagged update into a positional delete to the record's
current file group plus an insert of the new version routed by the insert
partitioner, so log files carry only positional deletes and every live record
stays in a base file. This makes the storage layout representable exactly by
table formats based on deletion vectors.
…rge-on-read instant

The merge-on-read rollback executor reverted the instant only in the native
timeline, so a pluggable table format never reverted its own metadata and was
left referencing files the rollback then deleted. Hoists the copy-on-write
executor's un-publish handling, which already reverts the table format and
handles instants still completed in the native timeline, into the base
rollback executor and uses it for both table types.
…en routing updates as deletes and inserts

Under hoodie.write.updates.as.deletes.and.inserts a key moves file groups on
every update while older file groups keep a tombstoned physical copy, so a
base-file-scanning index tags an incoming record once per remaining copy and
a repeated update of the same key fans out into duplicates. Reduce tagged
records by key keeping the copy with the latest location before decomposing,
so exactly one positional delete and one insert are produced per key.
…rt write mode

Small-file handling routes merge-on-read updates through a merge handle and
assigns inserts into existing file groups, which silently bypasses the
positional-delete log path the write mode requires. With the mode enabled the
delta commit executors always append updates and the upsert partitioners stop
offering small files, so inserts land in base files of new file groups.
…e Spark write path

Exercises decomposition, repeated updates of the same keys, and the
small-file routing gates through SparkRDDWriteClient on a merge-on-read
table, asserting one positional delete and one insert per key per round and
delete-only log files.
…insert write mode

The decomposed delete unconditionally tombstones the record's current
version, so under event-time ordering a late-arriving update with a lower
ordering value would incorrectly win; the write path now rejects the mode
unless the table uses commit-time ordering. Log compaction scheduling is also
rejected since it rewrites delete blocks and breaks the delete-only,
positionally consistent log layout the mode maintains.
…iew; persist pluggable table format settings

A table shared with another table format can hold data files with foreign
names; they belong to no file group and made buildFileGroups fail on a null
file id. TableBuilder.set now keeps hoodie.table.format.* keys so a
pluggable table format receives its settings from the first commit on.
…ions in the update-as-delete-insert write mode
@codecov-commenter

codecov-commenter commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.24590% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.42%. Comparing base (40a41c9) to head (a9fc7ea).
⚠️ Report is 114 commits behind head on master.

Files with missing lines Patch % Lines
...ache/hudi/table/action/commit/BaseWriteHelper.java 84.00% 2 Missing and 2 partials ⚠️
...le/action/rollback/BaseRollbackActionExecutor.java 76.92% 2 Missing and 1 partial ⚠️
...table/timeline/versioning/v1/ActiveTimelineV1.java 50.00% 3 Missing ⚠️
...tion/compact/ScheduleCompactionActionExecutor.java 33.33% 0 Missing and 2 partials ⚠️
...he/hudi/table/action/commit/HoodieWriteHelper.java 93.75% 0 Missing and 1 partial ⚠️
...udi/table/action/commit/JavaUpsertPartitioner.java 66.66% 0 Missing and 1 partial ⚠️
...ache/hudi/table/action/commit/JavaWriteHelper.java 93.33% 0 Missing and 1 partial ⚠️
...deltacommit/BaseJavaDeltaCommitActionExecutor.java 0.00% 0 Missing and 1 partial ⚠️
...eltacommit/BaseSparkDeltaCommitActionExecutor.java 0.00% 0 Missing and 1 partial ⚠️
...pache/hudi/common/table/HoodieTableMetaClient.java 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #20167      +/-   ##
============================================
+ Coverage     78.31%   80.42%   +2.10%     
- Complexity    33929    34935    +1006     
============================================
  Files          2544     2547       +3     
  Lines        141886   143034    +1148     
  Branches      17213    18728    +1515     
============================================
+ Hits         111125   115041    +3916     
+ Misses        23056    20072    -2984     
- Partials       7705     7921     +216     
Components Coverage Δ
hudi-common 83.97% <82.60%> (+0.28%) ⬆️
hudi-client 83.51% <85.85%> (+0.28%) ⬆️
hudi-flink 85.74% <ø> (+0.18%) ⬆️
hudi-spark-datasource 73.85% <ø> (+0.61%) ⬆️
hudi-utilities 78.18% <ø> (+3.63%) ⬆️
hudi-cli 70.05% <ø> (+54.98%) ⬆️
hudi-hadoop 70.98% <ø> (+0.17%) ⬆️
hudi-sync 76.15% <ø> (+0.58%) ⬆️
hudi-io 81.50% <ø> (+1.47%) ⬆️
hudi-timeline-service 83.06% <ø> (-0.39%) ⬇️
hudi-cloud 81.00% <ø> (+15.18%) ⬆️
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 52.34% <35.24%> (+0.87%) ⬆️
flink-integration-tests 49.44% <41.00%> (+0.60%) ⬆️
hadoop-mr-java-client 44.11% <59.66%> (+0.01%) ⬆️
integration-tests 13.45% <12.29%> (-0.07%) ⬇️
spark-client-hadoop-common 38.60% <53.27%> (-11.97%) ⬇️
spark-java-tests 52.38% <39.34%> (+0.13%) ⬆️
spark-scala-tests 47.04% <31.14%> (+0.02%) ⬆️
utilities 36.84% <30.32%> (+0.27%) ⬆️

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

Files with missing lines Coverage Δ
...java/org/apache/hudi/config/HoodieWriteConfig.java 92.60% <100.00%> (+0.11%) ⬆️
...ache/hudi/table/action/compact/CompactHelpers.java 86.00% <100.00%> (+1.21%) ⬆️
...on/rollback/CopyOnWriteRollbackActionExecutor.java 100.00% <100.00%> (+12.00%) ⬆️
...on/rollback/MergeOnReadRollbackActionExecutor.java 100.00% <100.00%> (ø)
...deltacommit/SparkUpsertDeltaCommitPartitioner.java 90.90% <100.00%> (+0.43%) ⬆️
...table/timeline/versioning/v2/ActiveTimelineV2.java 83.37% <100.00%> (+0.18%) ⬆️
...common/table/view/AbstractTableFileSystemView.java 89.58% <100.00%> (+0.34%) ⬆️
...he/hudi/table/action/commit/HoodieWriteHelper.java 96.87% <93.75%> (-3.13%) ⬇️
...udi/table/action/commit/JavaUpsertPartitioner.java 91.02% <66.66%> (-0.48%) ⬇️
...ache/hudi/table/action/commit/JavaWriteHelper.java 96.77% <93.33%> (-3.23%) ⬇️
... and 7 more

... and 297 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.

@hudi-bot

hudi-bot commented Oct 1, 2026

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L PR with lines of changes in (300, 1000]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants