Time Travel
Datomic never overwrites anything. When a fact changes, it adds the new fact and records that the old one was retracted. Every fact is stamped with the transaction that added or retracted it, and a transaction is an entity like any other. (Two exceptions: Datomic Pro supports excision, which permanently erases specific values, and an attribute marked :db/noHistory keeps only its current value.)
Because nothing is lost, the database is a value that you can ask about any earlier point. That is what makes Datomic's time travel possible, and it takes only a few lines of Datalog.
The games changed these 151 Pokemon over the years, so our database was loaded the way it grew: one transaction for each game generation, holding what that generation changed. Generations III to V didn't change any of them. Their transactions only record that the generation happened.
Transactions are entities
Each of our transactions says which generation it records, in :tx/generation, and when those games were first released, in :tx/released. Datomic also stamps every transaction with :db/txInstant, the moment it recorded it. That is when this server loaded the data, not 1996, because :db/txInstant is transaction time: when Datomic learned a fact, not when it was true in the world. The real-world release date is valid time, and Datomic has no built-in attribute for it, so we kept it ourselves, as an attribute of the transaction:
as-of
as-of is the database as it was after a transaction. In the editors on this page, the inputs after the query can be views of the database, like (as-of "Generation V"). Clefairy's type has changed, so let's look at it as it was in Generation V. The query declares the view with :in $ $then: $ is the database as it is now, $then is the view, and each clause says which one it reads from by starting with it:
(as-of "Generation V") is a shorthand this site provides so you don't have to look up a transaction id by hand. The real call is (d/as-of db t), where t is a transaction entity id, a basis-t number, or an instant. Written out, with the same lookup this site does for you:
And today, straight from the current database:
Two databases in one query
A query can read both. Which Pokemon have more attack today than they had in Generation V? Notice that the same ?e is used with both, since an entity keeps its id through time:
not works across databases too. Which Pokemon have a type they didn't have in Generation V?
history
(history) is a view with everything that ever happened, including the facts that were retracted. Its patterns have two more positions: the transaction, and whether the fact was added (true) or retracted (false). Here is every change to Clefairy's type, with the generation of the transaction that made it:
Normal was added in Generation I. In Generation VI it was retracted and Fairy was added. Datomic refuses any pattern that would scan every fact in the database, in history or otherwise. Binding the entity, like we did above, is enough; only the attribute stays free.
That Generation VI change is two facts in one transaction, a retraction and an assertion on the same entity and attribute. Nothing is overwritten in place; the old value leaves explicitly, and the new one is added as its own fact:
Remove the entity binding from the history query above, leaving [$history ?e ?a ?v ?tx ?added] on its own with nothing bound. Datomic refuses to run it. That's the full-scan protection in action.
since
as-of looks at everything up to a point. since looks only at what came after it: a view with only the facts added after a transaction, not the rest of the database. Which attack values changed after Generation VI? The Pokemon's name didn't change, so we read it from the current database:
A retired attribute
In Generation I there was a single Special stat, stored in :stat/special. Generation II split it into :stat/sp-attack and :stat/sp-defense. Datomic's schema only grows: an attribute, once created, can't be deleted, only stopped from being used. So :stat/special is still in the schema, but no Pokemon has it any more. Both eras of Bulbasaur fit in one row:
Types come and go
The type list has grown since. Steel and Dark arrived in Generation II, and Fairy in Generation VI. We recorded when each type appeared, not every later change to how types fare against each other, so the type chart you see for a past generation is today's, restricted to the types that existed then. How many types were there in Generation I?
Change the generation in the last query to "Generation II" and then "Generation VI" to watch Steel, Dark and Fairy arrive. Or change :stat/attack to :stat/defense in the two-database query to see who got more defense.
- Query the database as it looked at any past transaction with
as-of, or only what changed after one withsince. - Read the full history of an attribute, including retractions, with
history. - Tell transaction time (
:db/txInstant) apart from valid time, and explain why Datomic's schema only grows.