Storage

Time Travel

Query previous acceleration generations with timestamp-oriented syntax and timeline helpers.

Time travel is generation-based. Each successful acceleration refresh writes immutable parquet row groups and stamps them with a monotonic generation number per table (the optional acceleration layer). Historical reads resolve a timestamp or generation, and the scan then ignores row groups newer than that generation. It is intended primarily for audit, debugging, and reproducibility; it does not need to be the fastest route in the system.

The heap stays the source of truth for latest reads, pg_dump, and rebuilds. Time-travel reads are over the accelerated parquet layer, so a table needs at least one successful rvbbit.refresh_acceleration(...) or rvbbit.compact(...) before it has historical generations to read.

Query-Level Shorthand#

The preferred user-facing shorthand is a leading SQL comment, which scopes the whole statement without exposing generation numbers:

/* rvbbit: as_of = '2026-05-31 14:30:00-04' */
SELECT account_id, count(*)
FROM events
GROUP BY account_id;

Line comments work too:

-- rvbbit: as_of = '2026-05-31 14:30:00-04'
SELECT count(*) FROM events;

The comment is whole-query scope, similar to SET LOCAL: every rvbbit table in the statement is resolved against the same timestamp, and each table resolves that timestamp to its own generation internally.

A parser-level table clause (SELECT * FROM events AS OF TIMESTAMP '...') is not supported today — PostgreSQL rejects that syntax before extension hooks run, so use the comment form, rvbbit.set_as_of(...), or the GUC instead.

Session Form#

For a timestamp-based read that several statements should share, use the rvbbit.set_as_of(rel, ts) helper. It finds the newest generation committed at or before the timestamp, sets the rvbbit.as_of_generation GUC at session scope, and returns the generation it selected. Always pair it with rvbbit.set_as_of_reset():

SELECT rvbbit.set_as_of('events'::regclass, '2026-05-31 14:30:00-04'::timestamptz);

SELECT *
FROM events
WHERE account_id = 42;

SELECT rvbbit.set_as_of_reset();

The lowest-level control is the rvbbit.as_of_generation GUC, which takes an integer generation rather than a timestamp. Use SET LOCAL inside a transaction to scope it to one read; a positive value means "read row groups with generation <= value", and unset/empty/zero/negative means "latest":

BEGIN;
SET LOCAL rvbbit.as_of_generation = '3';

SELECT *
FROM events
WHERE account_id = 42;

COMMIT;

Timeline Discovery#

To inspect a table's generations, newest first:

SELECT *
FROM rvbbit.list_generations('events'::regclass);

That returns generation, committed_at, n_rows, and n_row_groups. For just the latest generation, use rvbbit.current_generation('events'::regclass).

UIs need a cheap way to show available snapshot ticks without scanning files. The metadata-only helper rvbbit.time_travel_timeline(...) reads the generation log, row-group catalog, and delete log — it never scans the heap or parquet:

SELECT *
FROM rvbbit.time_travel_timeline('events'::regclass);

It returns one row per generation (newest first) with:

  • generation — the generation identifier,
  • committed_at — when the generation committed,
  • rows_written / row_groups_written — the delta written at that tick,
  • visible_rows_estimate — approximate rows visible at that tick, derived from row-group metadata minus generation-aware tombstones,
  • visible_row_groups,
  • tombstones_visible.

Routing#

Time-travel queries favor correctness over speed. The router may use a narrower set of accelerated layouts, and falls back when a candidate cannot prove it can represent the requested generation.

What to expect: a historical query aims to return the correct result for that generation, not necessarily the fastest one. The timeline helper stays cheap, fallback is explicit when files are unavailable, and time-travel runs are kept out of the normal route-calibration data.

Retention#

Time travel gets more expensive the more generations you retain. Use the rvbbit.reap_generations(reloid regclass DEFAULT NULL, keep_days integer DEFAULT 30) reaper to age out old generations — it deletes catalog rows and unlinks the parquet files for generations strictly below the live snapshot whose committed_at is older than keep_days. Passing NULL reaps every eligible table, which makes it convenient to schedule:

-- Drop generations older than 14 days across all eligible tables.
SELECT * FROM rvbbit.reap_generations(NULL, 14);

The reaper only touches snapshot tables (those with a positive min_visible_generation) and never the current generation. Append tables are skipped, because their generations are cumulative and reaping an old one would drop live rows. There is no automatic retention schedule today — when and how aggressively to reap is your call.