Skip to content

Support row lineage assignment on commit - #4102

Open
cayetanobv wants to merge 10 commits into
apache:mainfrom
cayetanobv:feat/row-lineage-on-commit
Open

cayetanobv wants to merge 10 commits into
apache:mainfrom
cayetanobv:feat/row-lineage-on-commit

Conversation

@cayetanobv

@cayetanobv cayetanobv commented Oct 13, 2026 •

Copy link
Copy Markdown

Closes #3621 (part of #1551). Stacked on #3624, which adds ManifestListWriterV3. Only the last commit belongs to this PR; I will rebase once #3624 merges.

Rationale for this change

Every commit to a V3 table fails on a REST catalog. _SnapshotProducer._commit sets first-row-id on the new snapshot but never sets added-rows, and the spec requires both together. The REST fixture rejects the snapshot:

java.lang.IllegalArgumentException: Invalid added-rows (required when first-row-id is set): null

Without a server check, the table metadata goes wrong instead. add_snapshot computes next-row-id from added-rows, so next-row-id becomes None after the first commit and the next commit fails.

This PR sets added-rows to writer.next_row_id - first_row_id, which is the number of row IDs ManifestListWriterV3 assigned. Java's SnapshotProducer computes it the same way. The count includes existing rows in manifests that had no first_row_id, so next-row-id always moves past every assigned ID.

It also removes _calculate_added_rows. Nothing called it, and it counted only added_rows_count from the snapshot's own manifests, which would undercount the rows that the writer assigns.

Out of scope: a V3 table created by new_table_metadata has next-row-id = None, so a commit to it still fails. A REST server sets it to 0 on create, which hides this. That belongs with writing V3 metadata (#3551).

Rewritten manifests do not keep row IDs on EXISTING entries yet, so a merge append or partial delete moves them. #3972 fixes this by inheriting first_row_id on read. added-rows already matches Java in both cases.

Are these changes tested?

Yes. Three new tests in tests/table/test_snapshots.py commit to an empty V3 table inside a transaction, because writing a V3 metadata.json is not supported yet:

  • two appends: (first_row_id, added_rows) is (0, 3), then (3, 3), and next-row-id is 6
  • add_files: (0, 3), next-row-id is 3
  • a delete that rewrites a data file: the 2 surviving rows get new IDs, (3, 2), and next-row-id is 5

All three fail without the change. The unit suite passes locally.

This PR also un-skips the integration test test_v3_write_and_read_row_lineage. It now appends twice and reads _row_id and _last_updated_sequence_number with Spark (Iceberg 1.11.0), which must see row IDs 0 to 5 with sequence numbers 1 and 2. It passes with the change and fails without it on the same Invalid added-rows error.

I also ran this script against apache/iceberg-rest-fixture (sha256:db8de90b…) with the RustFS warehouse from dev/docker-compose-integration.yml:

t = cat.create_table(ident, schema=data.schema, properties={"format-version": "3"})
t.add_files([f"file://{src}"])           # 3 rows
cat.load_table(ident).append(data)        # 3 rows
cat.load_table(ident).delete("id == 1")   # rewrites both files

Before, on #3624 (c152089d):

lineage.t_2890d52f format-version=3 next-row-id=0
CommitStateUnknownException: RESTError 500: ... Invalid added-rows (required when first-row-id is set): null

After:

lineage.t_0e297eff format-version=3 next-row-id=0
add_files: first-row-id=0 added-rows=3 next-row-id=3 rows=3
append   : first-row-id=3 added-rows=3 next-row-id=6 rows=6
delete   : first-row-id=6 added-rows=4 next-row-id=10 rows=4

Are there any user-facing changes?

No API changes. Commits to V3 tables through a REST catalog now succeed.

Written with Claude Code (Claude Opus 5.5) and reviewed by me. One path has no test: added-rows for tables upgraded from V2, which follows Java but needs #3622 to exercise.

moomindani and others added 10 commits October 7, 2026 18:53
Add ManifestWriterV3 and ManifestListWriterV3. The manifest writer
uses the V3 record layout so the V3-only data file fields
(first_row_id, referenced_data_file, content_offset,
content_size_in_bytes) are written, rebinding V2-layout records when
needed. The manifest list writer implements the spec's First Row ID
Assignment: preserving existing first_row_id values, never assigning
one to delete manifests, and assigning a running value advanced by
existing and added row counts to unassigned data manifests. It also
exposes next_row_id for the commit path to update the table's
next-row-id.

Closes apache#3620
Carry an optional first_row_id through ManifestWriterV3 into the
produced manifest file (used when rewriting manifests whose
first_row_id is known), matching ManifestFiles.newWriter in Java.
Expand tests for parity with TestManifestWriterVersions and
TestManifestListVersions: reader compatibility of V3-written files,
metrics field round-trips, null first_row_id when reading V2 manifest
lists with the V3 layout, and preservation of writer-carried
first_row_id without advancing next_row_id.
Assigning a first-row-id while treating unknown existing/added row
counts as zero would let subsequent manifests overlap row ID ranges.
Raise a clear error instead, matching the reference implementation
which requires the counts. Also drop the always-None value from the
first-row-id-required error message and document the record layout
invariant behind the V3 rebinding.
Reading V2/V3 manifests and manifest lists used a fixed V2 read schema,
so V3-only fields (first_row_id, referenced_data_file, content_offset,
content_size_in_bytes) were silently dropped by Avro schema resolution,
breaking V3 round trips. Read with the latest known schema instead,
keeping the V2 default for constructing and writing records unchanged.

Also fixes ManifestListWriterV3._wrap sharing _data with the source
record via a shallow copy, and rebinding a record to the V3 layout
against a fixed V2 field count, which raised on V1-bound records.
max() and list indexing on the version-keyed schema dicts widen the
Literal[1, 2, 3] TableVersion back to int, so mypy flags the results
as incompatible with the declared TableVersion return types.
test_rest_manifest.py has its own todict helper (separate from
tests/avro/test_file.py's), which also enumerates every DataFile
property via reflection. Adding V3-only properties to DataFile in
the previous commit caused this helper to include them for a
V2-shaped record too, diverging from the V2 dict fastavro writes and
failing the REST integration round-trip test.
DataFile.from_args now rejects fields that are not in the target
layout, so rebinding a V1 data file to V3 raised on
block_size_in_bytes, which V3 does not have.

Generated-by: Claude Code
V3 snapshots set first-row-id but not added-rows, so REST catalogs
reject every V3 commit and next-row-id is lost after the first one.
Set added-rows to the row IDs that the V3 manifest list writer
assigns, as Java does, and remove the unused _calculate_added_rows.

Closes apache#3621
@cayetanobv
cayetanobv force-pushed the feat/row-lineage-on-commit branch from 39e7fd9 to 551c5f4 Compare October 13, 2026 07:02
@cayetanobv
cayetanobv marked this pull request as ready for review October 13, 2026 07:05

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support row lineage assignment on commit

2 participants