Snapshot dependency tracking
September 14, 2026 ยท View on GitHub
Dependency tokens let an in-process cache reuse a projection when its selected
database attributes have not changed. They are experimental and available in
Clojure and ClojureScript through datahike.dependency-tracking.
Enroll a named selector in a transaction, then capture the token and projection from the same resulting database value:
(require '[datahike.api :as d]
'[datahike.dependency-tracking :as deps])
(def selector {:namespaces #{"catalog"}
:exclude-attributes #{:catalog/counter}})
(def report
(d/transact conn
{:tx-data []
:tx-options
{:track-dependencies
{::catalog selector}}}))
(def snapshot (:db-after report))
(def cached-token (deps/token snapshot ::catalog selector))
(def cached-value (d/q '[:find ?e ?name
:where [?e :catalog/name ?name]] snapshot))
;; Reuse cached-value only when this returns true; otherwise recompute it
;; and capture a new token from the same DB used for recomputation.
(deps/valid? cached-token @conn ::catalog selector)
The transaction option also works with d/with. Selectors combine exact keyword
:attributes, exact string :namespaces, and string :namespace-prefixes.
:exclude-attributes takes precedence over those selections. Schema and ident
changes always invalidate every group, even when excluded. Include every
attribute on which the projection depends, including attributes used only in
filters, joins, or ordering. Tracking a namespace also tracks new attributes in
that namespace. Selecting "db" includes :db/txInstant, which normally changes
on every transaction.
Named groups belong to each immutable DB value. Ordinary transactions inherit them; a matching mutation replaces that group's token before any subsequent transaction function runs. Intermediate transaction values therefore get their own tokens, and a failed transaction cannot invalidate the original snapshot. Changing a selector gives it a fresh token. A nil selector removes the group; omitting a group leaves it unchanged.
Pass the expected selector to token and valid? when other code can enroll
the same group id. A mismatched selector returns no token, even if that group
is tracked. This prevents a replacement with a narrower selector from silently
weakening the cache's dependencies. The shorter arities omit this check and are
appropriate only when the caller controls the group's enrollment.
There are at most 16 groups per DB, with at most 256 total selector terms per group. Group ids and selector terms are limited to 512 characters. These bounds limit work on each mutation; unrelated row writes do not scan the catalog or allocate replacement tokens.
Tokens are opaque identities, not transaction numbers, equality hashes, or durability certificates. A changed token can still describe an equal projection; recomputation is the conservative response. An unchanged token proves only that no selected mutation occurred along the tracked lineage. It does not prove that a speculative transaction committed or that a branch still points to that DB.
Tracking is runtime-only: it changes no storage format and is not restored by reconnecting or loading a stored DB. Wholesale replacement drops tracking. Filtered and temporal views return nil rather than inheriting a certificate from their underlying current DB. Nil tokens never validate, including against another nil token. Re-enroll after loading an untracked DB.
This facility assumes changes go through Datahike's transaction and database operations. It cannot detect mutation of a Java array or date object held inside a value, nor edits to DB internals made by application code. Cache only projections whose input values obey the immutable-value contract. Tokens are local to the runtime and are not a remote-client cache protocol.