Skip to content

Core: Rebase replace transaction onto refreshed metadata on concurrent commit#16975

Open
Kurtiscwright wants to merge 46 commits into
apache:mainfrom
Kurtiscwright:replace-seq-reuse-repro
Open

Core: Rebase replace transaction onto refreshed metadata on concurrent commit#16975
Kurtiscwright wants to merge 46 commits into
apache:mainfrom
Kurtiscwright:replace-seq-reuse-repro

Conversation

@Kurtiscwright

@Kurtiscwright Kurtiscwright commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Fixes #16232
Rebased on PR #16975

Replacing a table replaces its schema, partitioning, and data, but it keeps the
table's history instead of deleting the table and making a new one. The old code
didn't do this when another commit landed first. While a replace was being
prepared, a second commit could update the table. The replace then reloaded the
latest table state to pass the catalog's conflict check, but still wrote out the
changes it had prepared at the start. The second commit's snapshots disappeared
from history, and if that commit had changed the schema, the replace could end
up pointing columns at IDs that no longer matched the data files.

Now, when the table has changed, the replace is re-applied on top of the latest
table state: the new schema, partitioning, sort order, location, and properties
are kept, but they sit on top of the other commit's snapshots, and the replace's
own new data is added after it. If the table no longer exists when the replace
commits (for example, it was dropped in the meantime), there is nothing to build
on, so the replace just creates the table fresh.

Add TableMetadata.buildReplacementPreservingIds. It keeps the field IDs the
replace schema already has, instead of matching columns up by name and handing
out new IDs. Matching by name can move a column to a different ID than its data
files were written with, which makes that data unreadable.

Two design notes:

  1. The replace gets its own re-apply path rather than reusing
    applyUpdates, because a replace's schema and partitioning live in the prepared
    metadata, not in the list of pending updates, so replaying only those updates
    would drop the replacement and leave the old schema in place.

  2. It uses buildReplacementPreservingIds rather than buildReplacement
    so a concurrent schema change can't shift the replace's field IDs away
    from its data files.

Add tests for the rebuilt replace, kept history, stable field IDs, and the
dropped-table case. Two REST-only tests used to expect a replace with an
unchanged schema or partitioning to fail on a spurious ID check; update them to
expect it to succeed.

Kurtis Wright and others added 2 commits June 26, 2026 17:10
…t commit

When a concurrent commit lands after a replace transaction is staged,
commitReplaceTransaction advanced base to the refreshed metadata to satisfy the
optimistic lock but committed the stale staged metadata. This dropped the
concurrent commit's snapshots from history and reused its sequence number,
violating sequence-number monotonicity. A replace is last-writer-wins on the
current schema, spec, and data, but it is not a drop-and-recreate: the table
must keep its history.

Rebuild the replacement on the refreshed base and replay the staged updates so
the concurrent commit's history is preserved and the replacement snapshot's
sequence number is re-derived from the refreshed base. Add
TableMetadata.buildReplacementPreservingIds, which keeps the staged schema's
field IDs instead of re-deriving them by name, so a concurrent schema change
cannot shift the IDs the staged data files were written against.

Add tests covering the rebase, history preservation, and field-id stability.
Update the two server-side conflict tests in CatalogTests, which previously
asserted a spurious last-assigned-id requirement failure, to assert the replace
succeeds as last-writer-wins.
Co-authored-by: Sreesh Maheshwar <maheshwarsreesh@gmail.com>

Copilot AI 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.

Pull request overview

This PR fixes a correctness bug in Core replace-table transactions where a concurrent commit could be silently overwritten. The new behavior rebases a staged replace onto refreshed table metadata when the base has advanced, preserving concurrent history (snapshots/schema/spec history) while still applying the replacement’s intended schema/spec/sort/location/properties and adding the replacement’s new data after the concurrent commit.

Changes:

  • Rebase BaseTransaction replace commits onto refreshed TableMetadata when concurrent commits advance the base, and replay staged updates on the rebuilt replacement.
  • Add TableMetadata.buildReplacementPreservingIds to rebuild replacements without reassigning schema field IDs by name.
  • Add/adjust tests to validate preserved history, stable field IDs, dropped-table handling, and updated REST/HMS expectations.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
hive-metastore/src/test/java/org/apache/iceberg/hive/TestHiveCreateReplaceTable.java Updates HMS tests to expect concurrent property preservation and relaxes spec equality to content equivalence.
core/src/test/java/org/apache/iceberg/TestReplaceTransaction.java Adds new replace-transaction concurrency tests (rebase behavior, schema history preservation, field ID stability, dropped-table case).
core/src/test/java/org/apache/iceberg/catalog/CatalogTests.java Updates REST/catalog concurrency tests to expect rebased replace to succeed instead of failing on spurious ID requirements.
core/src/main/java/org/apache/iceberg/TableMetadata.java Adds buildReplacementPreservingIds to rebuild replacements while keeping pre-assigned schema field IDs.
core/src/main/java/org/apache/iceberg/BaseTransaction.java Introduces staged replacement tracking and the rebase-and-replay path for replace commits when the base metadata changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 332 to 336
if (base != null) {
rebaseReplaceOnto(base);
}
}

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.

To keep in line with the simple-transaction path this has been added in the latest commit.

@manuzhang

Copy link
Copy Markdown
Member

@Kurtiscwright What do you mean by rebased on PR 16975 which is the current PR?

sejal-gupta-ksolves and others added 20 commits July 7, 2026 22:16
…ge (apache#16725)

* Docker: Add configuration to toggle info logs flooding in iceberg-rest-fixture

* Docker: Support REST fixture logging config

Generated-by: GPT-5

---------

Co-authored-by: Kevin Liu <kevin.jq.liu@gmail.com>
…#16979)

The monitor source rate was computed as perSecond(1.0 / rateLimit.getSeconds()).
Duration.getSeconds() rounds any sub-second rate limit down to 0, so the rate
became perSecond(Infinity), which turns off rate limiting. This change computes
the rate from millis, so sub-second intervals are limited correctly. The 60s
default is unchanged.

This made the maintenance E2E tests flaky, since they use sub-second rate
limits.  With no rate limit the source calls table.refresh() in a loop and uses
a full CPU core per job. CI runs the tests with -DtestParallelism=auto, which
starts one MiniCluster per fork, so these busy sources use up all the cores. The
converter runs on a timer, and under that load the timer fires too slowly,
letting the test fail after the timeout hits.
…e#16988)

Bumps [org.codehaus.jettison:jettison](https://github.com/jettison-json/jettison) from 1.5.5 to 1.5.6.
- [Release notes](https://github.com/jettison-json/jettison/releases)
- [Commits](jettison-json/jettison@jettison-1.5.5...jettison-1.5.6)

---
updated-dependencies:
- dependency-name: org.codehaus.jettison:jettison
  dependency-version: 1.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@df4cb1c...9c091bb)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](actions/setup-java@be666c2...ad2b381)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [com.diffplug.spotless:spotless-plugin-gradle](https://github.com/diffplug/spotless) from 8.6.0 to 8.7.0.
- [Release notes](https://github.com/diffplug/spotless/releases)
- [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md)
- [Commits](diffplug/spotless@gradle/8.6.0...gradle/8.7.0)

---
updated-dependencies:
- dependency-name: com.diffplug.spotless:spotless-plugin-gradle
  dependency-version: 8.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
)

* Build: Bump datamodel-code-generator from 0.63.0 to 0.64.1

Regenerate open-api/rest-catalog-open-api.py with datamodel-code-generator
0.64.1. As of 0.64.0, optional primitive const fields no longer emit the
const value as an injected default, so the generated discriminator fields
(action/type) are now `Literal[...] | None = None`. Committing the
regenerated output keeps `make generate` + `git diff --exit-code` green.

Supersedes apache#16985.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Mark REST spec discriminators as required so they are not generated as nullable

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…e#16989)

Bumps software.amazon.awssdk:bom from 2.46.10 to 2.46.15.

---
updated-dependencies:
- dependency-name: software.amazon.awssdk:bom
  dependency-version: 2.46.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
The current spec defines this object as a single enum constant, not a comma-separated list of enum constants.
* CVE scan: improve PR failure reporting

Keep the Trivy scan step green so GitHub opens the reporting step that prints the actionable CVE findings, then fail PR runs from that reporting step.

Also remove the Spark 3.5 Jackson CVE ignore entries on this test branch so the PR run exercises the failure UI.

Generated-by: GPT-5 Codex

* CVE scan: format Trivy findings as a table

Parse Trivy SARIF messages into a compact table and keep the PR annotation concise so the failed reporting step is easier to read.

Generated-by: GPT-5 Codex

* CVE scan: simplify Trivy report step

Extract SARIF parsing and report rendering into small shell helpers so the report step is easier to read without changing behavior.

Generated-by: GPT-5 Codex

* CVE scan: restore Spark 3.5 Trivy ignore

Restore the Spark 3.5 Jackson CVE ignore entries that were removed only to exercise the PR failure UI.

Generated-by: GPT-5 Codex

* CVE scan: document PR and push behavior

Clarify that Trivy always writes SARIF, PRs fail from the reporting step on findings, push runs keep findings informational, and missing or unparseable SARIF is still an error.

Generated-by: GPT-5 Codex
…e#17015)

Include snapshot row-lineage fields and manifest list key IDs in BaseSnapshot equals/hashCode and toString. This keeps comparisons and hashes aligned with metadata that changes row ID assignment or manifest-list encryption key selection, and makes those values visible in debug output.

Co-authored-by: Codex <codex@openai.com>
@Kurtiscwright

Copy link
Copy Markdown
Contributor Author

@Kurtiscwright What do you mean by rebased on PR 16975 which is the current PR?

I meant that I pulled this PR locally and started my PR based on #16975 but I didn't use the REST catalog changes and I added tests that simulate the history overwrite scenario.

@manuzhang

Copy link
Copy Markdown
Member

I meant that I pulled this PR locally and started my PR based on #16975

Then, which is your PR?

@Kurtiscwright

Copy link
Copy Markdown
Contributor Author

I meant that I pulled this PR locally and started my PR based on #16975

Then, which is your PR?

Sorry I typed the wrong PR number I rebased on, I meant #16232
This PR #16975 is mine, but I added the PR author from #16232 as a git co-author.

@manuzhang

Copy link
Copy Markdown
Member

Do you mean #16289?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Core: REPLACE TABLE transaction silently overwrites concurrent committed changes