Liking cljdoc? Tell your friends :D

Time Variance

As a temporal database, Datahike tracks transaction time for every change, enabling auditing, analytics, and time-travel queries. Each transaction records :db/txInstant, allowing you to view data at different points in time.

Time Travel Views

ViewFunctionReturnsUse Case
Current@conn or (d/db conn)Latest stateNormal queries
As-of(d/as-of db date)State at specific time"What did the data look like on July 1st?"
History(d/history db)All versions (current + historical)Audit trails, change analysis
Since(d/since db date)Changes after specific time"What changed since yesterday?"

Related: For git-like branching and merging of database snapshots, see Versioning. For removing old historical data from storage, see Garbage Collection.

Disabling History

If you don't need time-travel queries, disable history tracking to save storage:

(require '[datahike.api :as d])

(d/create-database {:store {:backend :memory :id #uuid "550e8400-e29b-41d4-a716-446655440000"} :keep-history? false})

Trade-off: Saves storage and improves write performance, but removes as-of/history/since queries and purging capabilities.

Setup for Examples

All examples below use this shared setup:

(require '[datahike.api :as d])

;; Simple schema for person data
(def schema [{:db/ident :name
              :db/valueType :db.type/string
              :db/unique :db.unique/identity
              :db/index true
              :db/cardinality :db.cardinality/one}
             {:db/ident :age
              :db/valueType :db.type/long
              :db/cardinality :db.cardinality/one}])

(def cfg {:store {:backend :memory :id #uuid "550e8400-e29b-41d4-a716-446655440001"} :initial-tx schema})

(d/create-database cfg)
(def conn (d/connect cfg))

;; Query to find names and ages
(def query '[:find ?n ?a :where [?e :name ?n] [?e :age ?a]])

DB (Current State)

@conn or (d/db conn) returns the current state - the most common view for queries:


;; add first data
(d/transact conn {:tx-data [{:name "Alice" :age 25}]})

;; define simple query for name and age
(def query '[:find ?n ?a :where [?e :name ?n] [?e :age ?a]])

(d/q query @conn)
;; => #{["Alice" 25]}

;; update the entity
(d/transact conn {:tx-data [{:db/id [:name "Alice"] :age 30}]})

;; `db` reflects the latest state of the database
(d/q query @conn)
;; => #{["Alice" 30]}

As-Of

You can query the database at a specific point in time using as-of:

(require '[datahike.api :as d])

;; define simple schema
(def schema [{:db/ident :name
              :db/valueType :db.type/string
              :db/unique :db.unique/identity
              :db/index true
              :db/cardinality :db.cardinality/one}
             {:db/ident :age
              :db/valueType :db.type/long
              :db/cardinality :db.cardinality/one}])

;; create our temporal database
(def cfg {:store {:backend :memory :id #uuid "550e8400-e29b-41d4-a716-446655440003"} :initial-tx schema})

(d/create-database cfg)

(def conn (d/connect cfg))


;; add first data
(d/transact conn {:tx-data [{:name "Alice" :age 25}]})

(def first-date (java.util.Date.))

;; define simple query for name and age
(def query '[:find ?n ?a :where [?e :name ?n] [?e :age ?a]])

(d/q query  @conn)
;; => #{["Alice" 25]}

;; update the entity
(d/transact conn {:tx-data [{:db/id [:name "Alice"] :age 30}]})

;; let's compare the current and the as-of value:
(d/q query  @conn)
;; => #{["Alice" 30]}

(d/q query (d/as-of @conn first-date))
;; => #{["Alice" 25]}

History

For querying all data over the whole time span you may use history which joins current and all historical data:

(require '[datahike.api :as d])

;; define simple schema
(def schema [{:db/ident :name
              :db/valueType :db.type/string
              :db/unique :db.unique/identity
              :db/index true
              :db/cardinality :db.cardinality/one}
             {:db/ident :age
              :db/valueType :db.type/long
              :db/cardinality :db.cardinality/one}])

;; create our temporal database
(def cfg {:store {:backend :memory :id #uuid "550e8400-e29b-41d4-a716-446655440004"} :initial-tx schema})

(d/create-database cfg)

(def conn (d/connect cfg))

;; add first data
(d/transact conn {:tx-data [{:name "Alice" :age 25}]})

;; define simple query for name and age
(def query '[:find ?n ?a :where [?e :name ?n] [?e :age ?a]])

;; history should have only one entry
(d/q query (d/history @conn))
;; => #{["Alice" 25]}

;; update the entity
(d/transact conn {:tx-data [{:db/id [:name "Alice"] :age 30}]})

;; both entries are present
(d/q query (d/history @conn))
;; => #{["Alice" 30] ["Alice" 25]}

Since

Changes since a specific point in time can be searched by using the since database:

(require '[datahike.api :as d])

;; define simple schema
(def schema [{:db/ident :name
              :db/valueType :db.type/string
              :db/unique :db.unique/identity
              :db/index true
              :db/cardinality :db.cardinality/one}
             {:db/ident :age
              :db/valueType :db.type/long
              :db/cardinality :db.cardinality/one}])

;; create our temporal database
(def cfg {:store {:backend :memory :id #uuid "550e8400-e29b-41d4-a716-446655440005"} :initial-tx schema})

(d/create-database cfg)

(def conn (d/connect cfg))


;; add first data
(d/transact conn {:tx-data [{:name "Alice" :age 25}]})

(def first-date (java.util.Date.))

;; define simple query for name and age
(def query '[:find ?n ?a :where [?e :name ?n] [?e :age ?a]])

(d/q query @conn)
;; => #{["Alice" 25]}

;; update the entity
(d/transact conn {:tx-data [{:db/id [:name "Alice"] :age 30}]})

;; let's compare the current and the as-of value:
(d/q query @conn)
;; => #{["Alice" 30]}

;; now we want to know any additions after a specific time
(d/q query (d/since @conn first-date))
;; => {}, because :name was transacted before the first date

;; let's build a query where we use the latest db to find the name and the since db to find out who's age changed
(d/q '[:find ?n ?a
       :in $ $since
       :where
       [$ ?e :name ?n]
       [$since ?e :age ?a]]
     @conn
     (d/since @conn first-date))

Meta Entity

With each transaction a meta entity is added to the index that stores the current point in time in the :db/txInstant attribute.

With this data present in the current index, you can search and analyze them for your purposes.

(require '[datahike.api :as d])

;; define simple schema
(def schema [{:db/ident :name
              :db/valueType :db.type/string
              :db/unique :db.unique/identity
              :db/index true
              :db/cardinality :db.cardinality/one}
             {:db/ident :age
              :db/valueType :db.type/long
              :db/cardinality :db.cardinality/one}])

;; create our temporal database
(def cfg {:store {:backend :memory :id #uuid "550e8400-e29b-41d4-a716-446655440006"} :initial-tx schema})

(d/create-database cfg)

(def conn (d/connect cfg))

;; add first data
(d/transact conn {:tx-data [{:name "Alice" :age 25}]})

;; let's find all transaction dates, should be two: one for the schema and one
;; for the first data
(d/q '[:find ?t :where [_ :db/txInstant ?t]] @conn)
;; => #{[#inst "2019-08-16T11:40:28.794-00:00"] [#inst "2019-08-16T11:40:26.587-00:00"]}

;; you might join over the tx id to get the date of any transaction
(d/q '[:find ?n ?t :where [_ :name ?n ?tx] [?tx :db/txInstant ?t]] @conn)
;; => #{["Alice" #inst "2019-08-16T11:40:28.794-00:00"]}

Data Purging

Retraction vs Purging: Normal retractions preserve data in history. Purging permanently deletes data from both current and historical indices.

When to purge: Privacy regulations (GDPR, HIPAA, CCPA) requiring "right to deletion", sensitive data removal, or retention policy enforcement.

⚠️ Warning: Purging is permanent and irreversible. Use only when legally required or explicitly needed.

Purge Operations

  • :db/purge - Remove specific datom (entity, attribute, value)
  • :db.purge/attribute - Remove all values for an attribute on an entity
  • :db.purge/entity - Remove entire entity and all its attributes
  • :db.history.purge/before - Remove all historical data before a date (retention policy cleanup)
(require '[datahike.api :as d])

;; define simple schema
(def schema [{:db/ident :name
              :db/valueType :db.type/string
              :db/unique :db.unique/identity
              :db/index true
              :db/cardinality :db.cardinality/one}
             {:db/ident :age
              :db/valueType :db.type/long
              :db/cardinality :db.cardinality/one}])

;; create our temporal database
(def cfg {:store {:backend :memory :id #uuid "550e8400-e29b-41d4-a716-446655440007"} :initial-tx schema})

(d/create-database cfg)

(def conn (d/connect cfg))


;; add data
(d/transact conn {:tx-data [{:name "Alice" :age 25}]})

;; define simple query for name and age
(def query '[:find ?n ?a :where [?e :name ?n] [?e :age ?a]])

(d/q query  @conn)
;; => #{["Alice" 25]}

(d/transact conn {:tx-data [[:db.purge/entity [:name "Alice"]]]})

;; data was removed from current database view
(d/q query  @conn)
;; => #{}

;; data was also removed from history
(d/q query (d/history @conn))
;; => #{}

Requirements for purging:

  • History must be enabled (:keep-history? true)
  • Cannot purge schema attributes
  • Use retractions for normal data lifecycle - reserve purging for compliance requirements

Purge and storage

The example above shows the new commit's indices: the datom is gone from @conn and from (d/history @conn). Layers below the new commit need explicit attention:

  • Pre-purge commit. Each commit owns its own index roots. The commit that existed before the purge still references the old tree nodes containing the datom — (d/commit-as-db conn <pre-purge-uuid>) still sees the purged data until garbage collection sweeps that intermediate commit. The eviction step is d/gc-storage with a grace-period cutoff old enough to drop the pre-purge commit; see Garbage Collection. Branch heads are always kept; only intermediate commits get swept.

  • Secondary indices. Purge routes a retraction event in-transaction to every secondary index covering an affected attribute, the same way :db/retract does. After purge, Scriptum full-text search, Proximum KNN, and Stratum columnar aggregates no longer return the purged datom on the live store. Scriptum has a filesystem caveat (Lucene tombstones-until-segment-merge); see Secondary indices: purge propagation.

  • Multiple branches. Purge is a transaction on one branch. If the same datom is reachable from another branch's head, or from its commits inside the GC window, purge that branch too. Structural sharing means the bytes are physically one copy in konserve, but the paths to reach them are independent.

  • Backups and storage-layer history. purge operates on the live store. Backups of the konserve store, S3 object versioning, ZFS snapshots, git-backed konserve backends, and logical replicas all retain pre-purge state on their own terms; addressing those is operational policy outside Datahike.

Can you improve this documentation? These fine people already did:
Konrad Kühne, Christian Weilbach, Dmitry Podgorniy & Judith
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close