Time Travel
Historical reads use recorded snapshots and forward changes, not guessed undo
against the current graph. For a selective node-property filter followed by a
plain WITH node and one typed relationship hop, reconstruction can retain only
the candidate nodes and their needed neighbors (#1261). The original query still
evaluates all filters and OPTIONAL MATCH behavior. Broad or unsupported shapes
fall back to the existing reconstruction path; this is not a universal cold-read
latency guarantee. See limitations for qualification status.
Because every change is recorded as an engram, patinaDB can answer read queries against the graph as it was at any past engram. The state is reconstructed (nearest snapshot + forward delta replay) into a temporary view, and your query runs against that view. The live graph is never modified.
USE … AS OF
Prefix a read with USE <db> AS OF '<engram-id>' to travel back in a specific
database:
USE sales AS OF '<engram-id>'
MATCH (o:Order) RETURN count(o)
Over REST you can equivalently pass an at field in the request body; over Bolt
the USE … AS OF prefix is parsed per query.
Semantics & constraints
- Reads only. Time travel reconstructs a read-only past view. You cannot write to the past or “restore” the database to an old state through time travel (that’s a different operation — full snapshot import/export).
- Consistent point-in-time. A time-travel query sees the entire graph as it was at that engram — a coherent snapshot, not a mix of old and new.
- Cost. Reconstruction is bounded by the distance from the nearest snapshot to the target engram. Frequent snapshots (see Engrams) keep this cheap; querying a point far from any snapshot replays more deltas. See Performance & tuning below.
An unknown engram id is an ERROR, not the empty graph (changed in issue #590).
AS OFan id that is not in the database’s history — a typo’d or stale id, or one squashed away by retention (see Engrams) — fails with an actionableunknown engram …error, so your application can always tell “that point in history is gone” apart from “the query matched nothing”. (Only the low-levelDataset::reconstructAPI keeps its empty-on-unknown behaviour; the user-facingAS OF/ restore entry points validate the id first.)Even so, if your application time-travels to ids it stores, prefer a tag — it is pinned against squash, so the point it names stays reachable and
SHOW TAGStells you whether it is still there.
Damaged history is an ERROR too — never a smaller graph (issue #1145). The engram history is a chain: each point is reconstructed from the nearest stored snapshot plus the deltas above it. If any of those pieces is missing — a lost chain link, a lost delta, a snapshot file that went away while its metadata still claims it — the read fails, naming the engram it could not resolve.
It did not used to. Each missing piece read as “nothing here”: a lost chain link looked like “no snapshot in this history” and replay restarted from an empty graph, a lost delta replayed as “that commit did nothing”. The answer was a graph that never existed, returned with no error at all — measured in one real case at 5 nodes where 40 were correct. Nothing distinguished it from a genuinely small result.
You should never see this: it means storage damage or partial data loss, not a normal condition. What matters is that you now see it as an error instead of as data. Take a backup restore seriously if it appears — and note that a live (non-
AS OF) query is unaffected, since it reads the current graph directly rather than replaying history.
Performance & tuning
An AS OF read is served by one of several paths. All of them return the
byte-identical result — the choice affects only speed, and every acceleration
falls back to a full reconstruct when it can’t prove it applies. That is why
these knobs are safe to flip in either direction on a live system.
- Full reconstruct (the floor). Materialises the whole graph as of the target engram. Always correct, cost grows with graph size.
- Warm-window reconstruct (always on). The nearest snapshot’s base state is cached and shared by every read in that window; only the in-window deltas are replayed on top. A second read in the same window is dramatically cheaper than the first.
- Label-scoped partial reconstruct (
PATINADB_PARTIAL_TIMETRAVEL, default on). When a query’s labels and relationship types are statically bounded, only that subgraph is rebuilt instead of the whole graph. An unbounded shape — a bareMATCH (n), a variable-length hop, a procedure call — falls back automatically. Read-only, so a write-only workload pays nothing for it. - Point-index lookup (
PATINADB_INDEXED_SNAPSHOTS, opt-in). For a cold single-vertex lookup anchored onid(n) = '<uuid>', a per-snapshot.snapidxsidecar turns a full-graph rebuild into a point read.
Practical guidance. Leave PATINADB_PARTIAL_TIMETRAVEL on; if you ever
suspect it, =0 restores the full reconstruct for every read — slower, identical
results. Reach for PATINADB_INDEXED_SNAPSHOTS only when a known set of
historical points is read repeatedly: the sidecar is built lazily and in the
background, so the read that triggers a build is itself served by the scan path
and only later reads benefit. Bound its disk with
PATINADB_MAX_SNAPSHOT_INDEXES (default 16 sidecars). Neither knob touches the
write path, so neither can slow a commit or a cluster’s apply loop.
A tag requests a snapshot at its engram. Once that snapshot is available, reconstruction can start there without replaying earlier deltas. Raft-server materialization is asynchronous: acknowledgement is not snapshot readiness, and a snapshot does not guarantee fast queries. See the unresolved 1M historical workload findings in limitations.
Tags — named, snapshotted points in history
A tag is a stable name for an engram (like a git tag), so you can time-travel to a meaningful point without tracking raw engram ids. Creating a tag also pins its engram and requests a snapshot:
- Pinned — a tagged engram is protected from squash: compaction never collapses it, so the point-in-time it names stays reachable.
- Snapshot preparation — on a Raft server, a background job builds the full snapshot. Until it completes, reads use existing checked reconstruction; incomplete history is reported rather than replaced by the current state.
CREATE TAG v1 -- tag the current HEAD
CREATE TAG release AS OF '<engram-uuid>' -- tag a specific engram
SHOW TAGS -- list name → engram
DROP TAG v1 -- remove (unpins if no other tag references it)
Read a database as it was at a tag:
USE default AS OF TAG 'v1' MATCH (n) RETURN n
On a cluster, CREATE TAG / DROP TAG replicate through Raft — every node
names and pins the same engram. Snapshot preparation finishes independently on
each node. AS OF TAG works where the required history is available, including
followers. SHOW TAGS is a local read; CREATE/DROP TAG need admin and go
through the leader. The returned bookmark covers tag creation, not snapshot
completion. Use admin-only SHOW JOBS on the target node
to inspect preparation. Embedded Dataset::create_tag remains synchronous.