Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion pyiceberg/table/update/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,16 @@ def update_table_metadata(
if enforce_validation:
return TableMetadataUtil.parse_obj(new_metadata.model_dump())
else:
return new_metadata.model_copy(deep=True)
return _copy_containers(new_metadata)


def _copy_containers(metadata: TableMetadata) -> TableMetadata:
"""Copy the list and dict fields of the metadata and share the frozen models they hold.

The frozen config cannot prevent a list append or a dict assignment on a field, so copying the
containers keeps the base metadata isolated without deep-copying the whole table history.
"""
return metadata.model_copy(update={name: value.copy() for name, value in metadata if isinstance(value, (list, dict))})


def _update_table_metadata_log(base_metadata: TableMetadata, metadata_location: str, last_updated_ms: int) -> TableMetadata:
Expand Down
14 changes: 14 additions & 0 deletions tests/table/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,20 @@ def test_metadata_isolation_from_illegal_updates(table_v1: Table) -> None:
assert len(base_metadata.partition_specs) == 1


def test_update_metadata_shares_unchanged_models(table_v2: Table) -> None:
base_metadata = table_v2.metadata
new_metadata = update_table_metadata(base_metadata, (SetPropertiesUpdate(updates={"test_a": "test_a"}),))

# The containers are copied, so a list append or dict assignment cannot reach the base metadata
assert new_metadata.snapshots is not base_metadata.snapshots
assert new_metadata.refs is not base_metadata.refs
assert new_metadata.properties is not base_metadata.properties
# The frozen models they hold are shared, so the cost does not grow with the table history
assert all(new is base for new, base in zip(new_metadata.snapshots, base_metadata.snapshots, strict=True))
assert all(new is base for new, base in zip(new_metadata.schemas, base_metadata.schemas, strict=True))
assert "test_a" not in base_metadata.properties


def test_generate_snapshot_id(table_v2: Table) -> None:
assert isinstance(_generate_snapshot_id(), int)
assert isinstance(table_v2.metadata.new_snapshot_id(), int)
Expand Down
Loading