Skip to content

Commit b98103e

Browse files
changing approach to avoid errors and keeping the snapshot_by_id the same
1 parent 3e9cea4 commit b98103e

3 files changed

Lines changed: 7 additions & 98 deletions

File tree

pyiceberg/table/inspect.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,13 +310,15 @@ def partitions(
310310
)
311311

312312
partitions_map: dict[tuple[str, Any], Any] = {}
313+
# snapshot_by_id is a linear scan, and there is one lookup per manifest entry
314+
snapshots_by_id = {snapshot.snapshot_id: snapshot for snapshot in self.tbl.metadata.snapshots}
313315

314316
for entry in itertools.chain.from_iterable(scan._plan_manifest_entries()):
315317
partition = entry.data_file.partition
316318
partition_record_dict = {
317319
field.name: partition[pos] for pos, field in enumerate(self.tbl.metadata.specs()[entry.data_file.spec_id].fields)
318320
}
319-
entry_snapshot = self.tbl.snapshot_by_id(entry.snapshot_id) if entry.snapshot_id is not None else None
321+
entry_snapshot = snapshots_by_id.get(entry.snapshot_id) if entry.snapshot_id is not None else None
320322
self._update_partitions_map_from_manifest_entry(
321323
partitions_map, entry.data_file, partition_record_dict, entry_snapshot
322324
)
@@ -532,9 +534,11 @@ def history(self) -> pa.Table:
532534

533535
history = []
534536
metadata = self.tbl.metadata
537+
# snapshot_by_id is a linear scan, and there is one lookup per snapshot log entry
538+
snapshots_by_id = {snapshot.snapshot_id: snapshot for snapshot in metadata.snapshots}
535539

536540
for snapshot_entry in metadata.snapshot_log:
537-
snapshot = metadata.snapshot_by_id(snapshot_entry.snapshot_id)
541+
snapshot = snapshots_by_id.get(snapshot_entry.snapshot_id)
538542

539543
history.append(
540544
{

pyiceberg/table/metadata.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -236,30 +236,9 @@ class TableMetadataCommonFields(IcebergBaseModel):
236236
def transform_properties_dict_value_to_str(cls, properties: Properties) -> dict[str, str]:
237237
return transform_dict_value_to_str(properties)
238238

239-
@property
240-
def _lazy_id_to_snapshot_position(self) -> dict[int, int]:
241-
"""Return an index of snapshot ID to its position in the snapshots list.
242-
243-
This is calculated once per snapshots list and cached.
244-
"""
245-
cached = self.__dict__.get("_id_to_snapshot_position")
246-
if cached is None or cached[0] is not self.snapshots:
247-
cached = (
248-
self.snapshots,
249-
{snapshot.snapshot_id: position for position, snapshot in enumerate(self.snapshots)},
250-
)
251-
# The model is frozen, so bypass the pydantic __setattr__ to memoize.
252-
object.__setattr__(self, "_id_to_snapshot_position", cached)
253-
return cached[1]
254-
255239
def snapshot_by_id(self, snapshot_id: int) -> Snapshot | None:
256240
"""Get the snapshot by snapshot_id."""
257-
snapshots = self.snapshots
258-
if (position := self._lazy_id_to_snapshot_position.get(snapshot_id)) is not None and position < len(snapshots):
259-
if (snapshot := snapshots[position]).snapshot_id == snapshot_id:
260-
return snapshot
261-
# Absent id, or the list was mutated in place and the cached positions no longer line up.
262-
return next((snapshot for snapshot in snapshots if snapshot.snapshot_id == snapshot_id), None)
241+
return next((snapshot for snapshot in self.snapshots if snapshot.snapshot_id == snapshot_id), None)
263242

264243
def schema_by_id(self, schema_id: int) -> Schema | None:
265244
"""Get the schema by schema_id."""

tests/table/test_metadata.py

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
new_table_metadata,
3838
)
3939
from pyiceberg.table.refs import SnapshotRef, SnapshotRefType
40-
from pyiceberg.table.snapshots import Operation, Snapshot, Summary
4140
from pyiceberg.table.sorting import NullOrder, SortDirection, SortField, SortOrder
4241
from pyiceberg.transforms import IdentityTransform
4342
from pyiceberg.typedef import UTF8
@@ -146,79 +145,6 @@ def test_parsing_correct_types(example_table_metadata_v2: dict[str, Any]) -> Non
146145
assert isinstance(table_metadata.schemas[0].fields[0].field_type, LongType)
147146

148147

149-
def test_snapshot_by_id(example_table_metadata_v2: dict[str, Any]) -> None:
150-
table_metadata = TableMetadataV2(**example_table_metadata_v2)
151-
152-
# Returns the same instance that is in the snapshots list, not a copy
153-
assert table_metadata.snapshot_by_id(3051729675574597004) is table_metadata.snapshots[0]
154-
assert table_metadata.snapshot_by_id(3055729675574597004) is table_metadata.snapshots[1]
155-
assert table_metadata.snapshot_by_id(-1) is None
156-
157-
158-
def test_snapshot_by_id_index_is_invalidated_on_model_copy(example_table_metadata_v2: dict[str, Any]) -> None:
159-
"""The snapshot lookup index must not survive a model_copy that replaces the snapshots."""
160-
table_metadata = TableMetadataV2(**example_table_metadata_v2)
161-
162-
# Build the index before copying, so a stale one would be carried over
163-
assert table_metadata.snapshot_by_id(3051729675574597004) is not None
164-
165-
new_snapshot = Snapshot(
166-
snapshot_id=1,
167-
parent_snapshot_id=3055729675574597004,
168-
sequence_number=35,
169-
timestamp_ms=1602638573591,
170-
manifest_list="s3://bucket/test/manifest-list",
171-
summary=Summary(Operation.APPEND),
172-
schema_id=1,
173-
)
174-
with_added = table_metadata.model_copy(update={"snapshots": table_metadata.snapshots + [new_snapshot]})
175-
assert with_added.snapshot_by_id(1) is new_snapshot
176-
assert with_added.snapshot_by_id(3051729675574597004) is not None
177-
178-
without_first = with_added.model_copy(update={"snapshots": with_added.snapshots[1:]})
179-
assert without_first.snapshot_by_id(3051729675574597004) is None
180-
assert without_first.snapshot_by_id(1) is new_snapshot
181-
182-
# The original is unaffected by either copy
183-
assert table_metadata.snapshot_by_id(1) is None
184-
assert table_metadata.snapshot_by_id(3051729675574597004) is not None
185-
186-
187-
def test_snapshot_by_id_reflects_in_place_snapshot_list_mutation(example_table_metadata_v2: dict[str, Any]) -> None:
188-
"""The snapshot lookup must not go stale when the snapshots list itself is mutated in place."""
189-
table_metadata = TableMetadataV2(**example_table_metadata_v2)
190-
snapshot_id = 3051729675574597004
191-
192-
# Build the index before mutating, so a stale one would still be in place
193-
assert table_metadata.snapshot_by_id(snapshot_id) is not None
194-
195-
# Replacing an entry: the lookup must return the new instance, not the replaced one
196-
altered = table_metadata.snapshots[0].model_copy(update={"summary": Summary(Operation.DELETE)})
197-
table_metadata.snapshots[0] = altered
198-
assert table_metadata.snapshot_by_id(snapshot_id) is altered
199-
200-
# Reordering: ids still resolve to the right snapshots
201-
table_metadata.snapshots.reverse()
202-
assert table_metadata.snapshot_by_id(snapshot_id) is altered
203-
assert table_metadata.snapshot_by_id(3055729675574597004) is table_metadata.snapshots[0]
204-
205-
# Removing: the id is gone
206-
table_metadata.snapshots.clear()
207-
assert table_metadata.snapshot_by_id(snapshot_id) is None
208-
209-
210-
def test_snapshot_by_id_index_is_not_serialized(example_table_metadata_v2: dict[str, Any]) -> None:
211-
"""The memoized index is an implementation detail and must stay out of the serialized form."""
212-
table_metadata = TableMetadataV2(**example_table_metadata_v2)
213-
assert table_metadata.snapshot_by_id(3051729675574597004) is not None
214-
215-
assert "_id_to_snapshot_position" not in table_metadata.model_dump()
216-
assert "_id_to_snapshot_position" not in table_metadata.model_dump_json()
217-
assert "_id_to_snapshot_position" not in TableMetadataV2.model_fields
218-
# Two equal instances stay equal when only one of them has built the index
219-
assert table_metadata == TableMetadataV2(**example_table_metadata_v2)
220-
221-
222148
def test_updating_metadata(example_table_metadata_v2: dict[str, Any]) -> None:
223149
"""Test creating a new TableMetadata instance that's an updated version of
224150
an existing TableMetadata instance"""

0 commit comments

Comments
 (0)