Time

Tests…

Datomic has powerful ways of accessing the immutable data that accumulates over time in the database.

Molecule maps those to 5 data getters that we can illustrate with a time line of transactions.

The 5 ways of getting data have the following semantics:

  • get - snapshot of tx 1-7 accumulated. This is the current view of the database (“as of now”) that we normally use.
  • getAsOf(t4) - snapshot of tx 1-4 accumulated. How the db looked after tx 4 was transacted.
  • getSince(t4) - snapshot of tx 5-7 accumulated. What has happened since tx 4 until now.
  • getHistory - all transactions! See all that has happened over time.
  • getWith(tx8data) - snapshot of tx 1-8 accumulated given some tx 8 data. “What if”-look into the future.

Point in time

The two methods getAsOf(t) and getSince(t) takes a point in time in the database that can be supplied in 4 different ways:

1. tx Transaction entity id

A transaction entity id is the 4th value of the Datomic quintuplets that tells us in what transaction this Datom/fact was asserted/retracted.

In Molecule code we can get this information by adding the generic tx attribute after an attribute:

Person(e5).likes.tx.get.head === ("pizza", tx4)

Here we get some transaction entity id tx4 (a Long number) for the transaction where it was asserted that Person entity fredId likes pizza.

Such transaction entity id can then be used as a point in time t for getAsOf(t) or getSince(t) in other queries.

2. t Transaction value

An alternative to the transaction entity id is a “transaction value” that is an auto-incremented number that Datomic generates automatically in the background for each transaction taking place. This can be useful if we for instance want to examine “the previous” transaction.

As when getting the transaction entity id with tx we can get the transaction value by appending the generic Molecule attribute t after some attribute:

val someT = Person(e5).likes_.t.get.head

Then we could ask “was there another value in the previous transaction?”

val previousT = someT - 1
Person(e5).likes.getAsOf(previousT) === Nil // There were no `likes` value before...

Transaction values can be converted to transaction entity ids and vice versa if needed with the Datomic Peer methods toTx and toT

// t -> tx
datomic.Peer.toTx(t1) === tx1

// tx -> t
datomic.Peer.toT(tx2) === t2

3. Transaction report

Each transaction returns a TxReport with information about the transaction and we can use the report itself as a point in time:

val txReport1 = Person.name("Fred").likes("pizza").save
val fred = txReport1.eid // Getting created entity id from tx report

val txReport2 = Person(fred).likes("sushi").update

Person(fred).likes.get === List("sushi")
Person(fred).likes.getAsOf(txReport1) === List("pizza")

4. java.util.Date

Lastly we can also supply a human time/date of type java.util.Date

val criticalDate = new Date("2017-04-26")
Person(e5).likes.getAsOf(criticalDate) === List("pizza")

Data getters

get current view

Normally we get the current state of the database with the get method on a molecule.

Person.name.age.get === ... // Persons as of now

But we might be interested in how the data looked at another point in time:

getAsOf(t)

When we call getAsOf(t) on a molecule we get the data as it looked at some point in time t.

We could for instance want to know what Persons existed in the database the 5th of November:

Person.name.age.getAsOf(nov5date) === ... // Persons as of November 5 (inclusive) 

getSince(t)

Likewise we might want to know what Persons have been added after or since 5th of November. Whn we call getSince(nov5date) we will get a snapshot of the current database filtered with only the data added/retracted after November 5:

Person.name.age.getSince(nov5date) === ... // Persons added after November 5

getHistory

The getHistory can for instance tell us how a Persons age attribute value has changed over time

Person(fredId).age.getHistory === ... // Current and previous ages of Fred

Note that this is not a snapshot in time but a series of all assertions and retractions over time that matches the query!

getWith(testTxData)

By supplying some test transaction data to getWith(testTxData) we can get a “branch” of the current database with the test transaction data applied. This is a very powerful way of testing future-like “what-if” scenarios

Person.name.age.getWith(<testTxData>) === ... // Persons including some new data 

The “test db” that such query works on is simply garbage collected when it goes out of scope. We therefore don’t need to any tear-down as we would normally need to testing with a mutable database.

3 return types

Each data getter has 3 interfaces that return data in different ways:

Typed

The standard getters return Lists of tuples of type-casted data.

:List[<tuple>]

  • get
  • getAsOf(t)
  • getSince(t)
  • getHistory
  • getWith(txData)

Lazily typed

If large data sets are expected, an Iterable of tuples of lazily type-cased data can be retrieved instead. Data is type-casted on each call to next on the iterator.

:Iterable[<tuple>]

  • getIterable
  • getIterableAsOf(t)
  • getIterableSince(t)
  • getIterableHistory
  • getIterableWith(txData)

Untyped

If typed data is not required we can get the raw untyped java collections of Lists of objects.

:java.util.Collection[java.util.List[AnyRef]]

  • getRaw
  • getRawAsOf(t)
  • getRawSince(t)
  • getRawHistory
  • getRawWith(txData)

Next

AsOf / Since…