Skip to content

Preserve a ref's retention policy when its snapshot moves - #3770

Open
1fanwang wants to merge 1 commit into
apache:mainfrom
1fanwang:preserve-branch-retention
Open

Preserve a ref's retention policy when its snapshot moves#3770
1fanwang wants to merge 1 commit into
apache:mainfrom
1fanwang:preserve-branch-retention

Conversation

@1fanwang

@1fanwang 1fanwang commented Aug 8, 2026

Copy link
Copy Markdown

Rationale for this change

create_branch() accepts max_ref_age_ms, max_snapshot_age_ms and min_snapshots_to_keep, and any later operation that moves the ref drops all three.

tbl.manage_snapshots().create_branch(
    snapshot_id=tbl.metadata.current_snapshot_id,
    branch_name="audit",
    max_ref_age_ms=86400000,
).commit()
tbl.metadata.refs["audit"].max_ref_age_ms   # 86400000

tbl.append(rows, branch="audit")
tbl.metadata.refs["audit"].max_ref_age_ms   # None

The branch loses its retention policy at the moment the policy starts to matter — a branch is created to hold a staged write, and the staged write is what erases it.

Two paths move a ref, and both rebuild it from a SetSnapshotRefUpdate that omits the retention fields, so _apply_table_update falls back to the defaults:

  • writing to a branch, via _SnapshotProducer._commit()
  • set_current_snapshot(), and the rollbacks that delegate to it

SetSnapshotRefUpdate already carries all three fields; nothing was populating them.

Java preserves them. TableMetadata.Builder.setBranchSnapshotInternal rebuilds the moved ref with SnapshotRef.builderFrom(ref, replacementSnapshotId), which copies the existing retention config forward. Both setBranchSnapshot (writes) and rollbackTo route through it.

This reads the existing ref and carries its fields through on both paths. create_tag() and create_branch() still set the fields explicitly from their arguments, so a ref with no policy does not acquire one.

#3649 fixes the same class of bug on the fast-forward path. It does not touch either path here, so the two are complementary.

Are these changes tested?

Integration, against the REST catalog from dev/docker-compose-integration.yml: create a branch with all three retention fields, write to it twice, assert the policy survives and that main gains nothing.

Red, with pyiceberg/table/update/snapshot.py at upstream/main
$ git checkout upstream/main -- pyiceberg/table/update/snapshot.py
$ pytest tests/integration/test_writes/test_writes.py -m integration -k retention -q

>       assert ref.max_ref_age_ms == 86400000
E       assert None == 86400000
tests/integration/test_writes/test_writes.py:2258: AssertionError
FAILED tests/integration/test_writes/test_writes.py::test_write_to_branch_preserves_retention

The unit tests fail the same way on both paths:

$ pytest tests/table/test_manage_snapshots.py -k retention -q
FAILED ...::test_branch_write_preserves_retention[memory|sql|sql_without_rowcount]
FAILED ...::test_rollback_preserves_retention[memory|sql|sql_without_rowcount]
6 failed, 6 passed
Green, with the change restored
$ pytest tests/table/test_manage_snapshots.py -k retention -q
12 passed, 9 deselected

$ pytest tests/integration/test_writes/test_writes.py tests/integration/test_snapshot_operations.py -m integration -q
146 passed, 1 skipped

The 6 that pass red as well as green are the "a ref without a policy stays without one" cases, which pin that this does not invent defaults.

Whole unit and integration suites pass; prek run -a clean.

Are there any user-facing changes?

A ref keeps the retention policy it was created with when its snapshot moves. No API change.

Copilot AI lite review requested due to automatic review settings August 8, 2026 04:28

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

Fixes a retention-policy regression in the snapshot commit/write path: when writing a new snapshot to an existing branch, the branch’s retention configuration (max_ref_age_ms, max_snapshot_age_ms, min_snapshots_to_keep) is now preserved instead of being dropped by the emitted set-snapshot-ref update.

Changes:

  • Preserve existing branch retention fields when _SnapshotProducer._commit() updates a branch ref during writes.
  • Add unit tests validating retention is preserved across multiple writes and remains unset when not configured.
  • Add an integration test covering the REST catalog scenario where branch retention previously regressed.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
pyiceberg/table/update/snapshot.py Carries existing ref retention fields into the SetSnapshotRefUpdate emitted during snapshot commits to a branch.
tests/table/test_manage_snapshots.py Adds unit coverage to ensure branch retention survives repeated writes and remains unset if not configured.
tests/integration/test_writes/test_writes.py Adds an integration regression test for preserving branch retention on repeated writes via REST catalog.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

create_branch() accepts max_ref_age_ms, max_snapshot_age_ms and
min_snapshots_to_keep, but any later operation that moves the ref drops all
three. Both paths that move a ref rebuild it from a SetSnapshotRefUpdate that
omits the retention fields, so the applied ref falls back to the defaults:

  - writing to a branch, via _SnapshotProducer._commit()
  - set_current_snapshot(), and the rollbacks that delegate to it

A branch created with a retention policy therefore loses it on its first write,
which is the point at which the policy starts to matter.

Carry the existing ref's retention fields through on both paths, mirroring
Java's TableMetadata.Builder.setBranchSnapshotInternal, which rebuilds the moved
ref with SnapshotRef.builderFrom(ref, replacementSnapshotId). create_tag() and
create_branch() still set the fields explicitly, so a ref with no policy does
not acquire one.

Signed-off-by: 1fanwang <1fannnw@gmail.com>
@1fanwang
1fanwang force-pushed the preserve-branch-retention branch from 198ed00 to 4f48ea4 Compare August 8, 2026 06:41
@1fanwang 1fanwang changed the title Preserve branch retention policy when writing to a branch Preserve a ref's retention policy when its snapshot moves Aug 8, 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.

2 participants