Skip to content

Instantly share code, notes, and snippets.

@stevebuik
Created June 25, 2017 07:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stevebuik/17ed50824f1bb814fab9e556a37cf18a to your computer and use it in GitHub Desktop.
Save stevebuik/17ed50824f1bb814fab9e556a37cf18a to your computer and use it in GitHub Desktop.
terse datomic schema vectors. based on ideas from this presentation at clj-syd https://boogler.gitbooks.io/data-macros/content/
(require '[clojure.pprint :refer [pprint]])
(require '[datomic.api :as d])
(require '[clojure.spec :as s])
(require '[clojure.spec.test :as st])
(s/def ::optional-attributes (s/* (s/alt :db/fulltext (s/and (partial = :fulltext)
; conformer should be last so that it returns the transformed value
(s/conformer (constantly true)))
:db/index (s/and (partial = :index)
(s/conformer (constantly true)))
:db/unique (s/and #{:value :identity}
(s/conformer {:value :db.unique/value
:identity :db.unique/identity}))
:db/doc string?)))
(s/def ::attribute (s/cat :db/ident keyword?
:db/cardinality (s/and #{:one :many}
(s/conformer (fn [card]
(keyword "db.cardinality" (name card)))))
:db/valueType (s/conformer (fn [type]
(keyword "db.type" (name type))))
:optional ::optional-attributes))
(s/def ::enum (s/cat :db/ident keyword?))
(s/def ::schema (s/or :attribute ::attribute
:enum ::enum))
(defn expand
"converts a terse Datomic attribute vector into a full schema map required for a transaction"
[terse]
(if (s/valid? ::schema terse)
(let [[type conformed] (s/conform ::schema terse)]
(case type
:attribute (let [sorter (sorted-map-by (fn [k1 k2]
(let [order {:db/id 1
:db/ident 2
:db/valueType 3
:db/cardinality 4
:db/index 5
:db/fulltext 6
:db/unique 7
:db/doc 8}
c (compare (get order k1 100) (get order k2 100))]
(if (zero? c) 1 c))))
optional (into {} (:optional conformed))]
(-> conformed
(assoc :db/id (d/tempid :db.part/db)
:db.install/_attribute :db.part/db)
(dissoc :optional)
(merge optional)
(->> (into sorter))))
:enum conformed))
(throw (ex-info "incorrect schema" {:explanation (s/explain-data ::schema terse)}))))
(s/fdef expand :args (s/cat :terse ::schema))
(st/instrument)
(let [uri "datomic:mem://terse-schema"
_ (d/create-database uri)
conn (d/connect uri)
; the schema below is the point of this gist. This vector is compact and easier to read than maps.
terse-schema [[:user/first-name :one :string :fulltext :index "A users first name"]
[:user/status :one :ref :index]
[:user/ACTIVE] [:user/BLOCKED]]
verbose-schema (map expand terse-schema)
verbose-data [{:db/id (d/tempid :db.part/user)
:user/first-name "Sam"
:user/status [:db/ident :user/ACTIVE]}]]
@(d/transact conn verbose-schema)
@(d/transact conn verbose-data)
(let [db (d/db conn)]
(pprint (d/q '[:find (pull ?u [:user/first-name {:user/status [:db/ident]}])
:in $
:where
[?u :user/first-name _]]
db)))
(d/delete-database uri))
@stevebuik
Copy link
Author

This syntax is similar to the schema DSL in Vase but uses schema/conform instead of reader macros.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment