Time Travel
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 reads as the EMPTY graph — it is not an error. Reconstruction walks the delta chain backwards from the id you name, and an id that isn’t in the log has an empty chain, so the result is a graph with no nodes:
count(n)returns0, everyMATCHreturns nothing. Your application cannot tell this apart from “the query matched nothing” at that point in time.This bites in two real ways: a typo’d or stale engram id read by a backend, and — more insidiously — an id that has been squashed away by retention (see Engrams). A squashed engram is genuinely gone, so reading
AS OFit silently returns zero rows rather than telling you the history you asked for no longer exists.If your application time-travels to ids it stores, existence-check first rather than trusting an empty result —
CALL patinadb.engrams()lists the reachable timeline. Better still, use a tag — it is pinned against squash, so the point it names stays reachable andSHOW TAGStells you whether it is still there.
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.
The cheapest historical read of all remains a tag: tagging snapshots its
engram, so AS OF TAG never replays a long delta chain however old the point is.
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 and snapshots its engram:
- Pinned — a tagged engram is protected from squash: compaction never collapses it, so the point-in-time it names stays reachable.
- Snapshotted — a full snapshot is taken at the tagged engram, so reading
AS OFthat tag is cheap (no long delta replay), however old it is.
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, pins, and snapshots the same engram — so AS OF TAG and SHOW TAGS
work against any node (including followers). SHOW TAGS is a local read;
CREATE/DROP TAG need admin and go through the leader.