Giter Site home page Giter Site logo

neverfox / datomic-schematode Goto Github PK

View Code? Open in Web Editor NEW

This project forked from moquist/datomic-schematode

0.0 3.0 0.0 470 KB

Express datomic schema and constraints concisely. Datomic Schematode transforms it into transactable datomic schema.

License: Eclipse Public License 1.0

Clojure 100.00%

datomic-schematode's Introduction

datomic-schematode Build Status

Note: Still pre-release.

CelegansGoldsteinLabUNC

They set a schematode on Norville at lunch, in the new deli. It
was sighted by laser, and fell through an einrosen-door from
across the street into the mauve threads of his tweed jacket as he
read the menu. Ascending his torso, it left a microscopic trail of
its gastropod-derived polymer network gel among the weave. It
crouched on his shoulder.

“Focaccia reubens and hold the horsey,” Norville told Benny, the deli man.

“Actually,” the schematode piped up, filling the room with its shrilly
precise and insistent voice, “he wants a sandwich without horseradish sauce
but with corned beef on an herb-topped flatbread, where corned-beef is
a salt-cured beef product, and the flatbread is…”

Because Norville had good taste, he was in a good deli. Because he was in a
good deli, and because the deli happened to be new, it had auto-scrubbers that
took the schematode out with one well-placed burst of plasma particulate.

Benny always bought the best and piled it high. The sandwich was delicious. It had no horseradish.

Datomic Schematode takes your concise expression of schema and constraints and expands upon it, so you can d/transact without explaining every detail yourself. It’s better than a talking worm.

Uses https://github.com/Yuppiechef/datomic-schema .

Artifact

Clojars Project

Examples

In the following examples, a few details are ellided. Please see dev/datomic_schematode/examples/deli_menu.clj for the full example code.

Express Your Schema

Here's a small schema for a deli menu:

(ns datomic-schematode.examples.deli-menu
  (:require [datomic-schematode :as dst]
            [datomic-schematode.constraints :as ds-constraints]))

(def schema1
  [{:namespace :sandwich
    :attrs [[:name :string :indexed]
            [:bread :enum [:focaccia :wheat :maize :rice] :indexed]
            [:meat :string "Many people like meat on their sandwiches"]
            [:needs-toothpick :boolean]]}
   {:namespace :salad
    :attrs [[:name :string :indexed]
            [:base :enum [:lettuce :spinach :pasta :unicorns] :indexed]
            [:dressing :enum [:ranch :honey-mustard :italian :ceasar :minoan]]]}])

Expand Your Schema

You don't need to do this normally, but it's always good to know how things work (especially if you're debugging!).

datomic-schematode.examples.deli-menu> (pprint (dst/schematize schema1 d/tempid))
;; =>(({:db/noHistory false,
;; =>   :db/cardinality :db.cardinality/one,
;; =>   :db.install/_attribute :db.part/db,
;; =>   :db/index false,
;; =>   :db/fulltext false,
;; =>   :db/doc "",
;; =>   :db/isComponent false,
;; =>   :db/valueType :db.type/boolean,
;; =>   :db/ident :sandwich/needs-toothpick,
;; =>   :db/id {:part :db.part/db, :idx -1000085}}
;; =>  {:db/noHistory false,
;; =>   :db/cardinality :db.cardinality/one,
;; =>   :db.install/_attribute :db.part/db,
;; =>   :db/index false,
;; => ...

Load Your Schema

Schematode has a load-schema! fn to transact your schema for you.

datomic-schematode.examples.deli-menu> (dst/load-schema! (d/connect db-url) schema1)
;; => (#<promise$settable_future$reify__4958@6af8f1e9: {:db-before datomic.db.Db@72124995, :db-after datomic.db.Db@c5df3f53, :tx-data [#Datum{:e 13194139534316 :a 50 :v #inst "2014-03-15T04:23:47.235-00:00" :tx 13194139534316 :added true}], :tempids {}}> ...)

Assert Some Facts

Let's add some things to our deli menu.

datomic-schematode.examples.deli-menu> (d/transact (d/connect db-url)
                                                   [{:db/id (d/tempid :db.part/user)
                                                     :sandwich/name "Norville's #1"
                                                     :sandwich/bread :sandwich.bread/focaccia
                                                     :sandwich/meat "corned beef"
                                                     :sandwich/needs-toothpick true}
                                                    {:db/id (d/tempid :db.part/user)
                                                     :sandwich/name "Thanksgiving Leftovers"
                                                     :sandwich/bread :sandwich.bread/maize
                                                     :sandwich/meat "turkey"
                                                     :sandwich/needs-toothpick false}
                                                    {:db/id (d/tempid :db.part/user)
                                                     :salad/name "Ceasar"
                                                     :salad/base :salad.base/lettuce
                                                     :salad/dressing :salad.dressing/ceasar}])
;; => #<promise$settable_future$reify__4958@65876428: {:db-before datomic.db.Db@bc569020, :db-after datomic.db.Db@eb44b720, :tx-data ...

Check Your Facts

Did our assertions work?

datomic-schematode.examples.deli-menu> (let [db (d/db (d/connect db-url))
                                             entities (map #(d/touch
                                                             (d/entity db
                                                                       (first %)))
                                                           (d/q '[:find ?e
                                                                  :where [?e :sandwich/bread]] db))]
                                         {:entities entities :count (count entities)})
;; => {:entities ({:sandwich/needs-toothpick true, :sandwich/meat "corned beef", :sandwich/bread :sandwich.bread/focaccia, :sandwich/name "Norville's #1", :db/id 17592186045433}
;; =>             {:sandwich/needs-toothpick false, :sandwich/meat "turkey", :sandwich/bread :sandwich.bread/maize, :sandwich/name "Thanksgiving Leftovers", :db/id 17592186045434}),
;; =>  :count 2}

Using Constraints

Constraints Step 1: Express Them

Datomic Schematode enables you to use any db/fn as a constraint, providing that it:

  1. returns nil on success (i.e., the constraint is satisifed) and
  2. returns a message string on failure.

Suppose that you want to constrain your data such that no sandwich can ever be named "soap-scum" (a poor example of a constraint, but a concise example of how any constraint fn could be written), and such that no two sandwiches can have the same bread and meat. The "soap-scum" constraint is not likely to be common, so you'll have to write your own db/fn for that one. But datomic-schematode.constraints/unique can help you out with multi-attribute uniqueness. Here's how the updated schema looks:

(def schema2
  [{:namespace :sandwich
    :attrs [[:name :string :indexed]
            [:bread :enum [:focaccia :wheat :maize :rice] :indexed]
            [:meat :string "Many people like meat on their sandwiches"]
            [:needs-toothpick :boolean]]
    :dbfns [;; We can express any db/fns we want here. If a
            ;; db/fn has the
            ;; :schematode.constraint-fn/active attribute
            ;; with the value true, it will be called as a
            ;; schematode constraint fn, which must return
            ;; either nil (success) or a message explaining
            ;; the violated constraint.
            {:db/ident :my-fn ; The :ident will be namespaced! ...in this case, to :sandwich/my-fn
             :schematode.constraint-fn/active true ; required
             :schematode.constraint-fn/name "Avoid at least one gross sandwich name" ; optional
             :schematode.constraint-fn/desc "Sandwiches with gross names repel customers." ; optional
             :db/fn (d/function '{:lang :clojure
                                  :params [db]
                                  :code (if (empty? (d/q '[:find ?e
                                                           :where [?e :sandwich/name "soap-scum"]]
                                                         db))
                                          nil
                                          "Ew. You are not allowed to name a sandwich \"soap-scum\".")})}
            ;; We can use helper fns to create common constraints.
            (ds-constraints/unique :sandwich :bread :meat)]}
   {:namespace :salad
    :attrs [[:name :string :indexed]
            [:base :enum [:lettuce :spinach :pasta :unicorns] :indexed]
            [:dressing :enum [:ranch :honey-mustard :italian :ceasar :minoan]]]}])

Let's pause and take a look at what schema2 looks like after expansion. dst/schematize works just as it did before, but here's how you can see what the db fns look like before they're transacted by dst/load-schema!:

datomic-schematode.examples.deli-menu> (pprint (dst/dbfnize schema2 d/tempid))
;; =>({:db/fn
;; =>  {:lang :clojure,
;; =>   :imports [],
;; =>   :requires [],
;; =>   :params [db],
;; =>   :code
;; =>   "(if (empty? (d/q (quote [:find ?e :where [?e :sandwich/name \"soap-scum\"]]) db)) nil \"Ew. You are not allowed to name a sandwich \\\"soap-scum\\\".\")",
;; =>   :fnref #<Delay@33526b60: :not-delivered>},
;; =>  :schematode.constraint-fn/desc
;; =>  "Sandwiches with gross names repel customers.",
;; =>  :schematode.constraint-fn/name
;; =>  "Avoid at least one gross sandwich name",
;; =>  :schematode.constraint-fn/active true,
;; =>  :db/ident :sandwich/my-fn,
;; =>  :db/id {:part :db.part/user, :idx -1000107}}
;; => {:db/fn
;; => ...

Constraints Step 2: Transact the necessary Schematode constraints schema and db/fns:

datomic-schematode.examples.deli-menu> (dst/init-schematode-constraints! (d/connect db-url))
;; => (#<promise$settable_future$reify__4958@7dd81cbd: {:db-before datomic.db.Db@d33b648e, :db-after datomic.db.Db@d4d8c6e7, :tx-data ...)

Constraints Step 3: Transact your schema with constraints added:

datomic-schematode.examples.deli-menu> (dst/load-schema! (d/connect db-url) schema2)
;; => (#<promise$settable_future$reify__4958@4ffefcb1: {:db-before datomic.db.Db@36c18235, :db-after datomic.db.Db@7827734f, :tx-data ...)

Constraints Step 4: Use :schematode/tx for all transactions.

datomic-schematode/tx is a handy wrapper fn you might like for this.

datomic-schematode.examples.deli-menu> (d/transact (d/connect db-url)
                                                   [[:schematode/tx :enforce [{:db/id (d/tempid :db.part/user)
                                                                               :sandwich/name "soap-scum"}
                                                                              {:db/id (d/tempid :db.part/user)
                                                                               :sandwich/name "Just Rice"
                                                                               :sandwich/bread :sandwich.bread/rice
                                                                               :sandwich/meat ""}
                                                                              {:db/id (d/tempid :db.part/user)
                                                                               :sandwich/name "Only Rice"
                                                                               :sandwich/bread :sandwich.bread/rice
                                                                               :sandwich/meat ""}]]])
;; => Exception ["Ew. You are not allowed to name a sandwich \"soap-scum\"."]["unique constraint failed for [:sandwich/bread :sandwich/meat]"]  ns-10241/eval10242/fn--10243 (form-init9213208939110354565.clj:1)

;; Or just use datomic-schematode/tx:
datomic-schematode.examples.deli-menu> (dst/tx (d/connect db-url)
                                                   :enforce
                                                   [{:db/id (d/tempid :db.part/user)
                                                     :sandwich/name "soap-scum"}
                                                    {:db/id (d/tempid :db.part/user)
                                                     :sandwich/name "Just Rice"
                                                     :sandwich/bread :sandwich.bread/rice
                                                     :sandwich/meat ""}
                                                    {:db/id (d/tempid :db.part/user)
                                                     :sandwich/name "Only Rice"
                                                     :sandwich/bread :sandwich.bread/rice
                                                     :sandwich/meat ""}])
;; => Exception ["Ew. You are not allowed to name a sandwich \"soap-scum\"."]["unique constraint failed for [:sandwich/bread :sandwich/meat]"]  ns-10241/eval10242/fn--10243 (form-init9213208939110354565.clj:1)

Test Your Constraints

You can test your constraints without attempting to transact anything. Just pull the :schematode/tx* db/fn out of Datomic and execute it on your transaction data (or use datomic-schematode/tx*, which wraps :schematode/tx* for you):

datomic-schemtode.examples.deli-menu> (let [my-schematode-tx* (:db/fn (d/entity (d/db (d/connect db-url)) :schematode/tx*))]
                                         (my-schematode-tx* (d/db (d/connect db-url))
                                                            [{:db/id (d/tempid :db.part/user)
                                                              :sandwich/name "soap-scum"}
                                                             {:db/id (d/tempid :db.part/user)
                                                              :sandwich/name "Just Rice"
                                                              :sandwich/bread :sandwich.bread/rice
                                                              :sandwich/meat ""}
                                                             {:db/id (d/tempid :db.part/user)
                                                              :sandwich/name "Only Rice"
                                                              :sandwich/bread :sandwich.bread/rice
                                                              :sandwich/meat ""}]))
;; => ("Ew. You are not allowed to name a sandwich \"soap-scum\"." "unique constraint failed for [:sandwich/bread :sandwich/meat]")

;; Or just use datomic-schematode/tx*:
datomic-schematode.examples.deli-menu> (dst/tx* (d/connect db-url)
                                                    [{:db/id (d/tempid :db.part/user)
                                                      :sandwich/name "soap-scum"}
                                                     {:db/id (d/tempid :db.part/user)
                                                      :sandwich/name "Just Rice"
                                                      :sandwich/bread :sandwich.bread/rice
                                                      :sandwich/meat ""}
                                                     {:db/id (d/tempid :db.part/user)
                                                      :sandwich/name "Only Rice"
                                                      :sandwich/bread :sandwich.bread/rice
                                                      :sandwich/meat ""}])
;; => ("Ew. You are not allowed to name a sandwich \"soap-scum\"." "unique constraint failed for [:sandwich/bread :sandwich/meat]")

Ignore Your Constraints

If you want to know about constraint violations, but transact the data anyhow, you can use :warn instead of :enforce when you call :schematode/tx.

N.B.: Don't do this in production. Warning once and then continuing without resolving the constraint issues will result in the same constraint messages being added to all future txs executed with :warn.

datomic-schematode.examples.deli-menu> (d/transact (d/connect db-url)
                                                   [[:schematode/tx :warn [{:db/id (d/tempid :db.part/user)
                                                                            :sandwich/name "soap-scum"}
                                                                           {:db/id (d/tempid :db.part/user)
                                                                            :sandwich/name "Just Rice"
                                                                            :sandwich/bread :sandwich.bread/rice
                                                                            :sandwich/meat ""}
                                                                           {:db/id (d/tempid :db.part/user)
                                                                            :sandwich/name "Only Rice"
                                                                            :sandwich/bread :sandwich.bread/rice
                                                                            :sandwich/meat ""}]]])
;; => #<promise$settable_future$reify__4958@35e20aca: {:db-before datomic.db.Db@4caaa420, :db-after datomic.db.Db@df9d98cd ...
;; => ... #Datum{:e 13194139534344 :a 74 :v "[\"Ew. You are not allowed to name a sandwich \\\"soap-scum\\\".\"][\"unique constraint failed for [:sandwich/bread :sandwich/meat]\"]" ...

Note that the constraint messages have been applied to the TX entity.

Analyze Constraint Costs

You can query the TX entities for the time elapsed while applying schematode constraints, or you can just call datomic-schematode/constraint-cost-stats.

This has not been tested with a large database, so the performance characteristics in different load scenarios and at scale are unknown. User beware!

datomic-schematode.examples.deli-menu> (let [db (d/db (d/connect db-url))
                                             query '[:find ?e :where [?e :schematode.constraint/elapsed-msec]]]
                                         (map #(:schematode.constraint/elapsed-msec (d/entity db (first %)))
                                              (d/q query db)))
;; => (0.001691 0.001691 0.001691 0.001691 0.001691)
datomic-schematode.examples.deli-menu> (dst/constraint-cost-stats (d/connect db-url))
;; => {:mean-msec 0.0016910000000000002, :median-msec 0.001691, :tx-count 5, :standard-deviation-msec 2.42434975903054E-19, :total-msec 0.008455}

TODO:

  • Add support for constraints executed on the peer.
  • Add vanilla support for required attrs.
  • Write much better tests.

Thanks

...to Aaron Brooks for sharing the idea for what became :schematode/tx with me.

datomic-schematode's People

Contributors

moquist avatar

Watchers

 avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.