|
28 | 28 | from typing import TYPE_CHECKING, Generic |
29 | 29 |
|
30 | 30 | from pyiceberg.avro.codecs import AvroCompressionCodec |
31 | | -from pyiceberg.exceptions import ValidationException |
| 31 | +from pyiceberg.exceptions import ( |
| 32 | + NoSuchSnapshotRefError, |
| 33 | + NotAncestorError, |
| 34 | + SnapshotRefTypeError, |
| 35 | + ValidationException, |
| 36 | +) |
32 | 37 | from pyiceberg.expressions import AlwaysFalse, BooleanExpression, Or |
33 | 38 | from pyiceberg.expressions.visitors import ( |
34 | 39 | ROWS_MIGHT_NOT_MATCH, |
|
52 | 57 | ) |
53 | 58 | from pyiceberg.partitioning import PartitionSpec |
54 | 59 | from pyiceberg.schema import Schema |
55 | | -from pyiceberg.table.refs import MAIN_BRANCH, SnapshotRefType |
| 60 | +from pyiceberg.table.refs import MAIN_BRANCH, SnapshotRef, SnapshotRefType |
56 | 61 | from pyiceberg.table.snapshots import ( |
57 | 62 | Operation, |
58 | 63 | Snapshot, |
59 | 64 | SnapshotSummaryCollector, |
60 | 65 | Summary, |
61 | 66 | ancestors_of, |
| 67 | + is_ancestor_of, |
62 | 68 | latest_ancestor_before_timestamp, |
63 | 69 | update_snapshot_summaries, |
64 | 70 | ) |
@@ -1043,6 +1049,35 @@ def _commit_if_ref_updates_exist(self) -> None: |
1043 | 1049 | self._updates = () |
1044 | 1050 | self._requirements = () |
1045 | 1051 |
|
| 1052 | + def _effective_refs(self) -> dict[str, SnapshotRef]: |
| 1053 | + """Return refs as they would appear after all currently-staged updates. |
| 1054 | +
|
| 1055 | + Committed refs from ``table_metadata.refs`` overlaid with the effects |
| 1056 | + of every ``SetSnapshotRefUpdate`` / ``RemoveSnapshotRefUpdate`` that has |
| 1057 | + been accumulated onto ``self._updates`` in this chain, in order. Later |
| 1058 | + stages win. Callers use this instead of ``table_metadata.refs`` when a |
| 1059 | + decision needs to observe the results of earlier operations in the |
| 1060 | + same ``manage_snapshots()`` chain. |
| 1061 | +
|
| 1062 | + Note that this projection is for *decision-making* only. Requirements |
| 1063 | + emitted via ``_set_ref_snapshot`` continue to reference committed |
| 1064 | + state, which is what the catalog checks at commit time and what makes |
| 1065 | + concurrent-write detection correct. |
| 1066 | + """ |
| 1067 | + refs: dict[str, SnapshotRef] = dict(self._transaction.table_metadata.refs) |
| 1068 | + for update in self._updates: |
| 1069 | + if isinstance(update, SetSnapshotRefUpdate): |
| 1070 | + refs[update.ref_name] = SnapshotRef( |
| 1071 | + snapshot_id=update.snapshot_id, |
| 1072 | + snapshot_ref_type=update.type, |
| 1073 | + max_ref_age_ms=update.max_ref_age_ms, |
| 1074 | + max_snapshot_age_ms=update.max_snapshot_age_ms, |
| 1075 | + min_snapshots_to_keep=update.min_snapshots_to_keep, |
| 1076 | + ) |
| 1077 | + elif isinstance(update, RemoveSnapshotRefUpdate): |
| 1078 | + refs.pop(update.ref_name, None) |
| 1079 | + return refs |
| 1080 | + |
1046 | 1081 | def _remove_ref_snapshot(self, ref_name: str) -> ManageSnapshots: |
1047 | 1082 | """Remove a snapshot ref. |
1048 | 1083 |
|
@@ -1232,6 +1267,61 @@ def _current_ancestors(self) -> set[int]: |
1232 | 1267 | ) |
1233 | 1268 | } |
1234 | 1269 |
|
| 1270 | + def fast_forward_branch(self, from_branch: str, to_ref: str) -> ManageSnapshots: |
| 1271 | + """Fast-forward ``from_branch`` to the snapshot referenced by ``to_ref``. |
| 1272 | +
|
| 1273 | + * If ``from_branch`` does not exist, it is created pointing at ``to_ref``'s snapshot (Java/Spark parity). |
| 1274 | + * If both refs already point to the same snapshot the call is a no-op. |
| 1275 | + * Otherwise ``from_branch`` must be a branch (not a tag) and its current snapshot |
| 1276 | + must be an ancestor of ``to_ref``'s snapshot. |
| 1277 | +
|
| 1278 | + Within a single ``manage_snapshots()`` chain, ref lookups observe earlier staged |
| 1279 | + operations via :meth:`_effective_refs`. This means that `create_branch(...)` followed |
| 1280 | + by `fast_forward_branch(...)` on the same ref works as expected. |
| 1281 | +
|
| 1282 | + Args: |
| 1283 | + from_branch: name of the branch to advance. |
| 1284 | + to_ref: name of the branch or tag whose snapshot ``from_branch`` will point to. |
| 1285 | +
|
| 1286 | + Returns: |
| 1287 | + This for method chaining. |
| 1288 | +
|
| 1289 | + Raises: |
| 1290 | + NoSuchSnapshotRefError: ``to_ref`` does not exist. |
| 1291 | + SnapshotRefTypeError: ``from_branch`` exists but is a tag. |
| 1292 | + NotAncestorError: ``from_branch``'s snapshot is not an ancestor of ``to_ref``'s snapshot. |
| 1293 | + """ |
| 1294 | + refs = self._effective_refs() |
| 1295 | + |
| 1296 | + if (to_snapshot_ref := refs.get(to_ref)) is None: |
| 1297 | + raise NoSuchSnapshotRefError(f"Ref does not exist: {to_ref}") |
| 1298 | + to_snapshot_id = to_snapshot_ref.snapshot_id |
| 1299 | + |
| 1300 | + if from_branch not in refs: |
| 1301 | + return self.create_branch(snapshot_id=to_snapshot_id, branch_name=from_branch) |
| 1302 | + |
| 1303 | + from_ref = refs[from_branch] |
| 1304 | + if from_ref.snapshot_ref_type != SnapshotRefType.BRANCH: |
| 1305 | + raise SnapshotRefTypeError(f"Ref {from_branch} is a tag, not a branch") |
| 1306 | + |
| 1307 | + if from_ref.snapshot_id == to_snapshot_id: |
| 1308 | + return self |
| 1309 | + |
| 1310 | + if not is_ancestor_of(to_snapshot_id, from_ref.snapshot_id, self._transaction.table_metadata): |
| 1311 | + raise NotAncestorError(f"Cannot fast-forward: {from_branch} is not an ancestor of {to_ref}") |
| 1312 | + |
| 1313 | + update, requirement = self._transaction._set_ref_snapshot( |
| 1314 | + snapshot_id=to_snapshot_id, |
| 1315 | + ref_name=from_branch, |
| 1316 | + type=SnapshotRefType.BRANCH, |
| 1317 | + max_ref_age_ms=from_ref.max_ref_age_ms, |
| 1318 | + max_snapshot_age_ms=from_ref.max_snapshot_age_ms, |
| 1319 | + min_snapshots_to_keep=from_ref.min_snapshots_to_keep, |
| 1320 | + ) |
| 1321 | + self._updates += update |
| 1322 | + self._requirements += requirement |
| 1323 | + return self |
| 1324 | + |
1235 | 1325 |
|
1236 | 1326 | class ExpireSnapshots(UpdateTableMetadata["ExpireSnapshots"]): |
1237 | 1327 | """Expire snapshots by ID. |
|
0 commit comments