Conversation
Prompted by review comments on the upstream PR our OverwriteAction was originally built on top of (apache#2185). Checked each comment against our own implementation; three applied here too: - Resurrection bug (the serious one): rewrite_manifest's fallback branch called add_existing_entry for every entry not newly deleted this round, including entries that were already Deleted by a prior overwrite -- add_existing_entry unconditionally resets status to Existing, silently resurrecting previously-deleted files as live data the next time their manifest got rewritten for an unrelated reason. Fixed by routing already-non-alive entries through add_deleted_entry instead, preserving their original snapshot_id as a tombstone. Added test_second_overwrite_does_not_resurrect_deleted_file to lock this in. - Wrong schema-id on rewritten manifests: schema was taken from table.metadata().current_schema() (the table's *current* schema) rather than the manifest being rewritten's own schema. After any schema evolution between the original write and a later overwrite, this stamped the wrong schema-id on old entries, which schema-id-aware readers (Java, PyIceberg) would misinterpret. Fixed by deriving both schema and partition spec directly from the manifest's own ManifestMetadata (which already carries fully resolved objects, not just IDs) -- this is also simpler than the existing partition_spec_by_id table lookup it replaces, and doesn't depend on the table still listing that partition spec. - Manifest naming/location bypassed convention: rewritten manifests were written to a hardcoded `{location}/metadata/` path with a fresh random UUID per manifest, rather than `metadata_location()` (which respects a configured write.metadata.path table property, already used correctly elsewhere in this same file for the manifest list) and the commit's own UUID (shared across every manifest touched by one commit, matching SnapshotProducer::new_manifest_writer's own convention). Added a commit_uuid() getter on SnapshotProducer and an index parameter to rewrite_manifest to keep names unique when a commit rewrites more than one manifest. Also fixed, one level up in shared code: SnapshotProducer::summary() unconditionally applied "truncate full table" semantics (replace computed added/removed counts with the previous snapshot's totals) for any Operation::Overwrite, which is wrong for OverwriteAction's explicit-file-list partial overwrites -- it already knows exactly what was added/removed. Added a `truncate_full_table()` method to SnapshotProduceOperation (default false, only meaningful for an operation that genuinely replaces the whole table) and had OverwriteOperation opt out explicitly. Updated test_delete_only_overwrite_summary, which had documented the old (wrong) truncated counts as expected behavior, to assert the correct ones. Not applicable: the review's manifest-filtering concern (dropping delete-only manifests) is already handled correctly on our side -- both FastAppendAction and OverwriteAction's existing_manifest() were confirmed to already keep manifests with has_deleted_files(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
9 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Our
OverwriteAction(crates/iceberg/src/transaction/overwrite.rs) was originally built on top of upstream's in-progress CoW work, apache/iceberg-rust#2185. That PR picked up a detailed review (review) flagging several correctness issues. I checked each one against our own implementation — three applied here too and are fixed in this PR; one was already handled correctly on our side; one (summary counts) is fixed one level up in shared code.Fixes
Resurrection bug (the serious one).
rewrite_manifest's fallback branch calledadd_existing_entryfor every entry not newly deleted this round — including entries alreadyDeletedby a prior overwrite.add_existing_entryunconditionally resets status toExisting, so a later overwrite that touches the same manifest for an unrelated reason silently resurrected previously-deleted files as live data. Fixed by routing already-non-alive entries throughadd_deleted_entryinstead, preserving their originalsnapshot_idas a tombstone. Addedtest_second_overwrite_does_not_resurrect_deleted_file(fails without the fix).Wrong schema-id on rewritten manifests. Schema was taken from
table.metadata().current_schema()— the table's current schema — rather than the manifest being rewritten's own schema. After any schema evolution between the original write and a later overwrite, this stamped the wrong schema-id on old entries, which schema-id-aware readers (Java, PyIceberg) would misinterpret. Fixed by deriving both schema and partition spec directly from the manifest's ownManifestMetadata(already-resolved objects, not just IDs) — simpler than thepartition_spec_by_idtable lookup it replaces, and doesn't depend on the table still listing that partition spec.Manifest naming/location bypassed convention. Rewritten manifests were written to a hardcoded
{location}/metadata/path with a fresh random UUID per manifest, instead ofmetadata_location()(which respects a configuredwrite.metadata.pathtable property, already used correctly elsewhere in this file for the manifest list) and the commit's own UUID (shared across every manifest touched by one commit, matchingSnapshotProducer::new_manifest_writer's convention). Added acommit_uuid()getter onSnapshotProducerand anindexparameter torewrite_manifestto keep names unique when a commit rewrites more than one manifest.Partial-overwrite summary counts (shared code).
SnapshotProducer::summary()unconditionally applied "truncate full table" semantics (replace computed added/removed counts with the previous snapshot's totals) for anyOperation::Overwrite— wrong forOverwriteAction's explicit-file-list partial overwrites, which already know exactly what was added/removed. Added atruncate_full_table()method toSnapshotProduceOperation(defaultfalse, only meaningful for an operation that genuinely replaces the whole table);OverwriteOperationopts out explicitly.test_delete_only_overwrite_summary, which had documented the old (wrong) truncated counts as expected, now asserts the correct ones.Not applicable: the review's manifest-filtering concern (dropping delete-only manifests) — confirmed both
FastAppendActionandOverwriteAction::existing_manifest()already keep manifests withhas_deleted_files().Test plan
cargo test -p iceberg --lib— 1625 passed, 0 failedcargo test -p iceberg --lib transaction::— 89 passed, including the new regression testcargo clippy -p iceberg --all-targets -- -D warnings— cleancargo fmt -p iceberg -- --check— cleancargo check --workspace --all-features --all-targets(excl. python bindings) — clean🤖 Generated with Claude Code