Skip to content

fix(core): refuse to downgrade a table ordered on several fields below version 9 - #20348

Open
wombatu-kun wants to merge 2 commits into
apache:masterfrom
wombatu-kun:fix-downgrade-several-ordering-fields
Open

wombatu-kun wants to merge 2 commits into
apache:masterfrom
wombatu-kun:fix-downgrade-several-ordering-fields

Conversation

@wombatu-kun

@wombatu-kun wombatu-kun commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

Closes #20347

The 9 to 8 downgrade copied several ordering fields as they are into hoodie.table.precombine.field, which Hudi 1.0.x and 0.x read as a single field name, so the release it targets could not upsert into the downgraded table.

Summary and Changelog

  • UpgradeDowngrade.run refuses a downgrade from version 9 or above to below 9 of a table ordered on several fields, before the rollback, the compaction and the metadata table downgrade. The message names the fields, says why 1.0.x and 0.x cannot read them, and points to keeping the table at version 9 or recreating it with a single ordering field.
  • NineToEightDowngradeHandler holds the check, which only UpgradeDowngrade.run calls. The check uses the fields the downgrade would write, so MySqlDebeziumAvroPayload tables still go back to their single _event_seq, and the handler now writes the parsed field, so a stray separator as in ts, no longer lands in the legacy key.
  • TestNineToEightDowngradeHandler covers a single field, ts, and the MySQL Debezium mapping. TestUpgradeDowngradeOrchestration refuses 10 to 8 and 10 to 6 downgrades before any step and lets 10 to 9 with two fields and 10 to 8 for MySQL Debezium proceed. TestUpgradeDowngrade refuses the version 9 MOR fixture ordered on ts,name at versions 8 and 6 and checks that the data and metadata tables keep their version, timeline, ordering fields and log files.

Impact

A downgrade below version 9 of a table ordered on several fields now fails with Cannot downgrade to table version 8: hoodie.table.ordering.fields=ts,name orders records on several fields, which Hudi 1.0.x and 0.x cannot read. ... and leaves the table as it was. Before, it reported success and left hoodie.table.precombine.field=ts,name, a field that 1.0.2 upserts and MOR merges look for and do not find. Every other downgrade behaves as before. The downgrade_table procedure turns the failure into a false result and logs the message.

Risk Level

low

The check only rejects downgrades whose result was unusable, and it runs before anything changes. Verified: the refusal tests fail on master with Expected org.apache.hudi.exception.HoodieUpgradeDowngradeException to be thrown, but nothing was thrown., and the downgrade on master writes hoodie.table.precombine.field=ts,name, which Hudi 1.0.2 and 0.15.0 resolve as their precombine field.

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

@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 makes a downgrade below version 9 fail early, before any rollback, compaction or metadata table change, when the table orders records on several fields, and it now writes the parsed single field to the precombine key. 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 small comment-clarity suggestion below; otherwise the code reads cleanly.

cc @yihua

propertiesToAdd.put(HoodieTableConfig.PRECOMBINE_FIELD, orderingFields);
List<String> orderingFields = getOrderingFieldsBelowVersionNine(tableConfig);
if (orderingFields.size() == 1) {
// the parsed field, so that a stray separator as in `ts,` does not end up in a key read as one field name

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: could you reword this comment as a full sentence, e.g. "Write the parsed field rather than the raw string, so a stray separator as in ts, does not end up in the key"? As written it is a fragment that is hard to follow.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done aef75ce

@github-actions github-actions Bot added the size:M PR with lines of changes in (100, 300] label Oct 15, 2026
@codecov-commenter

codecov-commenter commented Oct 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.74%. Comparing base (616f9ae) to head (aef75ce).
⚠️ Report is 12 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #20348      +/-   ##
============================================
+ Coverage     80.73%   80.74%   +0.01%     
- Complexity    35169    35319     +150     
============================================
  Files          2553     2562       +9     
  Lines        143582   144150     +568     
  Branches      17486    17609     +123     
============================================
+ Hits         115920   116400     +480     
- Misses        19701    19777      +76     
- Partials       7961     7973      +12     
Components Coverage Δ
hudi-common 84.17% <ø> (+<0.01%) ⬆️
hudi-client 83.96% <100.00%> (+0.01%) ⬆️
hudi-flink 86.08% <ø> (+<0.01%) ⬆️
hudi-spark-datasource 74.19% <ø> (-0.01%) ⬇️
hudi-utilities 78.32% <ø> (+0.17%) ⬆️
hudi-cli 70.80% <ø> (ø)
hudi-hadoop 71.18% <ø> (+0.01%) ⬆️
hudi-sync 76.78% <ø> (+0.19%) ⬆️
hudi-io 81.48% <ø> (-0.05%) ⬇️
hudi-timeline-service 83.41% <ø> (ø)
hudi-cloud 81.01% <ø> (+0.01%) ⬆️
hudi-kafka-connect 57.90% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 52.69% <100.00%> (+0.06%) ⬆️
flink-integration-tests 49.64% <50.00%> (-0.01%) ⬇️
hadoop-mr-java-client 43.95% <0.00%> (+0.01%) ⬆️
integration-tests 13.40% <0.00%> (-0.05%) ⬇️
spark-client-hadoop-common 38.52% <35.71%> (-0.16%) ⬇️
spark-java-tests 52.60% <100.00%> (-0.22%) ⬇️
spark-scala-tests 47.54% <50.00%> (-0.21%) ⬇️
utilities 36.99% <0.00%> (+0.12%) ⬆️

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

Files with missing lines Coverage Δ
...udi/table/upgrade/NineToEightDowngradeHandler.java 100.00% <100.00%> (ø)
...rg/apache/hudi/table/upgrade/UpgradeDowngrade.java 82.26% <100.00%> (+0.17%) ⬆️

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

SupportsUpgradeDowngrade upgradeDowngradeHelper) {
final HoodieTable table = upgradeDowngradeHelper.getTable(config, context);
HoodieTableMetaClient metaClient = table.getMetaClient();
validateOrderingFields(metaClient.getTableConfig(), HoodieTableVersion.EIGHT);

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: UpgradeDowngrade.run already validates this with the real target version before any handler runs, so this call only fires when the handler is driven directly, and it hardcodes 8 even for a 10 to 6 request.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done aef75ce. Only UpgradeDowngrade.run checks now, so a 10 to 6 request reports version 6, which TestUpgradeDowngradeOrchestration now covers.

when(tableConfig.getOrderingFieldsStr()).thenReturn(Option.of("ts,seq"));
HoodieUpgradeDowngradeException exception = assertThrows(HoodieUpgradeDowngradeException.class,
() -> handler.downgrade(config, context, "anyInstant", upgradeDowngradeHelper));
assertEquals("Cannot downgrade to table version 8: hoodie.table.ordering.fields=ts,seq orders records on several fields, "

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: three tests across three modules assert this message verbatim, so any wording tweak breaks all of them - matching on the ordering.fields part like the orchestration test does would be enough.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done aef75ce. No test asserts the whole message now: this test went away with the handler check, and TestUpgradeDowngrade matches the same leading part as the orchestration test, target version included.

…owngrade.run and match the message prefix in tests

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:M PR with lines of changes in (100, 300]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Downgrade below version 9 records several ordering fields as a single precombine field

4 participants