Skip to main content

Real Iceberg Rollback in Pure SQL

· 4 min read
Gnok Team

Most engines say they support time travel. Watch what happens when you actually try to roll back. Plenty of catalogs have shipped a "snapshot rollback" that quietly only edits the table's current-snapshot-id property — subsequent reads still hit the latest data. Until recently, gnok was one of them.

This post walks through how that path is now wired end-to-end: ALTER TABLE … SET SNAPSHOT actually flips the read pointer, the new is_current / parent_snapshot_id / sequence_number columns surface state honestly, and the $snapshots system view makes Iceberg metadata first-class SQL.

A demo you can re-run​

-- 0. Clean slate
DROP TABLE IF EXISTS analytics.hr.staff;

CREATE TABLE analytics.hr.staff (
id INT,
name VARCHAR,
salary INT
) COMMENT = 'snapshot demo';

-- 1. INSERT → snapshot S1 (seq=1, parent=NULL)
INSERT INTO analytics.hr.staff VALUES
(1, 'Alice', 100),
(2, 'Bob', 80),
(3, 'Carol', 90);

-- 2. UPDATE → snapshot S2 (seq=2, parent=S1)
UPDATE analytics.hr.staff SET salary = 150 WHERE id = 1;

-- 3. DELETE → snapshot S3 (seq=3, parent=S2)
DELETE FROM analytics.hr.staff WHERE id = 2;

SELECT id, name, salary FROM analytics.hr.staff ORDER BY id;
-- Alice=150, Carol=90 (no Bob)

Three commits, three snapshots. SHOW SNAPSHOTS returns the expected timeline:

SHOW SNAPSHOTS FROM analytics.hr.staff;

snapshot_id | is_current | parent_snapshot_id | seq | operation
1777740163004 | false | NULL | 1 | append
1777740187411 | false | 1777740163004 | 2 | overwrite
1777740201632 | true | 1777740187411 | 3 | delete

Two columns there are new. is_current says which snapshot read queries hit right now. parent_snapshot_id lets you walk the lineage back without a second SHOW TBLPROPERTIES round-trip.

The rollback​

-- Roll back to S1 — clean DDL form
ALTER TABLE analytics.hr.staff SET SNAPSHOT 1777740163004;

Equivalent forms the parser accepts:

ALTER TABLE analytics.hr.staff SET CURRENT SNAPSHOT 1777740163004;
ALTER TABLE analytics.hr.staff SET VERSION AS OF 1777740163004;
ROLLBACK TABLE analytics.hr.staff TO SNAPSHOT 1777740163004;
ROLLBACK TABLE analytics.hr.staff TO PARENT; -- one snapshot back
ALTER TABLE analytics.hr.staff SET TIMESTAMP AS OF 1777740164000;

The rollback is declarative — it does not write a new commit. It flips the table's main pointer back to S1. Newer snapshots stay on disk so a roll-forward works:

SELECT id, name, salary FROM analytics.hr.staff ORDER BY id;
-- Alice=100, Bob=80, Carol=90 ← original 3 rows back

-- Roll forward to S3
ROLLBACK TABLE analytics.hr.staff TO SNAPSHOT 1777740201632;

SELECT id, name, salary FROM analytics.hr.staff ORDER BY id;
-- Alice=150, Carol=90 ← back to current state

is_current flips correspondingly. No new snapshots get created during rollback — the history stays clean.

$snapshots — system metadata as queryable SQL​

Inspecting Iceberg metadata used to require shelling out to pyiceberg or running SHOW TBLPROPERTIES and parsing the result. The engine now exposes per-table system views you can SELECT from directly. The full set:

ViewContents
$snapshotsSnapshot metadata + summary stats
$historySnapshot history with ancestry tracking
$filesData files with partition info + column stats
$manifestsManifest files with file/row counts
$partitionsPartition statistics aggregated from data files
$refsBranch + tag references
$entriesAll manifest entries (incl. deleted)
$all_data_files, $all_delete_files, $all_manifestsCross-snapshot views
-- Every snapshot of a single table
SELECT snapshot_id, is_current, sequence_number, operation
FROM analytics.hr.staff.$snapshots
ORDER BY sequence_number DESC
LIMIT 10;

-- The active head only
SELECT snapshot_id, sequence_number, operation
FROM analytics.hr.staff.$snapshots
WHERE is_current = TRUE;

-- Oldest non-current snapshot (next candidate for EXPIRE)
SELECT snapshot_id, sequence_number
FROM analytics.hr.staff.$snapshots
WHERE NOT is_current
ORDER BY sequence_number ASC
LIMIT 1;

The views are scoped per-table (<catalog>.<schema>.<table>.$snapshots), not per-schema. For cross-table inventories use information_schema.tables joined to whichever metadata view applies.

Previously, $snapshots parsed but silently ignored any WHERE / ORDER BY / LIMIT you wrote — the system-table path short-circuited the planner. Now they go through the SQL planner properly: filters push down, orderings hold, projections work.

The metadata views inherit the same RBAC surface as regular tables, so audits work without a separate access path.

Snapshots tab in studio​

The studio Catalog Browser's Snapshots tab consumes the new columns directly — the current badge is sourced from is_current, parent-child lineage is shown alongside each row, and the rollback button calls ALTER TABLE … SET SNAPSHOT under the hood.

If you have a tab open against a rolled-back table, the result panel shows an amber "rolled back" chip below the row count — because reading from a non-newest snapshot is the kind of bug people lose hours to, and silently doing it is worse than the warning.

Why it matters​

Time travel is one of Iceberg's headline features. Half-shipped time travel is a worse experience than no time travel at all — users learn not to trust it. The new path is honest about what it does and exposes the metadata you'd want to verify it.

Try the demo above against your own catalog and watch is_current flip.