-
Notifications
You must be signed in to change notification settings - Fork 8
Cookbook Time Travel
Recipes for querying Minigraf's bi-temporal history. Organised by the four temporal query classes from the RecallGraph taxonomy.
← Graph Traversal | Bitemporal Modeling →
A point-in-time query returns the state of the database at a single frozen moment. Both time axes (transaction time and valid time) support point-in-time queries independently or combined.
Problem: Query the database exactly as it existed after transaction N.
;; Config table updated three times
(transact [[:cfg :app/max-conn 10]]) ;; tx 1
(transact [[:cfg :app/max-conn 50]]) ;; tx 2
(transact [[:cfg :app/max-conn 100]]) ;; tx 3
;; What was max-conn after tx 1?
(query [:find ?v
:as-of 1
:valid-at :any-valid-time
:where [:cfg :app/max-conn ?v]])
;; => 10
Notes:
-
:as-of Nuses the sequential tx counter (1, 2, 3…), not the Unix ms tx-id shown by the REPL's "Transacted (tx: …)" output - Each
(transact [...])increments the counter once regardless of how many facts it contains -
:valid-at :any-valid-timesuppresses the default valid-time filter; omit it only if your facts carry explicit valid-time ranges that you want applied
Visualize: Open this recipe in the time travel visualizer →
Problem: Query the database as it existed at a specific wall-clock timestamp.
(query [:find ?v
:as-of "2025-06-01T12:00:00Z"
:valid-at :any-valid-time
:where [:cfg :app/max-conn ?v]])
Notes:
- Matches the latest transaction whose
tx_id(Unix ms) is ≤ the given timestamp - Timestamps must be ISO 8601 UTC; Minigraf rejects local-timezone or offset timestamps
- If no transaction existed before the given timestamp, the query returns empty
Problem: Query what was true in the real world on a specific date, regardless of when it was recorded.
;; Employment history with explicit valid-time ranges
(transact {:valid-from "2020-01-01" :valid-to "2023-06-01"}
[[:alice :works-at :techcorp]])
(transact {:valid-from "2023-06-01"}
[[:alice :works-at :startupco]])
;; Where did Alice work on 2021-07-01?
(query [:find ?company
:valid-at "2021-07-01"
:where [:alice :works-at ?company]])
;; => :techcorp
;; Where does Alice work today?
(query [:find ?company
:where [:alice :works-at ?company]])
;; => :startupco (no :valid-at = currently valid facts only)
Notes:
-
:valid-at "date"filters to facts wherevalid-from ≤ date < valid-to - Omitting
:valid-atreturns only facts currently valid (valid-to = foreverandvalid-from ≤ now) -
:valid-at :any-valid-timereturns all facts regardless of valid-time — use this when you need the full history
Visualize: Open this recipe in the time travel visualizer →
Problem: Ask "what did the database know at transaction N about what was true on date D?"
;; As-of tx 1 (only TechCorp recorded): where was Alice in 2024?
(query [:find ?company
:as-of 1
:valid-at "2024-01-01"
:where [:alice :works-at ?company]])
;; => empty — TechCorp expired before 2024; StartupCo not yet in DB at tx 1
;; As-of tx 2 (both recorded): where was Alice in 2024?
(query [:find ?company
:as-of 2
:valid-at "2024-01-01"
:where [:alice :works-at ?company]])
;; => :startupco
Notes:
-
:as-ofcontrols what was recorded;:valid-atcontrols what was true in the real world - Use this for auditing decisions: "what information did we have available when we made choice X?"
- Both axes are fully independent; combining them narrows the result to the intersection
Visualize: Open Recipe 3's data at :as-of 1, :valid-at 2024-01-01 in the time travel visualizer →
A time interval query gathers data across a range of the timeline rather than at a single point.
Problem: List every value ever recorded for an entity/attribute pair, including retracted values.
;; All salary values ever recorded for :alice (including retracted ones)
(transact [[:alice :salary 75000]]) ;; tx 1
(retract [[:alice :salary 75000]])
(transact [[:alice :salary 80000]]) ;; tx 3
(query [:find ?salary ?tx
:valid-at :any-valid-time
:where [:alice :salary ?salary]
[:alice :db/tx-count ?tx]])
;; => [(75000, 1), (80000, 3)]
Notes:
-
:valid-at :any-valid-timewith no:as-ofreturns all facts ever asserted, including retracted ones -
[:alice :db/tx-count ?tx]binds the tx-count of each salary fact — the pseudo-attribute correlates with the preceding real-attribute pattern on the same entity - Sort by
?txto read changes in chronological order
Visualize: Open this recipe in the time travel visualizer →
Problem: Find all values asserted for an attribute within a specific transaction range.
;; Price changes between tx 3 and tx 7
(query [:find ?price ?tx
:valid-at :any-valid-time
:where [:product-x :product/price ?price]
[:product-x :db/tx-count ?tx]
[(>= ?tx 3)]
[(<= ?tx 7)]])
Notes:
-
?txis an integer (sequential counter); compare with>=and<=expression predicates - To find changes across all entities in a tx range, replace the entity with
?e:[?e :product/price ?price] [?e :db/tx-count ?tx] - Combine with
:db/tx-id ?tito get the wall-clock time of each change:[(/ ?ti 1000) ?unix-sec]
Problem: Find all facts that were valid at any point during a given date range.
;; Insurance policies valid at any point during 2023
;; Overlap condition: fact started before range-end AND fact ended after range-start
;; 2023-01-01 = 1672531200000 ms | 2024-01-01 = 1704067200000 ms
(query [:find ?policy ?name
:valid-at :any-valid-time
:where [?policy :policy/name ?name]
[?policy :db/valid-from ?vf]
[?policy :db/valid-to ?vt]
[(< ?vf 1704067200000)] ;; started before end of range
[(>= ?vt 1672531200000)]]) ;; ended on or after start of range
;; => all policies with any overlap with 2023
Notes:
- Valid-time values are stored as Unix milliseconds; pre-compute range boundaries before querying
- Open-ended facts have
valid-to = 9223372036854775807(i64::MAX) and always satisfy>= range_start - The overlap condition is:
fact_start < range_end AND fact_end >= range_start
A time-point lookup finds the point(s) on the timeline where given criteria were satisfied — the inverse of point-in-time.
Problem: Find the transaction at which a specific fact was first recorded.
;; When was :alice's name first recorded?
(query [:find (min ?tx)
:valid-at :any-valid-time
:where [:alice :person/name _]
[:alice :db/tx-count ?tx]])
;; => the earliest tx count for any name fact for :alice
Notes:
-
(min ?tx)returns the earliest;(max ?tx)returns the most recent assertion - To get the wall-clock time instead: replace
:db/tx-countwith:db/tx-id— the value is Unix milliseconds - To find the first assertion across all entities for an attribute, use
?einstead of:aliceand add?eto:find
Problem: Find the valid-time at which a specific fact started being true in the real world.
;; When did Alice's employment at TechCorp begin (valid-time)?
(query [:find ?vf
:valid-at :any-valid-time
:where [:alice :works-at :techcorp]
[:alice :db/valid-from ?vf]])
;; => Unix ms timestamp corresponding to 2020-01-01
Notes:
-
[:alice :db/valid-from ?vf]binds thevalid-fromof the fact matched by the preceding pattern - If the fact was asserted without an explicit
valid-from, the value equalstx_id(the wall-clock time of the transaction) - Divide the result by 1000 to get Unix seconds; divide by 86400000 for days since epoch
Problem: Find the valid-time at which a specific fact stopped being true.
;; When did Alice's TechCorp employment end?
(query [:find ?vt
:valid-at :any-valid-time
:where [:alice :works-at :techcorp]
[:alice :db/valid-to ?vt]
[(< ?vt 9223372036854775807)]]) ;; exclude open-ended facts
;; => Unix ms timestamp corresponding to 2023-06-01
Notes:
-
9223372036854775807isi64::MAX— the sentinel for "valid forever"; filter it out to find only expired facts - Retracted facts may carry
valid-to = i64::MAXat the moment of retraction; use:as-ofto distinguish retracted-then-replaced from genuinely expired - To find all facts that expired before a given date: add
[(<= ?vt <date-ms>)]
A time-interval lookup finds the interval(s) during which given criteria held — the inverse of time-interval.
Problem: Compute how long a specific fact was continuously valid.
;; Duration of Alice's TechCorp employment in milliseconds
(query [:find ?duration
:valid-at :any-valid-time
:where [:alice :works-at :techcorp]
[:alice :db/valid-from ?vf]
[:alice :db/valid-to ?vt]
[(< ?vt 9223372036854775807)] ;; exclude open-ended
[(- ?vt ?vf) ?duration]])
;; => duration in ms; divide by 86400000 for days
Notes:
-
[(- ?vt ?vf) ?duration]is an arithmetic binding — it computes and binds the result - Filter open-ended facts (
valid-to = i64::MAX) first; subtracting i64::MAX overflows - For open-ended facts, substitute the current Unix ms timestamp as the upper bound before computing duration
Problem: List every time interval during which a given entity/attribute pair held a value.
;; All employment periods for :alice (all employers, all intervals)
(query [:find ?company ?vf ?vt
:valid-at :any-valid-time
:where [:alice :works-at ?company]
[:alice :db/valid-from ?vf]
[:alice :db/valid-to ?vt]])
;; => [(:techcorp, <vf-ms>, <vt-ms>), (:startupco, <vf-ms>, 9223372036854775807)]
Notes:
- Results include all historical intervals; open-ended ones show
valid-to = 9223372036854775807 - Order by
?vfto read the history chronologically - Add
[(< ?vt 9223372036854775807)]to show only closed (expired) periods; remove it to include the current open interval
Visualize: See every valid period of Recipe 3's data on the bitemporal map →
Problem: Record which actor made each change, and later find all changes made by a specific actor.
;; Transact actor metadata alongside the data change — same tx-count ties them together
(transact [[:tx-meta-1 :tx/actor "alice@example.com"]
[:tx-meta-1 :tx/reason "quarterly-review"]
[:alice :salary 80000]])
;; Find all salary changes made by alice@example.com
(query [:find ?entity ?salary ?tx
:valid-at :any-valid-time
:where [?meta :tx/actor "alice@example.com"]
[?meta :db/tx-count ?tx]
[?entity :salary ?salary]
[?entity :db/tx-count ?tx]])
;; => [(:alice, 80000, N)] — all salary facts recorded in the same tx as the actor fact
Notes:
- Correlate the actor entity's
:db/tx-countwith data entities':db/tx-countto find all facts written in the same transaction - Use a naming convention for tx-meta entities (e.g.,
:tx-meta-<uuid>) to avoid collisions - Actor facts are themselves bitemporal — query them with
:as-ofto reconstruct who was responsible for a change at a past tx
← Graph Traversal | Bitemporal Modeling →
Reference: Bi-temporal queries, Pseudo-attributes