When searching for values with Tupelo Datomic, the fundamental result type is a
TupleSet (a Clojure set containing unique Clojure vectors). This overcomes a
possible problem with the native Datomic return type of datomic.query.EntityMap,
which is lazy-loading and may appear to be missing data (unless forced). Here
is an example of the Tupelo Datomic find
function in action:
; For general queries, use td/find. It returns a set of tuples (a TupleSet). Duplicate
; tuples in the result will be discarded.
(let [tuple-set (td/find :let [$ (live-db)]
:find [?name ?loc] ; <- shape of output tuples
:where {:person/name ?name :location ?loc} ) ; <- Clojure map encodes query
]
(s/validate ts/TupleSet tuple-set) ; verify expected type using Prismatic Schema
(s/validate #{ [s/Any] } tuple-set) ; literal definition of TupleSet
(is (= tuple-set #{ ["Dr No" "Caribbean"] ; Even though London is repeated, each tuple is
["James Bond" "London"] ; still unique. Otherwise, any duplicate tuples
["M" "London"] } ))) ; will be discarded since output is a clojure set.
Tupelo Datomic uses the find
function (& variants) for retrieving values from the database. The
find
function modifies the original Datomic query syntax of (datomic.api/q …)
in three ways.
-
For convenience, the Tupelo Datomic find
form does not need to be wrapped in a map literal nor
is any quoting required.
-
To clarify the relationship between program symbols and query arguments, the :in
keyword has been replaced with the :let
keyword, and
the syntax has been copied from the Clojure let
special form. In this way, each of
the query variables is more closely aligned with its actual value. Also, the
implicit DB $
must be explicitly tied to its data source in all cases (as
shown above).
-
Most importantly, the Datalog-inspired query syntax has been simplified with an equivalent syntax
based on plain Clojure maps.
The above query matches any entity that has both a :person/name
and a :location
attribute. For
each matching entity, the two values corresponding to :person/name
and :location
will be bound
to the ?name
and ?loc
symbols, respectively, which are used to generate an output tuple of the
shape [?name ?loc]
. Each output tuple is added to the result set, which is returned to the caller.
Since the returned value is a normal Clojure set, duplicate elements are not allowed and any
non-unique values will be discarded.
What if you want to query for a person with two types of weapons? Suppose you wish to find all of
the operatives with both :weapon/guile
and :weapon/gun
attributes (or more)? Then just list
both desired traits, and Datomic will perform in implicit join operation in the query (logical
"and"):
; Search for people that match both {:weapon/type :weapon/guile} and {:weapon/type :weapon/gun}
(let [tuple-set (td/find :let [$ (live-db)]
:find [?name]
:where {:person/name ?name :weapon/type :weapon/guile }
{:person/name ?name :weapon/type :weapon/gun } ) ]
(is (= #{["Dr No"] ["M"]} tuple-set )))
Receiving a TupleSet result is the most general case, but in many instances we
can save some effort. If we are retrieving the value for a single attribute per
entity, we don’t need to wrap that result in a tuple. In this case, we can use
the function td/find-attr
, which returns a set of scalars as output rather
than a set of tuples of scalars:
; If you want just a single attribute as output, you can get a set of values (rather than a set of
; tuples) using td/find-attr. As usual, any duplicate values will be discarded.
(let [names (td/find-attr :let [$ (live-db)]
:find [?name] ; <- a single attr-val output allows use of td/find-attr
:where {:person/name ?name} )
cities (td/find-attr :let [$ (live-db)]
:find [?loc] ; <- a single attr-val output allows use of td/find-attr
:where {:location ?loc} )
]
(is (= names #{"Dr No" "James Bond" "M"} )) ; all names are present, since unique
(is (= cities #{"Caribbean" "London"} ))) ; duplicate "London" discarded
A parallel case is when we want results for just a single entity, but multiple values are needed.
In this case, we don’t need to wrap the resulting tuple in a set and we can use the function
td/find-entity
, which returns just a single tuple as output rather than a set of tuples:
; If you want just a single tuple as output, you can get it (rather than a set of
; tuples) using td/find-entity. It is an error if more than one tuple is found.
(let [beachy (td/find-entity :let [$ (live-db) ; assign multiple find variables
?loc "Caribbean"] ; just like clojure 'let' special form
:find [?eid ?name] ; <- output tuple shape
:where {:db/id ?eid :person/name ?name :location ?loc} )
busy (try ; error - both James & M are in London
(td/find-entity :let [$ (live-db)
?loc "London"]
:find [?eid ?name] ; <- output tuple shape
:where {:db/id ?eid :person/name ?name :location ?loc} )
(catch Exception ex (.toString ex)))
]
(is (matches? [_ "Dr No"] beachy )) ; found 1 match as expected
(is (re-find #"Exception" busy))) ; Exception thrown/caught since 2 people in London
Note that, in the first the call to find-entity
, the symbol ?loc
is bound to the string
"Caribbean", while the symbols ?eid
and ?name
are left free. This means the query map in the
:where
clause will match any entity that posseses all three attributes :db/id
, :location
, and
:person/name
(note that every entity has the :db/id
attribute by definition). In addition, only
entities whose :location
attribute has the value "Caribbean" will be selected. Once an entity is
selected, its values for the attributes :db/id
and :location
are bound to the symbols ?eid
and
?name
, respectively, and the output tuple [?eid ?name]
is added to the result set. Similar
processing happens for the second call to find-entity
when ?loc
is bound to the string "London".
Of course, in some instances you may only want the value of a single attribute for a single
entity. In this case, we may use the function td/find-value
, which returns a single scalar
result instead of a set of tuples of scalars:
; If you know there is (or should be) only a single scalar answer, you can get the scalar value as
; output using td/find-value. It is an error if more than one tuple or value is present.
(let [beachy (td/find-value :let [$ (live-db) ; Find the name of the
?loc "Caribbean"] ; only person in the Caribbean
:find [?name]
:where {:person/name ?name :location ?loc} )
busy (try ; error - multiple results for London
(td/find-value :let [$ (live-db)
?loc "London"]
:find [?eid]
:where {:db/id ?eid :person/name ?name :location ?loc} )
(catch Exception ex (.toString ex)))
multi (try ; error - result tuple [?eid ?name] is not scalar
(td/find-value :let [$ (live-db)
?loc "Caribbean"]
:find [?eid ?name]
:where {:db/id ?eid :person/name ?name :location ?loc} )
(catch Exception ex (.toString ex)))
]
(is (= beachy "Dr No")) ; found 1 match as expected
(is (re-find #"Exception" busy)) ; Exception thrown/caught since 2 people in London
(is (re-find #"Exception" multi))) ; Exception thrown/caught since 2-vector is not scalar
If one wishes to use queries returning possibly duplicate result items, then the Datomic Pull API is
required. Searching for data via find-pull
returns results in a List (a Clojure vector), rather
than a Set, so that duplicate result items are not discarded. As an example, let’s find the
location of all of our entities:
; If you wish to retain duplicate results on output, you must use td/find-pull and the Datomic
; Pull API to return a list of results (instead of a set).
(let [result-pull (td/find-pull :let [$ (live-db)] ; $ is the implicit db name
:find [ (pull ?eid [:location]) ] ; output :location for each ?eid found
:where [ [?eid :location] ] ) ; find any ?eid with a :location attr
result-sort (sort-by #(-> % first :location) result-pull)
]
(s/validate [ts/TupleMap] result-pull) ; a list of tuples of maps
(is (= result-sort [ [ {:location "Caribbean"} ]
[ {:location "London" } ]
[ {:location "London" } ] ] )))
Suppose James throws his knife at a villan. We need to remove it from the DB.
(td/transact *conn*
(td/retract-value james-eid :weapon/type :weapon/knife))
(is (= (td/entity-map (live-db) james-eid) ; lookup by EID
{:person/name "James Bond" :location "London" :weapon/type #{:weapon/wit :weapon/gun} :person/secret-id 7 } ))
Once James has defeated Dr No, we need to remove him (& everything he possesses) from the database.
; We see that Dr No is in the DB...
(let [tuple-set (td/find :let [$ (live-db)]
:find [?name ?loc] ; <- shape of output tuples
:where {:person/name ?name :location ?loc} ) ]
(is (= tuple-set #{ ["James Bond" "London"]
["M" "London"]
["Dr No" "Caribbean"]
["Honey Rider" "Caribbean"] } )))
; we do the retraction...
(td/transact *conn*
(td/retract-entity [:person/name "Dr No"] ))
; ...and now he's gone!
(let [tuple-set (td/find :let [$ (live-db)]
:find [?name ?loc]
:where {:person/name ?name :location ?loc} ) ]
(is (= tuple-set #{ ["James Bond" "London"]
["M" "London"]
["Honey Rider" "Caribbean"] } )))