Time travel

A Datomic database is an accumulation of facts, and a database value is immutable. Every transaction is itself an entity, with a :db/txInstant recording when it happened. That makes it possible to look at the database as it was at any point, or at only what changed since then.

Finding transaction times

(def tx-instants (reverse (sort (d/q '[:find [?when ...] :where [_ :db/txInstant ?when]]
                                     (d/db conn)))))
(show "transaction instants" (count tx-instants))

(def data-tx-date (first tx-instants))
(def schema-tx-date (second tx-instants))
transaction instants: 3

There are three transactions: the bootstrap transaction that every database starts with, the schema, and the data. Sorted newest first, the first instant is the data transaction and the second is the schema transaction.

The rest of this chapter runs one query against different views of the database:

(def communities-query '[:find [?c ...] :where [?c :community/name]])

as-of: the database at a point in time

d/as-of returns the database as it was at a time, which can be given as a t, a transaction id or an instant:

(let [db-asof-schema (-> conn d/db (d/as-of schema-tx-date))]
  (count (d/q communities-query db-asof-schema)))

(let [db-asof-data (-> conn d/db (d/as-of data-tx-date))]
  (count (d/q communities-query db-asof-data)))
as of schema: 0
as of data: 150

Right after the schema transaction there were no communities yet; right after the data transaction there were 150.

since: only what changed after a point

d/since returns a database that contains only the facts added after a time:

(let [db-since-data (-> conn d/db (d/since schema-tx-date))]
  (count (d/q communities-query db-since-data)))

(let [db-since-data (-> conn d/db (d/since data-tx-date))]
  (count (d/q communities-query db-since-data)))
since schema: 150
since data: 0

with: what if?

d/with applies a transaction to a database value without committing it. It returns the same kind of report as transact, and its :db-after is a database you can query. The connection is untouched:

(def new-data-tx (read-edn "examples/seattle/seattle-data1.edn"))

(let [db-if-new-data (-> conn d/db (d/with new-data-tx) :db-after)]
  (count (d/q communities-query db-if-new-data)))

(count (d/q communities-query (d/db conn)))
with new data: 258
current: 150

This is useful for trying a transaction out, validating it, or computing a speculative result.

Committing the new data

Now transact it for real:

@(d/transact conn new-data-tx)
(count (d/q communities-query (d/db conn)))

(let [db-since-data (-> conn d/db (d/since data-tx-date))]
  (count (d/q communities-query db-since-data)))
after new data: 258
since first data: 108

The database now has 258 communities, and since shows exactly the 108 that the new transaction added.

Note that seattle-data1.edn mentions neighborhoods and districts that already exist, such as "Beacon Hill". Because their names are unique identities, those tempids upsert onto the existing entities instead of creating duplicates.

More time functions

nitomic also supports d/history (a database of every assertion and retraction ever made), d/as-of-t, d/since-t, d/basis-t, d/next-t, d/is-history, d/filter and d/is-filtered, and the transaction log via d/log and d/tx-range.