Skip to content

Spec-evolution coverage for RewriteFiles, and keep partition metrics in its summary - #2

Closed
rexminnis wants to merge 19 commits into
hotcache:feat/rewrite-files-actionfrom
rexminnis:drls/3046-spec-evolution-fixture
Closed

rexminnis wants to merge 19 commits into
hotcache:feat/rewrite-files-actionfrom
rexminnis:drls/3046-spec-evolution-fixture

Conversation

@rexminnis

Copy link
Copy Markdown

The fixture offered on apache#3046: an identity(ts) → day(ts) table on V2 and V3.

  • test(transaction): cover rewrites on a spec-evolved table — a survivor stays Existing in its spec-0 manifest with its timestamptz value and partition summary intact; one rewrite filters a manifest of each spec; retiring the last old-spec file drops the spec-0 manifest with chained totals exact. All pass on the branch as-is.
  • fix(transaction): keep partition metrics in the rewrite summary — build_summary merged two collectors that don't trust partition metrics, so replace snapshots never reported changed-partition-count or partitions.*. Adds the new files to the filter's collector instead and honours write.summary.partition-limit. Its test fails without the fix.

cargo test -p iceberg --lib (1817), cargo fmt --check and clippy -D warnings are clean; no public API changes.

hotcache and others added 19 commits August 25, 2026 11:17
- Return explicit errors instead of silently defaulting to 0 for
  sequence_number and file_sequence_number in manifest entries
- Use standard manifest naming scheme (<uuid>-m<counter>.avro)
  instead of custom names (-m-filter-, -m-added)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`ManifestFilterManager::filter_manifests` built each surviving entry with
status Existing and the original snapshot id and sequence numbers, then
handed it to `ManifestWriter::add_entry`, which rewrites the status to
Added, replaces the snapshot id with the new snapshot's, and clears the
file sequence number. Survivors of a rewrite were therefore recorded as
newly added files of the rewrite snapshot.

Use `add_existing_entry`, which only sets the status and leaves the ids
the filter already resolved, and drop its `#[allow(dead_code)]`.
Java's `RewriteFiles.dataSequenceNumber` lets a compaction record the
sequence number of the files it replaces, so the rewritten file does not
shadow deletes written concurrently at a higher sequence number. Without
it the added files inherit the rewrite snapshot's own sequence number.

`RewriteFilesAction::data_sequence_number` sets it, and
`write_added_manifest` then writes the added files through
`ManifestWriter::add_file`, which records the given number on the entry.
A number above the one the new snapshot will carry is rejected at commit;
on a V1 table that is 0, matching the V1 entry layout, which records no
sequence number at all.

The parameter is `i64`, not `u64`, to match `ManifestWriter::add_file`
and every other sequence number in the crate.
`commit_snapshot` handed `SnapshotProducer` an empty added-file list and
went straight to `commit_with_manifests`, so neither
`validate_added_data_files` nor `validate_duplicate_files` ever saw the
files a rewrite adds. A file carrying a partition value the default spec
cannot hold, or a path that is already live, was written into a manifest
and committed.

Pass the added files to the producer and run both validations first, and
drop `write_added_manifest`'s own content-type check, which
`validate_added_data_files` now makes unreachable.
`commit_snapshot` created a fresh `(0..)` counter on every call while the
commit uuid stayed fixed for the life of the action, so a retried commit
rewrote `{uuid}-m0.avro` over the manifest the previous attempt had
written under the same name.

Java keeps `manifestCount` on the snapshot producer and never resets it.
The counter cannot live on Rust's `SnapshotProducer`: that is built fresh
inside each `commit_snapshot` call and so resets with it. The object that
survives a retry is the action, which the transaction re-invokes through
an `Arc`, so the counter goes on `MergingSnapshotProducer` as an
`AtomicU64` — `commit` only has `&self`. `fast_append` is unaffected: it
draws a new commit uuid per attempt unless the caller pins one.
The filter carried a manifest forward on `has_deleted_files()`, so a
manifest whose entries are all Deleted stayed in every following snapshot
for the rest of the table's life. Java keeps an existing manifest on
`hasAddedFiles() || hasExistingFiles()`; its third arm matches the
snapshot being produced, which no manifest read from the current snapshot
can carry. The manifests this commit rewrites are produced by the filter
pass and kept on their own.
`starting_snapshot_id` was recorded and never read. A rewrite planned
against one snapshot and committed against a later one silently drops any
delete written in between: the replacement file keeps the deleted rows.

Java resolves this per file in `validateNoNewDeletesForDataFiles`, which
needs a delete-file index this crate does not have yet. Until then the
commit fails closed when the current snapshot carries a delete manifest
with a sequence number above the starting snapshot's.
Every rewrite test used the V3 fixture, so nothing covered the V2
manifest and entry layout the action also writes. Both cases now run on
V2 as well, through a V2 catalog fixture alongside the V3 one.
`cargo fmt` over the two new modules, and `public-api.txt` regenerated
for `Transaction::rewrite_files`. The clippy warning on the unnecessary
parentheses around the manifest counter went with the counter itself.
Resolves two conflicts from the transaction module re-export refactor on
main: action types are now re-exported at `iceberg::transaction::*`, so
`RewriteFilesAction` is exported the same way and `public-api.txt` records
it under the flat path.

Co-authored-by: Jegor van Opdorp <jegorvanopdorp@gmail.com>
V1 manifest entries carry no sequence numbers, and `ManifestEntryV1`
deserialization fills both in as 0. Nothing in the rewrite path treats
that as missing, so a V1 table compacts like V2 and V3 and the surviving
entries keep Existing status, their snapshot id and their zero sequence
numbers. Cover it so it stays that way.
`validate_added_data_files` is shared by fast append and rewrite, but its
content-type error said "for fast append", so a rewrite that was handed a
delete file reported the wrong operation and not which file was wrong.
Drop the operation from the message and name the file and its content
type instead.
`validate_no_new_deletes` fell back to sequence number 0 when the starting
snapshot was no longer in the table, which let a rewrite commit when the
table held no delete manifest at all and otherwise blamed an unrelated
delete manifest for the rejection. An expired starting snapshot means no
delete can be ruled out, so say that instead.
`validate_no_new_deletes` skipped the check whenever either the starting
snapshot id or the current snapshot was absent. A missing starting
snapshot id means the table held nothing when the rewrite was planned, so
everything it holds at commit time, delete files included, arrived after
that -- the case the check is least able to skip. Reject it, and keep
returning early only when the table still has no snapshot, where the
filter rejects the rewrite for its missing sources anyway.
The conflict check is table-wide and always on, so a compaction fails on a
delete committed anywhere in the table while it ran, not only on the files
it replaces. Say so on the action, along with the two other cases that
cannot rule out a delete and what the check does not cover, so a caller
can tell an expected rejection from a bug.

Also stop the public docs linking to the private MergingSnapshotProducer,
which rustdoc drops on the generated page.
The macro from apache#2928 landed while this branch was open and snapshot.rs
already uses it. Use it for the merging producer and the rewrite action
too, so the whole transaction module reads the same way.
The filter resolves each manifest's own partition spec, but every test
ran on the default spec. Add an identity(ts) -> day(ts) fixture on V2
and V3: a survivor stays Existing in a spec-0 manifest with its
timestamptz value and partition summary intact, one rewrite filters a
manifest of each spec, and retiring the last old-spec file drops the
spec-0 manifest with the chained totals still exact.
build_summary merged the filter's removal collector into a fresh one;
merge drops partition metrics unless both sides trust them and neither
did, so a rewrite never reported changed-partition-count or the
partitions.* summaries. Add the new files to the filter's collector
instead, and honour write.summary.partition-limit as fast append does.
@hotcache

Copy link
Copy Markdown
Owner

Merged — both commits are now on feat/rewrite-files-action as 43a09a5 and 89786a8, with you as the author. I’m closing this PR manually since GitHub didn’t pick it up.

Sorry this PR got a bit messy. Shortly after you opened it, I rewrote the branch so all the commits use the same identity. Some were authored as abel, which is the same person as hotcache but shows up as a different name in the commits. For an ASF PR, that would look like a third contributor.

The actual trees were byte-for-byte identical, but the rewrite changed all the SHAs from the merge commit onward, including the e7db3c8 you originally based this on. That’s why the PR ended up showing 19 commits and dirty.

Nothing you did caused this, and there’s nothing you need to redo. I rebased your two commits onto the new tip and they applied cleanly.

I also took care of the DELETED entry change you mentioned in apache#3046, so no need to spend more time on the fixture. The assertions that expected the empty manifest to disappear now check that it has no live entries, and that a later rewrite removes it.

An append keeps the empty manifest around, same as Java, since only a merging commit rebuilds the manifest list. So the test does a second rewrite instead of an append. More details in my reply on apache#3046.

Thanks again for the fixture and for running it against Polaris. Finding those missing DELETED entries really needed a Java reader, so we probably wouldn’t have caught this otherwise.

@hotcache hotcache closed this Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants