Repository navigation
Raise when planning a scan of an unknown snapshot - #4218
dragonknightk wants to merge 1 commit into
Conversation
Closes issue 4197
# Rationale for this change
A scan whose `snapshot_id` names no snapshot, for example an expired one, answers two ways: `plan_files()` returns no tasks and `count()` returns `0`, while `to_arrow()` on the same scan raises `ValueError: Snapshot not found: <id>`.
Reproducer:
- append twice, then expire the first snapshot with `t.maintenance.expire_snapshots().by_id(s1).commit()`
- `t.scan(snapshot_id=s1).count()` returns `0` and `plan_files()` returns `[]`
- `t.scan(snapshot_id=s1).to_arrow()` raises `ValueError: Snapshot not found: ...`
The local planning path returned `[]` whenever `self.snapshot()` was `None`, which covers both an empty table and an id that is not in the metadata. Only the empty table should plan to nothing.
Java makes the same distinction. `SnapshotScan.useSnapshot` rejects an unknown id with `Preconditions.checkArgument(table().snapshot(scanSnapshotId) != null, "Cannot find snapshot with ID %s", scanSnapshotId)`, so the `snapshot == null` branch in `SnapshotScan.planFiles()` only returns an empty result for an empty table.
### What changed
| Where | Now |
| --- | --- |
| `DataScan._plan_files_local` | Raises `ValueError(f"Snapshot not found: {self.snapshot_id}")` when a `snapshot_id` is set but not found, the same error that `projection()` and therefore `to_arrow()` already raise |
| `count()` | Uses `plan_files()`, so it raises as well |
| `to_arrow()`, `to_arrow_batch_reader()` | Already raised from `projection()`, unchanged |
| A scan without a `snapshot_id` on a table without snapshots | Still plans to nothing |
### Why planning, not the scan constructor
Java checks when the snapshot is set on the scan; this change checks when the files are planned. pyiceberg accepts the id in the `DataScan` constructor and in `update()`, and `TableScan.snapshot()` returns `None` for an unknown id, which `test_table_scan_missing_snapshot_zero` relies on. Checking in planning keeps those as they are and raises at the same point as `to_arrow()`, which only fails once it is called.
### Server-side planning
Unchanged: the snapshot id is sent to the REST server, which plans against its own metadata. The Java reference implementation calls `useSnapshot` in `CatalogHandlers.planTableScan`, so it rejects an unknown id with a 400 that pyiceberg raises as `BadRequestError`. With server-side planning, `plan_files()` and `count()` therefore raise the server's error when the server rejects the id, instead of the `ValueError` that `to_arrow()` raises from `projection()` before planning (・ω・)b
## Are these changes tested?
Yes, in `tests/table/test_init.py`:
| Test | Checks |
| --- | --- |
| `test_table_scan_plan_files_missing_snapshot` | `plan_files()` and `count()` raise `Snapshot not found: 0` for a scan of `table_v2` with snapshot id `0`, which the fixture does not have. The id `0` also makes sure the check does not treat the id as a boolean. Without this change, the test fails with `DID NOT RAISE ValueError` |
| `test_data_scan_plan_files_no_current_snapshot` | A table without snapshots is already covered: `plan_files()` returns `[]` and `count()` returns `0` for a scan without a `snapshot_id` of a table without snapshots |
## Are there any user-facing changes?
Yes, a bug fix: `plan_files()` and `count()` on a scan whose `snapshot_id` names no snapshot now raise `ValueError: Snapshot not found: <id>`, instead of returning no tasks and `0` (`・ω・´)
## AI Disclosure
Human-led contribution. Grok Bot, working for dragonknightk, assisted with this change. All submitted content was reviewed by a human before submission.
|
@RyanLin5967 thanks for flagging this one — a The fix is tiny: two lines in One check is red and not from this PR: @Fokko ready for review when you have a moment (´。• ᵕ •。`) |
Closes #4197
Rationale for this change
A scan whose
snapshot_idnames no snapshot, for example an expired one, answers two ways:plan_files()returns no tasks andcount()returns0, whileto_arrow()on the same scan raisesValueError: Snapshot not found: <id>.Reproducer:
t.maintenance.expire_snapshots().by_id(s1).commit()t.scan(snapshot_id=s1).count()returns0andplan_files()returns[]t.scan(snapshot_id=s1).to_arrow()raisesValueError: Snapshot not found: ...The local planning path returned
[]wheneverself.snapshot()wasNone, which covers both an empty table and an id that is not in the metadata. Only the empty table should plan to nothing.Java makes the same distinction.
SnapshotScan.useSnapshotrejects an unknown id withPreconditions.checkArgument(table().snapshot(scanSnapshotId) != null, "Cannot find snapshot with ID %s", scanSnapshotId), so thesnapshot == nullbranch inSnapshotScan.planFiles()only returns an empty result for an empty table.What changed
DataScan._plan_files_localValueError(f"Snapshot not found: {self.snapshot_id}")when asnapshot_idis set but not found, the same error thatprojection()and thereforeto_arrow()already raisecount()plan_files(), so it raises as wellto_arrow(),to_arrow_batch_reader()projection(), unchangedsnapshot_idon a table without snapshotsWhy planning, not the scan constructor
Java checks when the snapshot is set on the scan; this change checks when the files are planned. pyiceberg accepts the id in the
DataScanconstructor and inupdate(), andTableScan.snapshot()returnsNonefor an unknown id, whichtest_table_scan_missing_snapshot_zerorelies on. Checking in planning keeps those as they are and raises at the same point asto_arrow(), which only fails once it is called.Server-side planning
Unchanged: the snapshot id is sent to the REST server, which plans against its own metadata. The Java reference implementation calls
useSnapshotinCatalogHandlers.planTableScan, so it rejects an unknown id with a 400 that pyiceberg raises asBadRequestError. With server-side planning,plan_files()andcount()therefore raise the server's error when the server rejects the id, instead of theValueErrorthatto_arrow()raises fromprojection()before planning (・ω・)bAre these changes tested?
Yes, in
tests/table/test_init.py:test_table_scan_plan_files_missing_snapshotplan_files()andcount()raiseSnapshot not found: 0for a scan oftable_v2with snapshot id0, which the fixture does not have. The id0also makes sure the check does not treat the id as a boolean. Without this change, the test fails withDID NOT RAISE ValueErrortest_data_scan_plan_files_no_current_snapshotplan_files()returns[]andcount()returns0for a scan without asnapshot_idof a table without snapshotsAre there any user-facing changes?
Yes, a bug fix:
plan_files()andcount()on a scan whosesnapshot_idnames no snapshot now raiseValueError: Snapshot not found: <id>, instead of returning no tasks and0(`・ω・´)AI Disclosure
Human-led contribution. Grok Bot, working for dragonknightk, assisted with this change. All submitted content was reviewed by a human before submission.