Add rudimentary constraint checking

main
Eric Ihli 4 years ago
parent d780502a7c
commit 5c1242eccb

@ -34,7 +34,7 @@
clojure.lang.IDeref clojure.lang.IDeref
(deref [_] (into #{} xf @relvar))) (deref [_] (into #{} xf @relvar)))
(deftype BaseRelVar [relvar-name spec store] (deftype BaseRelVar [relvar-name store constraints]
PRelVar PRelVar
(project (project
[this attributes] [this attributes]
@ -47,9 +47,19 @@
(load! [this relations] (reset! store relations)) (load! [this relations] (reset! store relations))
(insert! (insert!
[this relation] [this relation]
(run!
(fn [constraint]
(when (constraint @this)
(throw (ex-info "Constraint Exception" {}))))
constraints)
(swap! store conj relation)) (swap! store conj relation))
(insert! (insert!
[this & relations] [this & relations]
(run!
(fn [constraint]
(when (constraint @this)
(throw (ex-info "Constraint Exception" {}))))
constraints)
(swap! store set/union (into #{} relations))) (swap! store set/union (into #{} relations)))
clojure.lang.IDeref clojure.lang.IDeref
@ -61,5 +71,9 @@
(defn restrict- [relvar xf] (defn restrict- [relvar xf]
(->RelVar relvar xf)) (->RelVar relvar xf))
(def *constraints* (atom {}))
(defmacro defrelvar (defmacro defrelvar
[relvar-name & specs]) [relvar-name & constraints]
(swap! *constraints* assoc-in [relvar-name :constraints] constraints)
`(->BaseRelVar '~relvar-name (atom #{}) [~@constraints]))

@ -1,6 +1,5 @@
(ns com.owoga.prhyme.core (ns com.owoga.prhyme.core
(:require [clojure.java.io :as io] (:require [clojure.java.io :as io]
[clojure.pprint :as pprint]
[clojure.string :as string] [clojure.string :as string]
[clojure.set :as set] [clojure.set :as set]
[com.owoga.prhyme.util :as u] [com.owoga.prhyme.util :as u]

@ -87,6 +87,14 @@ Let's start by imagining a nice syntax for this.
** Relvar protocols ** Relvar protocols
The PRelVar functions return a RelVar that is not data-modifiable - it doesn't
have the load!, insert!, delete!, etc... functions.
For performance reasons, we do still need a way to persist derived relvars
somewhere. We'll eventually want to define some type of semantics for specifying
that a derived relation be cached rather than requiring it to be recalculated
every time the relations of its base relvar are updated.
#+NAME: relvar protocols #+NAME: relvar protocols
#+BEGIN_SRC clojure :noweb no-export #+BEGIN_SRC clojure :noweb no-export
(defprotocol PRelations (defprotocol PRelations
@ -110,7 +118,7 @@ Let's start by imagining a nice syntax for this.
(rename [this renames])) (rename [this renames]))
#+END_SRC #+END_SRC
** Relvar implementation ** Part 1. of Infrastructure for Essential State
The =project= function of a relvar will be returning another relvar. The The =project= function of a relvar will be returning another relvar. The
implementation might look something like this: implementation might look something like this:
@ -146,7 +154,7 @@ implementing these types/functions.
clojure.lang.IDeref clojure.lang.IDeref
(deref [_] (into #{} xf @relvar))) (deref [_] (into #{} xf @relvar)))
(deftype BaseRelVar [relvar-name spec store] (deftype BaseRelVar [relvar-name store constraints]
PRelVar PRelVar
<<relational algebra for base relvars>> <<relational algebra for base relvars>>
@ -162,10 +170,13 @@ implementing these types/functions.
(defn restrict- [relvar xf] (defn restrict- [relvar xf]
(->RelVar relvar xf)) (->RelVar relvar xf))
(defmacro defrelvar <<constraints>>
[relvar-name & specs])
#+END_SRC #+END_SRC
Clojure's core set library includes a =project= function, but I'm not sure if
it returns a transducer. I'll mark that as a todo. Look into whether this
map/select-keys can be replaced by =set/project=.
#+NAME: relational algebra for derived relvars #+NAME: relational algebra for derived relvars
#+BEGIN_SRC clojure #+BEGIN_SRC clojure
(project (project
@ -191,9 +202,19 @@ implementing these types/functions.
(load! [this relations] (reset! store relations)) (load! [this relations] (reset! store relations))
(insert! (insert!
[this relation] [this relation]
(run!
(fn [constraint]
(when (constraint @this)
(throw (ex-info "Constraint Exception" {}))))
constraints)
(swap! store conj relation)) (swap! store conj relation))
(insert! (insert!
[this & relations] [this & relations]
(run!
(fn [constraint]
(when (constraint @this)
(throw (ex-info "Constraint Exception" {}))))
constraints)
(swap! store set/union (into #{} relations))) (swap! store set/union (into #{} relations)))
#+END_SRC #+END_SRC
@ -210,7 +231,7 @@ implementing these types/functions.
(ns example (ns example
(:require [com.owoga.frp.infrastructure :refer [->BaseRelVar project load!]])) (:require [com.owoga.frp.infrastructure :refer [->BaseRelVar project load!]]))
(def Offer (->BaseRelVar 'Offer nil (atom #{}))) (def Offer (->BaseRelVar 'Offer (atom #{}) '()))
(def OfferPrices (project Offer [:price])) (def OfferPrices (project Offer [:price]))
(load! Offer #{{:address "123 Fake St." :price 2e5}}) (load! Offer #{{:address "123 Fake St." :price 2e5}})
@ -222,18 +243,36 @@ implementing these types/functions.
: #{{:price 200000.0}} : #{{:price 200000.0}}
: :
** Derived Relvar implementation ** Part 2. of Infrastructure for Essential State
The PRelVar functions return a RelVar that is not data-modifiable - it doesn't have the load!, insert!, delete!, etc... functions. The code above covers requirement 1. from the infrastructure for essential state; namely:
For performance reasons, we do still need a way to persist derived relvars 1. some means of storing and retrieving data in the form of relations assigned to named relvars
somewhere. We'll eventually want to define some type of semantics for specifying
that a derived relation be cached rather than requiring it to be recalculated Now we can load!, insert!, project and restrict. We'll get to adding some other functionality later. Let's explore something more complex: constraints.
every time the relations of its base relvar are updated.
This is requirement 2.
2. a state manipulation language which allows the stored relvars to be updated (within the bounds of the integrity constraints)
Instead of definining a RelVar type direcly, like we've done in the examples
above, we can define it inside a macro that handles creating constraints for us.
This way the relvar and constraints can't easily be evaluated in seperate parts
of the code that might allow relations that violate soon-to-be constraints to be
loaded
#+NAME: constraints
#+BEGIN_SRC clojure :noweb no-export
(def *constraints* (atom {}))
(defmacro defrelvar
[relvar-name & constraints]
(swap! *constraints* assoc-in [relvar-name :constraints] constraints)
`(->BaseRelVar '~relvar-name (atom #{}) [~@constraints]))
#+END_SRC
#+NAME: essential state infrastructure #+NAME: essential state infrastructure
#+BEGIN_SRC clojure :noweb no-export #+BEGIN_SRC clojure :noweb no-export
(def constraints (atom {}))
(defmacro candidate-key [relvar tuple] (defmacro candidate-key [relvar tuple]
`(swap! constraints assoc-in ['~relvar :candidate-key] '~tuple)) `(swap! constraints assoc-in ['~relvar :candidate-key] '~tuple))
@ -267,14 +306,7 @@ every time the relations of its base relvar are updated.
clojure.lang.IDeref clojure.lang.IDeref
(deref [_] @store)) (deref [_] @store))
(defmacro defrelvar
[relvar-name & specs]
(let [ns-str (str *ns*)
relvar-kw (keyword ns-str (str relvar-name))
specs (map eval (for [[k v] (partition 2 specs)]
`(s/def ~(keyword ns-str (str relvar-name "-" (name k))) ~v)))]
(eval `(s/def ~relvar-kw (s/coll-of (s/keys :req ~specs))))
`(def ~relvar-name (->RelVar ~(str relvar-name) ~relvar-kw (atom #{})))))
(defrelvar dictionary-word (defrelvar dictionary-word
:id int? :id int?
@ -448,14 +480,22 @@ Despite this the intention is not for observers to be used as a substitute for t
(deftest test-project (deftest test-project
(testing "projection" (testing "projection"
(let [Offer (frp/->BaseRelVar 'Offer nil (atom #{})) (let [Offer (frp/->BaseRelVar 'Offer (atom #{}) '())
OfferPrices (frp/project Offer [:price])] OfferPrices (frp/project Offer [:price])]
(frp/load! Offer #{{:address "123 Fake St." :price 2e5}}) (frp/load! Offer #{{:address "123 Fake St." :price 2e5}})
(is (= @OfferPrices #{{:price 2e5}}))))) (is (= @OfferPrices #{{:price 2e5}})))))
(deftest test-insert! (deftest test-insert!
(testing "insert!" (testing "insert!"
(let [Offer (frp/->BaseRelVar 'Offer nil (atom #{}))] (let [Offer (frp/->BaseRelVar 'Offer (atom #{}) '())]
(frp/insert! Offer {:address "123 Fake St." :price 1.5e5}) (frp/insert! Offer {:address "123 Fake St." :price 1.5e5})
(is (= @Offer #{{:address "123 Fake St." :price 1.5e5}}))))) (is (= @Offer #{{:address "123 Fake St." :price 1.5e5}})))))
(deftest test-defrelvar
(testing "macro works"
(let [Offer (frp/defrelvar Offer (fn [offers] (map #(> (:price %) 0) offers)))]
(is (thrown-with-msg?
Exception
#"Constraint Exception"
(frp/insert! Offer {:price -1}))))))
#+END_SRC #+END_SRC

@ -2,16 +2,23 @@
(:require [com.owoga.frp.infrastructure :as frp] (:require [com.owoga.frp.infrastructure :as frp]
[clojure.test :refer [deftest is testing]])) [clojure.test :refer [deftest is testing]]))
(deftest test-insert!
(testing "insert!"
(let [Offer (frp/->BaseRelVar 'Offer nil (atom #{}))]
(frp/insert! Offer {:address "123 Fake St." :price 1.5e5})
(is (= @Offer #{{:address "123 Fake St." :price 1.5e5}})))))
(deftest test-project (deftest test-project
(testing "projection" (testing "projection"
(let [Offer (frp/->BaseRelVar 'Offer nil (atom #{})) (let [Offer (frp/->BaseRelVar 'Offer (atom #{}) '())
OfferPrices (frp/project Offer [:price])] OfferPrices (frp/project Offer [:price])]
(frp/load! Offer #{{:address "123 Fake St." :price 2e5}}) (frp/load! Offer #{{:address "123 Fake St." :price 2e5}})
(is (= @OfferPrices #{{:price 2e5}}))))) (is (= @OfferPrices #{{:price 2e5}})))))
(deftest test-insert!
(testing "insert!"
(let [Offer (frp/->BaseRelVar 'Offer (atom #{}) '())]
(frp/insert! Offer {:address "123 Fake St." :price 1.5e5})
(is (= @Offer #{{:address "123 Fake St." :price 1.5e5}})))))
(deftest test-defrelvar
(testing "macro works"
(let [Offer (frp/defrelvar Offer (fn [offers] (map #(> (:price %) 0) offers)))]
(is (thrown-with-msg?
Exception
#"Constraint Exception"
(frp/insert! Offer {:price -1}))))))

Loading…
Cancel
Save