This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-4110-c1c49a228c27c777f9e63f7a46d801d85fcfd285 in repository https://gitbox.apache.org/repos/asf/iceberg-python.git
commit 945c8db4c82b4fb1c2decc29109bf9630515b0d2 Author: Bishop Kammeraad <[email protected]> AuthorDate: Sat Oct 24 21:42:14 2026 +0000 fix(table): reuse partition field IDs on v3 tables (#4110) * fix(table): reuse partition field IDs on v3 tables UpdateSpec only looked up historical partition fields when the format version was exactly 2, so v3 tables got a fresh field ID when a removed partition field was added back. Java does this for format version >= 2. * test: cover removing and re-adding a partition field in one update Co-authored-by: Copilot App <[email protected]> --------- Co-authored-by: Kevin Liu <[email protected]> Co-authored-by: Copilot App <[email protected]> --- pyiceberg/table/update/spec.py | 2 +- tests/table/test_init.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pyiceberg/table/update/spec.py b/pyiceberg/table/update/spec.py index e060d6c26..5b55d96b1 100644 --- a/pyiceberg/table/update/spec.py +++ b/pyiceberg/table/update/spec.py @@ -268,7 +268,7 @@ class UpdateSpec(UpdateTableMetadata["UpdateSpec"]): return PartitionSpec(*partition_fields, spec_id=new_spec_id) def _partition_field(self, transform_key: tuple[int, Transform[Any, Any]], name: str | None) -> PartitionField: - if self._transaction.table_metadata.format_version == 2: + if self._transaction.table_metadata.format_version >= 2: source_id, transform = transform_key historical_fields = [] for spec in self._transaction.table_metadata.specs().values(): diff --git a/tests/table/test_init.py b/tests/table/test_init.py index 5ee5e4566..20acc870a 100644 --- a/tests/table/test_init.py +++ b/tests/table/test_init.py @@ -1479,6 +1479,30 @@ def test_last_assigned_partition_id(table_v2: Table) -> None: AssertLastAssignedPartitionId(last_assigned_partition_id=1).validate(base_metadata) [email protected]("table", [lf("table_v2"), lf("table_v3")]) +def test_update_spec_reuses_removed_partition_field_id(table: Table) -> None: + txn = table.transaction() + txn.update_spec().remove_field("x").add_field("y", BucketTransform(16), "y_bucket").commit() + txn.update_spec().add_identity("x").commit() + + assert txn.table_metadata.spec().fields == ( + PartitionField(source_id=2, field_id=1001, transform=BucketTransform(16), name="y_bucket"), + PartitionField(source_id=1, field_id=1000, transform=IdentityTransform(), name="x"), + ) + assert txn.table_metadata.last_partition_id == 1001 + + [email protected]("table", [lf("table_v2"), lf("table_v3")]) +def test_update_spec_remove_and_add_same_field_reuses_id(table: Table) -> None: + txn = table.transaction() + txn.update_spec().remove_field("x").add_identity("x").commit() + + assert txn.table_metadata.spec().fields == ( + PartitionField(source_id=1, field_id=1000, transform=IdentityTransform(), name="x"), + ) + assert txn.table_metadata.last_partition_id == 1000 + + def test_assert_default_spec_id(table_v2: Table) -> None: base_metadata = table_v2.metadata AssertDefaultSpecId(default_spec_id=base_metadata.default_spec_id).validate(base_metadata)
