Hi All,

I am pleased to announce the initial release of Tupelo Datomic, a library
of functions to make living and working with Datomic as effortless as
possible.

While the native Datomic API is very powerful, there are many details and
options which are often unnecessary and make code more cumbersome than
necessary.  Tupelo Datomic aims to focus on the simple and most common
use-cases, so that saving and retrieving data using Datomic is nearly
effortless.

The following samples are from the Tupelo Datomic documentation at:
https://github.com/cloojure/tupelo/blob/master/src/tupelo/datomic.adoc

--------------------------------------------------------------------------------------------------------------------

Suppose we’re trying to keep track of information for the world’s premiere
spy agency. Let’s create a few attributes that will apply to our heroes &
villains (see the executable code in the unit test
<https://github.com/cloojure/tupelo/blob/master/test/tst/tupelo/datomic_bond.clj>
).

  (:require [tupelo.datomic   :as td]
            [tupelo.schema    :as ts])

  ; Create some new attributes. Required args are the attribute name
(an optionally namespaced
  ; keyword) and the attribute type (full listing at
http://docs.datomic.com/schema.html). We wrap
  ; the new attribute definitions in a transaction and immediately
commit them into the DB.
  (td/transact *conn* ;   required              required
zero-or-more
                      ;  <attr name>         <attr value type>
<optional specs ...>
    (td/new-attribute   :person/name         :db.type/string
:db.unique/value)      ; each name      is unique
    (td/new-attribute   :person/secret-id    :db.type/long
:db.unique/value)      ; each secret-id is unique
    (td/new-attribute   :weapon/type         :db.type/ref
:db.cardinality/many)  ; one may have many weapons
    (td/new-attribute   :location            :db.type/string)     ;
all default values
    (td/new-attribute   :favorite-weapon     :db.type/keyword ))  ;
all default values

For the :weapon/type attribute, we want to use an enumerated type since
there are only a limited number of choices available to our antagonists:

  ; Create some "enum" values. These are degenerate entities that
serve the same purpose as an
  ; enumerated value in Java (these entities will never have any
attributes). Again, we
  ; wrap our new enum values in a transaction and commit them into the DB.
  (td/transact *conn*
    (td/new-enum :weapon/gun)
    (td/new-enum :weapon/knife)
    (td/new-enum :weapon/guile)
    (td/new-enum :weapon/wit))

Let’s create a few antagonists and load them into the DB. Note that we are
just using plain Clojure values and literals here, and we don’t have to
worry about any Datomic specific conversions.

  ; Create some antagonists and load them into the db.  We can specify
some of the attribute-value
  ; pairs at the time of creation, and add others later. Note that
whenever we are adding multiple
  ; values for an attribute in a single step (e.g. :weapon/type), we
must wrap all of the values
  ; in a set. Note that the set implies there can never be duplicate
weapons for any one person.
  ; As before, we immediately commit the new entities into the DB.
  (td/transact *conn*
    (td/new-entity { :person/name "James Bond" :location "London"
:weapon/type #{ :weapon/gun :weapon/wit   } } )
    (td/new-entity { :person/name "M"          :location "London"
:weapon/type #{ :weapon/gun :weapon/guile } } )
    (td/new-entity { :person/name "Dr No"      :location "Caribbean"
:weapon/type    :weapon/gun                 } ))

And, just like that, we have values persisted in the DB! Let’s check that
they are really there:

  ; Verify the antagonists were added to the DB
  (let [people (get-people (live-db)) ]
    (is (= people
           #{ {:person/name "James Bond"    :location "London"
:weapon/type #{:weapon/wit    :weapon/gun} }
              {:person/name "M"             :location "London"
:weapon/type #{:weapon/guile  :weapon/gun} }
              {:person/name "Dr No"         :location "Caribbean"
:weapon/type #{:weapon/gun               } } } )))

<https://github.com/cloojure/tupelo/blob/master/src/tupelo/datomic.adoc#entityspec-entityid-and-lookupref>EntitySpec,
EntityID, and LookupRef

Entities in Datomic are specified using an EntitySpec, which is either an
EntityID (EID) or a LookupRef.

An EntityID (EID) is a globally unique Long value that uniquely specifies
any entity in the Datomic DB. These are always positive for committed
entities in Datomic (negative values indicate temporary EIDs used only in
building transactions).

A LookupRef is an attribute-value pair (wrapped in a vector), which
uniquely specifies an entity. If an entity has an attribute specified as
either :db.unique/value or :db.unique/identity, that entity may be
specified using a LookupRef.

Here we verify that we can find James Bond and retrieve all of his attr-val
pairs using either type of EntitySpec:

  ; Using James' name, lookup his EntityId (EID).
  (let [james-eid   (td/query-scalar  :let    [$ (live-db)]     ; like
Clojure let
                                      :find   [?eid]
                                      :where  [ [?eid :person/name
"James Bond"] ] )
        ; Retrieve James' attr-val pairs as a map. An entity can be
referenced by either EID or LookupRef
        james-map   (td/entity-map (live-db) james-eid)
       ; use EID
        james-map2  (td/entity-map (live-db) [:person/name "James
Bond"] )    ; use LookupRef
  ]
    (is (= james-map {:person/name "James Bond" :location "London"
:weapon/type #{:weapon/wit :weapon/gun} } ))
    (is (= james-map james-map2 ))

We can also use either type of EntitySpec for update

    ; Update the database with more weapons.  If we overwrite some
items that are already present
    ; (e.g. :weapon/gun) it is idempotent (no duplicates are allowed).
The first arg to td/update
    ; is an EntitySpec (either EntityId or LookupRef) and determines
the Entity that is updated.
    (td/transact *conn*
      (td/update james-eid   ; update using EID
          { :weapon/type #{ :weapon/gun :weapon/knife }
            :person/secret-id 007 } )   ; Note that James has a
secret-id but no one else does

      (td/update [:person/name "Dr No"] ; update using LookupRef
        { :weapon/type #{ :weapon/gun :weapon/knife :weapon/guile } } )))

As expected, our database contains the updated values for Dr No and James
Bond. Notice that, since :weapon/type is implemented as a set in Datomic,
duplicate values are not allowed and both antagonists have only a single
gun:

  ; Verify current status. Notice there are no duplicate weapons.
  (let [people (get-people (live-db)) ]
    (is (= people
      #{ { :person/name "James Bond" :location "London" :weapon/type
#{:weapon/wit :weapon/knife :weapon/gun} :person/secret-id 7 }
         { :person/name "M" :location "London"          :weapon/type
#{:weapon/guile :weapon/gun} }
         { :person/name "Dr No" :location "Caribbean"   :weapon/type
#{:weapon/guile :weapon/knife :weapon/gun} } } )))

<https://github.com/cloojure/tupelo/blob/master/src/tupelo/datomic.adoc#enum-values>Enum
Values

The benefit of using enumerated values in Datomic is that we can easily
restrict the the domain of acceptable values more easily than by using
plain keyword values. For example, if we try to give James a non-existent
weapon, Datomic will generate an exception:

  ; Try to add non-existent weapon. This throws since the bogus kw
does not match up with an entity.
  (is (thrown? Exception   @(td/transact *conn*
                              (td/update [:person/name "James Bond"] ;
update using a LookupRef
                                { :weapon/type #{ :there.is/no-such-kw
} } ))))  ; bogus value for :weapon/type causes exception

<https://github.com/cloojure/tupelo/blob/master/src/tupelo/datomic.adoc#query-functions-in-tupelo-datomic>Query
Functions in Tupelo Datomic

When querying for values using Tupelo Datomic, the fundamental result type
is a TupleSet (a Clojure set containing unique Clojure vectors). This
overcomes the problem where native result type datomic.query.EntityMap is
lazy-loading and may give unexpected results. Here is an example of Tupelo
Datomic query in action:

  ; For general queries, use td/query.  It returns a set of tuples
(TupleSet).  Any duplicated
  ; tuples will be discarded
  (let [tuple-set   (td/query  :let    [$ (live-db)]
                               :find   [?name ?loc] ; <- shape of output tuples
                               :where  [ [?eid :person/name ?name]
 ; pattern-matching rules specify how the variables
                                         [?eid :location    ?loc ] ] )
 ;   must be related (implicit join)
  ]
    (is (s/validate  ts/TupleSet  tuple-set))       ; verify expected
type using Prismatic Schema
    (is (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 modifies the original Datomic query syntax compared to
(datomic.api/q
...) in two ways. For convenience, the query form does not need to be
wrapped in a map literal nor is any quoting required. Most importantly, the
:in keyword has been replaced with the :let keyword, and the syntax has
been copied from the Clojure letspecial form so that each query variables
are more closely aligned with their actual values. Also, the implicit DB $ must
be explicitly tied to its data source in all cases (as shown above).

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 functiontd/query-set, 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/query-set.  As usual, any duplicate values will
be discarded.
  (let [names     (td/query-set :let    [$ (live-db)]
                                :find   [?name] ; <- a single attr-val
output allows use of td/query-set
                                :where  [ [?eid :person/name ?name] ] )
        cities    (td/query-set :let    [$ (live-db)]
                                :find   [?loc]  ; <- a single attr-val
output allows use of td/query-set
                                :where  [ [?eid :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 result
tuple in a set and we can use the function td/query-tuple, which returns 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/query-tuple.  It is an error if more than one
tuple is found.
  (let [beachy    (td/query-tuple :let    [$    (live-db)     ; assign
multiple query variables
                                           ?loc "Caribbean"]  ; just
like clojure 'let' special form
                                  :find   [?eid ?name] ; <- output tuple shape
                                  :where  [ [?eid :person/name ?name      ]
                                            [?eid :location    ?loc] ] )
        busy      (try ; error - both James & M are in London
                    (td/query-tuple :let    [$ (live-db)
                                             ?loc "London"]
                                    :find   [?eid ?name] ; <- output tuple shape
                                    :where  [ [?eid :person/name ?name]
                                              [?eid :location    ?loc ] ] )
                    (catch Exception ex (.toString ex)))
  ]
    (is (matches? beachy [_ "Dr No"] ))           ; found 1 match as expected
    (is (re-seq #"IllegalStateException" busy)))  ; Exception
thrown/caught since 2 people in London

Of course, in some instances you may want only the value of only a single
attribute for a single entity. In this case, we may use the function
td/query-scalar, which returns a single scalar value 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/query-scalar. It is an error if more than one
tuple or value is present.
  (let [beachy    (td/query-scalar  :let    [$    (live-db)     ;
assign multiple query variables
                                             ?loc "Caribbean"]  ; just
like clojure 'let' special form
                                    :find   [?name]
                                    :where  [ [?eid :person/name ?name]
                                              [?eid :location    ?loc ] ] )
        busy      (try ; error - multiple results for London
                    (td/query-scalar  :let    [$    (live-db)
                                               ?loc "London"]

                                      :find   [?eid]
                                      :where  [ [?eid :person/name  ?name]
                                                [?eid :location     ?loc ] ] )
                    (catch Exception ex (.toString ex)))
        multi     (try ; error - tuple [?eid ?name] is not scalar
                    (td/query-scalar  :let    [$    (live-db)
                                               ?loc "Caribbean"]
                                      :find   [?eid ?name]
                                      :where  [ [?eid :person/name  ?name]
                                                [?eid :location     ?loc ] ] )
                    (catch Exception ex (.toString ex)))
  ]
    (is (= beachy "Dr No"))                       ; found 1 match as expected
    (is (re-seq #"IllegalStateException" busy))   ; Exception
thrown/caught since 2 people in London
    (is (re-seq #"IllegalStateException" multi))) ; Exception
thrown/caught since 2 people in London

<https://github.com/cloojure/tupelo/blob/master/src/tupelo/datomic.adoc#using-the-datomic-pull-api>Using
the Datomic Pull API

If one wishes to use queries returning possibly duplicate result items,
then the Datomic Pull api is required. A Pull query 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/query-pull and the Datomic
  ; Pull API to return a list of results (instead of a set).
  (let [result-pull     (td/query-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)
  ]
    (is (s/validate [ts/TupleMap] result-pull))    ; a list of tuples of maps
    (is (= result-sort  [ [ {:location "Caribbean"} ]
                          [ {:location "London"   } ]
                          [ {:location "London"   } ] ] )))

<https://github.com/cloojure/tupelo/blob/master/src/tupelo/datomic.adoc#using-datomic-partitions>Using
Datomic Partitions

Datomic allows the user to create *partitions* within the DB. Datomic
partitions serve solely as a structural optimization, and do not control or
limit how or by whom datoms may be accessed. The effect of a partition in
Datomic is to effectively "pre-sort" all entities in that partition so that
they are adjacent in storage, which *may* improve access times for related
entities that are often accessed together.

In Tupelo Datomic, we may easily create and use partitions:

  ; Create a partition named :people (we could namespace it like
:db.part/people if we wished)
  (td/transact *conn*
    (td/new-partition :people ))

  ; Create Honey Rider and add her to the :people partition
  (let [tx-result   @(td/transact *conn*
                        (td/new-entity :people ; <- partition is first
arg (optional) to td/new-entity
                          { :person/name "Honey Rider" :location
"Caribbean" :weapon/type #{:weapon/knife} } ))
        [honey-eid]  (td/eids tx-result)  ; retrieve Honey Rider's EID
from the seq (destructuring)
  ]
    (is (s/validate ts/Eid honey-eid))  ; verify the expected type
    (is (= :people ; verify the partition name for Honey's EID
           (td/partition-name (live-db) honey-eid))))

<https://github.com/cloojure/tupelo/blob/master/src/tupelo/datomic.adoc#future-work>Future
Work

Lots more to come!
<https://github.com/cloojure/tupelo/blob/master/src/tupelo/datomic.adoc#license>
License

Copyright © 2015 Alan Thompson.

Distributed under the Eclipse Public License, the same as Clojure.

<https://github.com/cloojure/tupelo/blob/master/src/tupelo/datomic.adoc#todo-list-todo>

-- 
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
--- 
You received this message because you are subscribed to the Google Groups 
"Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to clojure+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to