This repository has been archived by the owner. It is now read-only.
Make the border between Clojure and Datomic a more convenient and safe place to live.
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


Write spectacular data definitions! Our goal is to make the border between Clojure and Datomic a more convenient and safe place to live. Browse the API or continue scrolling.

Define your Datomic schemas using spec-tacular's spec DSL and receive the following in return:

  • Representation of Datomic entities as maps that verify (upon creation and association) that entity attributes have the correct fields, and in turn, the correct types

  • Core Typed aliases for each spec

  • Specialized query language with a map-like syntax that allows queries to be expressed with domain-specific spec keywords instead of Datomic attribute-keywords. Entities returned from queries are lazily constructed and can be used in typed code without extra casts.

  • Simple transaction interface with Datomic, using create! as a constructor, and assoc! as an update function.

WARNING: spec-tacular is not maintained.

Quick Start

[spec-tacular "0.6.2-SNAPSHOT"] ; unstable
[spec-tacular "0.6.1"]


Creating Specs

(require '[spark.spec-tacular :as sp :refer [defspec defunion defenum]])
;; Sets up a House entity containing a mandantory color and optionally
;; a Mailbox. It may also link in any number of Occupants.
(defspec House
  (:link [occupants :is-many :Occupant])
  [mailbox :is-a :Mailbox]
  [color :is-a :Color :required])

(defenum Color   ;; Houses can only be green or orange..
  green, orange) ;; makes for interesting neighborhoods

(defspec Mailbox              ;; Hope you don't want to get your mail
  [has-mail? :is-a :boolean]) ;; cause mailboxes only know if they have mail

;; Specs can have docstrings
(defspec Chimney
  "Chimneys are super complicated and require documentation"
  (:link [house :is-a House]))
(doc Chimney) ;; Such words

;; Houses can be occupied by either People or Pets.
(defunion Occupant :Person :Pet)

;; Each Person has a name that serves as an identifying field
;; (implemented as Datomic's notion of identity), and an age.
(defspec Person
  [name :is-a :string :identity :unique]
  [age :is-a :long])

(defunion Pet :Dog :Cat :Porcupine)

(defspec Dog
  [fleas? :is-a :boolean])

;; Cats can contain links (passed by reference to the database) to all
;; the occupants of the house that they hate.  For their nefarious
;; plots, no doubt.
(defspec Cat
  [hates :is-many :Occupant :link])

(defspec Porcupine) ;; No fields, porcupines are boring

Creating Databases

(require '[spark.spec-tacular.schema :as schema])
;; Returns a schema with entries for each spec defined in my-ns
(schema/from-namespace *ns*)
;; => ({:db/id ....,
;;      :db/ident :house/occupants,
;;      :db/valueType :db.type/ref,
;;      :db/cardinality :db.cardinality/many,
;;      ....}
;;     ....)

;; Creates a database with the earlier schema installed.
;; Returns a connection to that database.
(schema/to-database! (schema/from-namespace *ns*))
;; => #<LocalConnection datomic.peer.LocalConnection@....>

Changing Databases

(require '[spark.spec-tacular.datomic :as sd])
;; Use the House schema to create a database and connection
(def conn-ctx {:conn (schema/to-database! (schema/from-namespace *ns*))})

;; Create a green house:
(def h (sd/create! conn-ctx (house {:color :Color/green})))

;; Some quick semantics:
(:color h)                                    ;; => :Color/green
(= h (house {:color :Color/green}))           ;; => false
(sp/refless= h (house {:color :Color/green})) ;; => true
(assoc h :random-kw 42)                       ;; => error
(set [h h])                                   ;; => #{h}
(set [h (house {:color :Color/green})])       ;; => #{h (house {:color :Color/green})}

;; Let some people move in:
(def joe     (sd/create! conn-ctx (person {:name "Joe" :age 32})))
(def bernard (sd/create! conn-ctx (person {:name "Bernard" :age 25})))

(def new-h (sd/assoc! conn-ctx h :occupants [joe bernard]))
;; => assoc! returns a new House with the new field

h ;; => is still the simple green house
(sd/refresh conn-ctx h) ;; => new-h
;; In most cases, you can forego the `refresh` and just use the return
;; value of `assoc!`

;; Bernard and Joe get a cat, who hates both of them,
(def zuzu (sd/create! conn-ctx (cat {:hates (:occupants new-h)})))
(sd/assoc! conn-ctx h :occupants (conj (:occupants new-h) zuzu))

;; They build a mailbox, and try to put it up in another House:
(let [mb (mailbox {:has-mail? false})
      h1 (sd/assoc! conn-ctx h :mailbox mb)
      h2 (sd/create! conn-ctx (house {:color :Color/orange :mailbox mb}))]
  ;; But since Mailboxes are passed by value,
  ;; the Mailbox get duplicated
  (= (:mailbox h1) (:mailbox h2)) ;; => false

Querying Databases

(require '[spark.spec-tacular.datomic :as sd])
;; First let's distinguish the mailboxes -- let's say Joe and Bernard
;; get some mail
(def mb1 (sd/assoc! conn-ctx (:mailbox h1) :has-mail? true))

;; Get the database
(def db (sd/db conn-ctx))

;; Use % to look for the only find variable
(sd/q :find [:Mailbox ...] :in db :where [% {:has-mail? false}])
;; => #{(:mailbox h2)}, the mailbox from house h2
(sd/q :find [:Mailbox ...] :in db :where [% {:has-mail? true}])
;; => #{mb1}, that's Joe and Bernard's mailbox

;; Find the Houses without mail
(sd/q :find [:House ...] :in db :where
      [% {:mailbox {:has-mail false}}])
;; => #{h2}

;; Find the House and it's human occupants when the mailbox has mail
;; Use %1 and %2 to to look for multiple find variables
(sd/q :find :House :Person :in db :where
      [%1 {:occupants %2 :mailbox {:has-mail true}}])
;; => #{[h1 joe] [h2 bernard]}

This last example means we're looking for any :occupants that are :Persons. Even though we represent Datomic's cardinality "many" as a collection in Clojure, we still use a relation to search for members of that collection on the database. Those familiar with Datomic may understand that this part of the query (roughly) expands to

[.... [?house :house/occupants ?person] ....]

When we get the result of the query back in Clojure, we take that result and return it as a set. Onwards!

;; If you want to get the spec name of entities on the database, you
;; can use the special :spec-tacular/spec keyword.  Here we restrict
;; the occupants to the :Pet spec and then return all kinds of Pet's
;; that live in houses:
(sd/q :find [:string ...] :in db :where
      [:House {:occupants [:Person {:name %}]}])
;; => #{"Joe" "Bernard"}

Although maps work as you would expect in a query, the vector form [<spec> <map>] is protected syntax meaning the map should be restricted to things of type <spec>.

Updating from v.0.4.x to v0.5.0

  • Replace all spark.sparkspec namespaces with spark.spec-tacular
  • Check all calls to = to see if refless= is more appropriate
  • Check all sets if you mix local instances and instances on the database; these are nolonger = nor do they hash to the same number even if they are otherwise equivalent.
  • Rename defenum to defunion

Updating from v.0.5.x to v0.6.0

  • :is-many fields are now represented as clojure.lang.PersistentHashSets
  • spark.spec-tacular.restify was removed, it may come back eventually but in the meantime, if you need web serialization we accept pull requests
  • some queries that dynamically pull out :spec-tacular/specs are no longer supported, use pull or sd/query instead of sd/q

Short Term Roadmap

  • Create defattr that can be used as a field type to allow shared Datomic namespaces between fields of different specs


Copyright © 2014-2015 Spark Community Investment

Distributed under the Apache License Version 2.0