What did the catalog say last Tuesday?

2026-06-18T16:06:40Z by Showboat 0.6.1

What did the catalog say last Tuesday?

Keep every nightly snapshot — deduplicated to just the churn — and any past state is yours.

The catalog changes every day: titles get corrected, records arrive, copies go missing. A lake that only holds today can’t answer the question an auditor, a cataloger, or a puzzled librarian eventually asks — what did the record say back then? Storing a full copy of a two-million-bib catalog every night would answer it, expensively, in a pile of near-identical snapshots.

The sierra-snapshot tenant does something leaner: it keeps a bitemporal bronze that records only what actually moved each night, every row stamped with the epoch it opened and (once superseded) the epoch it closed. The full state of any past night is then reconstructed on demand. This walkthrough runs the real engine — the pure diff_epoch classifier and the BronzeStore envelope (in-memory DuckDB) — over a tiny synthetic catalog across three nights. Every command reproduces; showboat verify re-runs each one and diffs the output.

1 · The engine — classify one night against the last

Each night’s snapshot is set-diffed against the prior open state. The classifier is pure — maps of pk -> row_hash in, an EpochDelta out, no I/O and no epoch arithmetic. It sorts every key into new, changed, deleted, or unchanged, and from those derives the two actions the store applies: open a fresh row (new ∪ changed) and close the prior one (deleted ∪ changed). Unchanged rows are left alone — that no-op is the whole dedup.

uv run python docs/demos/time-travel/_driver.py diff

The engine — classify one night against the last
================================================
Every night's snapshot is set-diffed against the prior open state. The diff is PURE
(pk -> row_hash maps in, an EpochDelta out — no I/O, no epoch math). The version tokens
below stand in for the content row_hash:

  prior open (Mon): {'101': 'holes-v1', '102': 'wonder-v1', '103': 'hatchet-v1'}
  current   (Tue): {'101': 'holes-v1', '102': 'wonder-v2', '104': 'terabithia-v1'}

  new=1 changed=1 deleted=1 unchanged=1
  opened (insert a fresh row): ['102', '104']
  closed (cap the prior row) : ['102', '103']

  opened = new ∪ changed; closed = deleted ∪ changed; the UNCHANGED row (101) is a
  no-op — that's the dedup that keeps storage ≈ base + churn, not a full copy per night.

2 · Three nightly snapshots — stored as churn, not copies

Now feed three real nights through a BronzeStore. Each returns its EpochDelta, and the envelope ends up holding a base plus the churn — only the rows that actually moved — instead of a full copy per night. That’s what makes “keep every snapshot forever” affordable.

uv run python docs/demos/time-travel/_driver.py snapshots

Three nightly snapshots — stored as churn, not copies
=====================================================
  epoch 1: new=3 changed=0 deleted=0 unchanged=0
  epoch 2: new=1 changed=1 deleted=1 unchanged=1
  epoch 3: new=0 changed=1 deleted=0 unchanged=2

  envelope rows stored: 6     (a base of 3 + 3 churn events)
  full nightly copies would be: 9
  Unchanged rows are never rewritten — only what actually moved is recorded, each row
  stamped with the epoch it opened and (once superseded) the epoch it closed.

3 · Time-travel — what did the catalog hold each night?

Because every row carries the epoch it was open from and to, reconstructing a past night is a single as_of(epoch) query: return the rows whose window contains it. Here is the same catalog read back as it stood on each of the three nights — the corrected title, the withdrawn record, the new arrival, the copy that went missing, each exactly where it was.

uv run python docs/demos/time-travel/_driver.py time_travel

Time-travel — what did the catalog hold each night?
===================================================

  as_of(epoch 1)  — Monday night:
      bib 101  Holes                             Available
      bib 102  Wonder                            Available
      bib 103  Hatchet                           Available

  as_of(epoch 2)  — Tuesday night:
      bib 101  Holes                             Available
      bib 102  Wonder (10th anniversary ed.)     Available
      bib 104  Bridge to Terabithia              Available

  as_of(epoch 3)  — Wednesday night:
      bib 101  Holes                             Available
      bib 102  Wonder (10th anniversary ed.)     Available
      bib 104  Bridge to Terabithia              Missing

  Read bib 102 down the nights: its title is corrected between Monday and Tuesday and
  the old wording is still recoverable. Bib 103 (Hatchet) exists on Monday and is gone
  by Tuesday; bib 104 arrives Tuesday and goes Missing on Wednesday. Every past state is
  reconstructed exactly — not a guess, the rows that were actually open that night.

4 · The safety rails — idempotent + monotonic

A timeline is only trustworthy if you can’t corrupt it. Re-running an epoch with the same data is a no-op (so a backfill is safe to retry), and epochs must move forward — feeding an out-of-order epoch is refused with an error, never silently misfiled into the past.

uv run python docs/demos/time-travel/_driver.py safety

The bitemporal safety rails — idempotent + monotonic
====================================================
Re-running an epoch with the same data is a no-op (re-ingest epoch 3):
  delta -> new=0 changed=0 deleted=0 unchanged=3   (nothing rewritten)

And epochs must move forward — feeding an out-of-order epoch is refused, not silently
misfiled (try to ingest epoch 2 after the store is already at epoch 3):
  ValueError: out-of-order epoch 2 for 'catalog': bronze is already at epoch 3; ingest epochs in ascending order

Idempotent re-runs + strictly non-decreasing epochs are what make the timeline
trustworthy: you can re-run a backfill safely, and you can never corrupt the past.

Proof

Every command above re-runs under showboat verify. The on-thesis invariants — as_of reconstructs each night exactly, a changed row keeps its past value recoverable, an out-of-order epoch is refused, and re-running an epoch is a no-op — are pinned by the demo’s own guard test, run against the real engine:

uv run pytest tests/demos/test_time_travel_demo.py -q 2>&1 | sed -E 's/ in [0-9.]+s//'
......                                                                   [100%]
6 passed

Where this goes next — open questions, not commitments

Bitemporal bronze ships today for the snapshot tenant. The next moves are ideas, not commitments, posed as questions:

The discipline is the point. We ship the part we can stand behind — every night kept as churn, every past state reconstructed exactly, the timeline impossible to corrupt — and it all re-runs on demand.


all walkthroughs · Rendered from 566da3f on 2026-06-18 · showboat verify: reproduces. A living artifact — the version ledger is git.