Here, we’ll walk through how Molecule provides access to Datomic’s various generic interfaces and apis.
As we saw in building blocks, an entity can have attributes from multiple namespaces
Entity id | Attribue | Value |
---|---|---|
johnId | :Person/name | “John” |
johnId | :Person/likes | “pizza” |
johnId | :Person/age | 24 |
johnId | :Site/cat | “customer” |
At runtime we can explore what attributes an entity has by calling touch
directly on a Long
entity id:
johnId.touch === Map(
":db/id" -> johnId,
":Person/name" -> "John",
":Person/likes" -> "pizza",
":Person/age" -> 24,
":Site/cat" -> "customer"
)
The touch
method recursively retrieves referenced entities.
So if John had references, we migh want to apply a max depth level with touch(<maxLevel>)
:
johnId.touchMax(2) === Map(
":db/id" -> johnId,
":Person/name" -> "John"
":Person/friends" -> List(
Map(
":db/id" -> lisaId,
":Person/name" -> "Lisa"
":Person/friends" -> List(
Map(
":db/id" -> monaId,
":Person/name" -> "Mona"
":Person/friends" -> Set(ids...) // Mona's friends (3 levels deep) only as ids - not attribute maps
),
Map(...) // + more friends of Lisa (2 levels deep)
)
),
Map(...) // + more friends of John (1 level deep)
)
)
If we want to sort the key-value pairs we can also ask for a List with sorted pairs.
johnId.touchList === List(
":db/id" -> johnId,
":Person/age" -> 24,
":Person/likes" -> "pizza",
":Person/name" -> "John",
":Site/cat" -> "customer"
)
The entity id pair ":db/id" -> johnId
is present for all touch calls and will always be first. The remaining pairs are hereafter sorted by key. Nested Lists likewise.
When testing we also have a convenience method touchListQuoted
that returns output that we can paste into tests so that we can avoid having to quote keys and Strings etc. For our example this would look like this:
johnId.touchListQuoted ===
s"""List(
| ":db/id" -> 101L,
| ":Person/age" -> 24,
| ":Person/likes" -> "pizza",
| ":Person/name" -> "John",
| ":Site/cat" -> "customer")""".stripMargin
touch
, touchList
and touchQuoted
can all have a max-depth applied.
We can look for an optionally present attribute value. Here we ask the entity id johnId
if it has a :Site/cat
attribute value (of type String
):
johnId[String](":Site/cat") === Some("customer")
johnId[Int](":Person/age") === Some(24)
// Likewise, a non-present attribute returns None
johnId[String](":Site/member") === None
In the following sections, we’ll refer to the 5 basic components E-A-V-T-Op of a datom:
E | A | V | T | Op |
---|---|---|---|---|
johnId | :Person/likes | pizza | t3 | true |
The T component can be either a point in time t
in the database, a transaction entity id tx
or transaction time txInstant
.
In Molecule we use the following syntax for generic attributes to represent the various components of datoms:
Generic attr | Type | Component | Generic information |
---|---|---|---|
e |
Long |
E | Entity id |
a |
String |
A | Attribute |
v |
Any |
V | Value |
t |
Long |
T | Point in time in database |
tx |
Long |
T | Transaction entity id |
txInstant |
Date |
T | Transaction time |
op |
Boolean |
Op | Operation: assertion (true) / retraction (false) |
The Datom API in Molecule is a set of generic attributes, as described above, that can be mixed with your custom attributes to handle generic information.
Here are some examples:
Get the entity id with e
:
Person.e.name.get.head === (benId, "Ben")
Get transaction info with txInstant
, t
or tx
:
// When was Ben's age updated?
Person(benId).age.txInstant.get.head === (26, Date("April 4, 2019"))
// In which transaction entity was Ben's age updated?
Person(benId).age.tx.get.head === (26, tx2)
// What's the transaction value t where Ben's age was updated?
Person(benId).age.t.get.head === (26, t2)
With a history db we can access the point in time t
in the database and assertion/retraction statuses with op
// When was Ben's age updated back in time?
Person(benId).age.t.op.getHistory.sortBy(r => (r._2, r._3)) === List(
(25, t1, true), // age 25 asserted in transaction t1
(25, t2, false), // age 25 retracted in transaction t2
(26, t2, true) // age 26 asserted in transaction t2
)
Sometimes we will be interested in more generic data where we don’t know in advance what attributes will be involved. Then we can use the generic Datom attribute a
for Attribute name and v
for value. We could for instance ask what we know about an entity over time in the database:
// What do we know about the johnId entity?
Person(johnId).a.v.t.op.getHistory.sortBy(r => (r._2, r._3)) === List(
(":Person/name", "John", t3, true),
(":Person/likes", "pizza", t3, true),
(":Person/likes", "pizza", t6, false),
(":Person/likes", "pasta", t6, true)
)
By applying values to generic attributes we can filter search results:
// What was asserted/retracted in transaction tx3 about what John likes?
Person(johnId).likes.tx(tx6).op.getHistory.sortBy(r => (r._2, r._3)) === List(
("pizza", t6, false), // John no longer likes pizza
("pasta", t6, true) // John now likes pasta
)
Some index descriptions in the following sections are respectfully quoted from the Datomic documentation.
Datomic maintains four indexes that contain ordered sets of datoms. Each of these indexes is named based on the sort order used. E, A, and V are always sorted in ascending order, while T is always in descending order:
Molecule provides access to each index by instantiating a corresponding Index object with one or more arguments and then adding generic attributes matching the data to be returned.
Contrary to Datomic’s datoms API that returns Datoms, Molecule returns tuples of data matching the generic attributes added to the Index object. This way, Index molecules transparently share the same return type semantics as normal molecules.
The EAVT index provides efficient access to everything about a given entity. Conceptually this is very similar to row access style in a SQL database, except that entities can possess arbitrary attributes rather then being limited to a predefined set of columns.
// Create EAVT Index molecule with 1 entity id argument
EAVT(e1).e.a.v.t.get === List(
(e1, ":Person/name", "Ben", t1),
(e1, ":Person/age", 25, t2),
(e1, ":Golf/score", 5.7, t2)
)
// Maybe we are only interested in the attribute/value pairs:
EAVT(e1).a.v.get === List(
(":Person/name", "Ben"),
(":Person/age", 25),
(":Golf/score", 5.7)
)
// Two arguments to narrow the search
EAVT(e1, ":Person/age").a.v.get === List(
(":Person/age", 25)
)
The AVET index provides efficient access to particular combinations of attribute and value.
// Create AVET Index molecule with 1 entity id argument
AVET(":Person/age").v.e.t.get === List(
(25, e1, t2),
(23, e2, t5),
(14, e3, t7)
)
// Narrow search with multiple arguments
AVET(":Person/age", 25).e.t.get === List( (e1, t2) )
AVET(":Person/age", 25, e1).e.v.get === List( (e1, t2) )
AVET(":Person/age", 25, e1, t2).e.v.get === List( (e1, t2) )
The AVET Index can be filtered by a range of values between from
(inclusive) and until
(exclusive) for an attribute:
AVET.range(":Person/age", Some(14), Some(20)).v.e.t.get === List(
(14, e4, t7) // 14 is included in value range
// 23 not included in value range
// 25 outside value range
)
// If `from` is None, the range starts from the beginning
AVET.range(":Person/age", None, Some(40)).v.e.t.get === List(
(14, e3, t7),
(23, e2, t5)
)
// If `until` is None, the range goes to the end
AVET.range(":Person/age", Some(20), None).v.e.t.get === List(
(23, e2, t5),
(25, e1, t2)
)
The AEVT index provides efficient access to all values for a given attribute, comparable to traditional column access style.
// Create AEVT Index molecule with 1 entity id argument
AEVT(":Person/name").e.v.t.get === List(
(e1, "Ben", t2),
(e2, "Liz", t5)
)
// Narrow search with multiple arguments
AEVT(":Person/name", e1).e.v.get === List( (e1, "Ben") )
AEVT(":Person/name", e1, "Ben").e.v.get === List( (e1, "Ben") )
AEVT(":Person/name", e1, "Ben", t2).e.v.get === List( (e1, "Ben") )
The VAET index contains all and only datoms whose attribute has a :db/valueType of :db.type/ref. This is also known as the reverse index, since it allows efficient navigation of relationships in reverse.
// Say we have 3 entities pointing to one entity:
Release.e.name.Artists.e.name.get === List(
(r1, "Abbey Road", a1, "The Beatles"),
(r2, "Magical Mystery Tour", a1, "The Beatles"),
(r3, "Let it be", a1, "The Beatles")
)
// .. then we can get the reverse relationships with the VAET Index:
VAET(a1).v.a.e.get === List(
(a1, ":Release/artists", r1),
(a1, ":Release/artists", r2),
(a1, ":Release/artists", r3)
)
// Narrow search with multiple arguments
VAET(a1, ":Release/artist").e.get === List(r1, r2, r3)
VAET(a1, ":Release/artist", r2).e.get === List(r2)
VAET(a1, ":Release/artist", r2, t7).e.get === List(r2)
Some index descriptions in the following sections respectfully quoted from the Datomic documentation.
Datomic’s database log is a recording of all transaction data in historic order, organized for efficient access by transaction. The Log is therefore an efficient source of finding data by transaction time.
The Molecule Log implementation takes two arguments to define a range of transactions between from
(inclusive) and until
(exclusive). One or more generic attributes are then added to the Log molecule to define what data to return.
Contrary to Datomic’s Log implementation, Molecule returns data as a flat list of tuples of data that matches the generic attributes in the Log molecule. This is to transparently share the same return semantics as other molecules.
// Data from transaction t1 (inclusive) until t4 (exclusive)
Log(Some(t1), Some(t4)).t.e.a.v.op.get === List(
(t1, e1, ":Person/name", "Ben", true),
(t1, e1, ":Person/age", 25, true),
(t2, e2, ":Person/name", "Liz", true),
(t2, e2, ":Person/age", 23, true),
(t3, e1, ":Person/age", 25, false),
(t3, e1, ":Person/age", 26, true)
)
If the from
argument is None
data from the beginning of the log is matched:
Log(None, Some(t3)).v.e.t.get === List(
(t1, e1, ":Person/name", "Ben", true),
(t1, e1, ":Person/age", 25, true),
(t2, e2, ":Person/name", "Liz", true),
(t2, e2, ":Person/age", 23, true)
// t3 not included
)
If the until
argument is None
data from until the end of the log is matched:
Log(Some(t2), None).v.e.t.get === List(
// t1 not included
(t2, e2, ":Person/name", "Liz", true),
(t2, e2, ":Person/age", 23, true),
(t3, e1, ":Person/age", 25, false),
(t3, e1, ":Person/age", 26, true)
)
A Datomic database schema is saved as data which makes it possible to make queries about the structure of the schema.
Query your schema with Schema
and build on with schema attributes:
Attribute | Description | Example value |
---|---|---|
id |
Attribute definition entity id | Long |
a |
(Partition-)Namespace-prefixed attribute name | “:ind_Person/name” or “:Person/name” if no partitions |
part |
Partition name. | “ind” or "" if no partitions |
nsFull |
Namespace name with partition prefix | “ind_Person” or “Person” if no partitions |
ns |
Namespace name | “Person” |
attr |
Attribute name | “name” |
tpe |
Attribute Datomic type | See types below |
card |
Attribute cardinality | “one”/“many” |
doc |
Attribute documentation string | String |
index |
Attribute index status | true / not set |
unique |
Attribute uniqueness status | true / not set |
fulltext |
Attribute fulltext search status | true / not set |
isComponent |
Attribute isComponent status | true / not set |
noHistory |
Attribute noHistory status | true / not set |
enum |
Attribute enum values | String |
t |
Attribute definition transaction point in time | Long / Int |
tx |
Attribute definition transaction entity id | Long |
txInstant |
Attribute definition transaction wall-clock time | java.util.Date |
Since the schema is based on your Data Model, Schema queries are also a way to query your Data Model.
Datomic’s bootstrapped attributes are transparently filtered out so that only custom schema data based on your Data Model is returned.
Here are some examples:
Here we find various elements of attribute names:
Schema.a.part.ns.nsFull.attr.get === List (
(":sales_Customer/name", "sales", "Customer", "sales_Customer", "name"),
(":accounting_Invoice/invoiceLine", "accounting", "Invoice", "accounting_Invoice", "invoiceLine"),
// etc..
)
Datomic type and cardinality of attributes
Schema.a.tpe.card.get === List (
(":sales_Customer/name", "string", "one"),
(":accounting_Invoice/invoiceLine", "ref", "many")
)
Scala Int
and Long
are both represented as Datomic type long
:
Datomic type | Scala type |
---|---|
string | String |
long | Int |
long | Long |
float | Float |
double | Double |
bigint | BigInt |
bigdec | BigDecimal |
boolean | Boolean |
instant | java.util.Date |
uuid | java.util.UUID |
uri | java.net.URI |
ref | Long |
These can be retrieved as mandatory or optional attribute values:
Schema.a
.index
.doc$
.unique$
.fulltext$
.isComponent$
.noHistory$
.get === List(
(":sales_Customer/name",
true, // indexed
"Customer name", // doc
None, // Uniqueness not set
Some(true), // Fulltext search set so that we can search for names
None, // Not a component
None // History is preserved (noHistory not set)
),
(":accounting_Invoice/invoiceLine",
true, // indexed
"Ref to Invoice lines", // doc
None, // Uniqueness not set
None, // Fulltext search not set
Some(true), // Invoice is a component - owns invoice lines
None // History is preserved (noHistory not set)
),
)
Enumerated values can be defined in the schema and then retrieved generically with Schema.enum
:
// Defined enum values
Schema.a.enum.get.groupBy(_._1).map(g => g._1 -> g._2) === Map(
":Person/gender" -> List("female", "male"),
":Interests/sports" -> List("golf", "basket", "badminton")
)
“In what transaction/when were the attributes created in the schema?”
Schema.t.tx.txInstant.get === List(
(t1, tx1, <Date: 2018-11-07 09:28:10>), // Initial schema transaction
(t2, tx2, <Date: 2019-01-12 12:43:27>), // Additional schema attribute definitions...
)