Handle nested maps, sequences in maps, and reverse references.
This commit is contained in:
parent
d9a8cb0d6a
commit
44db8116bf
2 changed files with 158 additions and 24 deletions
|
@ -477,28 +477,72 @@
|
|||
true
|
||||
entity))
|
||||
|
||||
(defn explode-entities [schema report]
|
||||
(let [initial-es (:entities report)
|
||||
initial-report (assoc report :entities [])]
|
||||
(loop [report initial-report
|
||||
es initial-es]
|
||||
(let [[entity & entities] es]
|
||||
(cond
|
||||
(nil? entity)
|
||||
report
|
||||
(defn- #?@(:clj [^Boolean reverse-ref?]
|
||||
:cljs [^boolean reverse-ref?]) [attr]
|
||||
(if (keyword? attr)
|
||||
(= \_ (nth (name attr) 0))
|
||||
(raise "Bad attribute type: " attr ", expected keyword"
|
||||
{:error :transact/syntax, :attribute attr})))
|
||||
|
||||
(map? entity)
|
||||
;; TODO: reverse refs, lists, nested maps
|
||||
(if-let [eid (:db/id entity)]
|
||||
(let [exploded (for [[a v] (dissoc entity :db/id)]
|
||||
[:db/add eid a v])]
|
||||
(recur report (concat exploded entities)))
|
||||
(raise "Map entity missing :db/id, got " entity
|
||||
{:error :transact/entity-missing-db-id
|
||||
:op entity }))
|
||||
(defn- reverse-ref [attr]
|
||||
(if (keyword? attr)
|
||||
(if (reverse-ref? attr)
|
||||
(keyword (namespace attr) (subs (name attr) 1))
|
||||
(keyword (namespace attr) (str "_" (name attr))))
|
||||
(raise "Bad attribute type: " attr ", expected keyword"
|
||||
{:error :transact/syntax, :attribute attr})))
|
||||
|
||||
true
|
||||
(recur (util/conj-in report [:entities] entity) entities))))))
|
||||
(declare explode-entity)
|
||||
|
||||
(defn- explode-entity-a-v [db entity eid a v]
|
||||
;; a should be symbolic at this point. Map it. TODO: use ident/entid to ensure we have a symbolic attr.
|
||||
(let [reverse? (reverse-ref? a)
|
||||
straight-a (if reverse? (reverse-ref a) a)
|
||||
straight-a* (get-in db [:idents straight-a] straight-a)
|
||||
_ (when (and reverse? (not (ds/ref? (schema db) straight-a*)))
|
||||
(raise "Bad attribute " a ": reverse attribute name requires {:db/valueType :db.type/ref} in schema"
|
||||
{:error :transact/syntax, :attribute a, :op entity}))
|
||||
a* (get-in db [:idents a] a)]
|
||||
(cond
|
||||
reverse?
|
||||
(explode-entity-a-v db entity v straight-a eid)
|
||||
|
||||
(and (map? v)
|
||||
(not (id-literal? v)))
|
||||
;; Another entity is given as a nested map.
|
||||
(if (ds/ref? (schema db) straight-a*)
|
||||
(let [other (assoc v (reverse-ref a) eid
|
||||
;; TODO: make the new ID have the same part as the original eid.
|
||||
;; TODO: make the new ID not show up in the tempids map. (Does Datomic exposed the new ID this way?)
|
||||
:db/id (id-literal :db.part/user))]
|
||||
(explode-entity db other))
|
||||
(raise "Bad attribute " a ": nested map " v " given but attribute name requires {:db/valueType :db.type/ref} in schema"
|
||||
{:error :transact/entity-map-type-ref
|
||||
:op entity }))
|
||||
|
||||
(sequential? v)
|
||||
(if (ds/multival? (schema db) a*) ;; dm/schema
|
||||
(mapcat (partial explode-entity-a-v db entity eid a) v) ;; Allow sequences of nested maps, etc. This does mean [[1]] will work.
|
||||
(raise "Sequential values " v " but attribute " a " is :db.cardinality/one"
|
||||
{:error :transact/entity-sequential-cardinality-one
|
||||
:op entity }))
|
||||
|
||||
true
|
||||
[[:db/add eid a* v]])))
|
||||
|
||||
(defn- explode-entity [db entity]
|
||||
(if (map? entity)
|
||||
(if-let [eid (:db/id entity)]
|
||||
(mapcat (partial apply explode-entity-a-v db entity eid) (dissoc entity :db/id))
|
||||
(raise "Map entity missing :db/id, got " entity
|
||||
{:error :transact/entity-missing-db-id
|
||||
:op entity }))
|
||||
[entity]))
|
||||
|
||||
(defn explode-entities [db entities]
|
||||
"Explode map shorthand, such as {:db/id e :attr value :_reverse ref}, to a list of vectors,
|
||||
like [[:db/add e :attr value] [:db/add ref :reverse e]]."
|
||||
(mapcat (partial explode-entity db) entities))
|
||||
|
||||
(defn maybe-ident->entid [db [op e a v tx :as orig]]
|
||||
(let [e (get (idents db) e e) ;; TODO: use ident, entid here.
|
||||
|
@ -582,10 +626,7 @@
|
|||
;; Normalize Datoms into :db/add or :db/retract vectors.
|
||||
(update :entities (partial map maybe-datom->entity))
|
||||
|
||||
;; Explode map shorthand, such as {:db/id e :attr value :_reverse ref},
|
||||
;; to a list of vectors, like
|
||||
;; [[:db/add e :attr value] [:db/add ref :reverse e]].
|
||||
(->> (explode-entities (schema db)))
|
||||
(update :entities (partial explode-entities db))
|
||||
|
||||
(update :entities (partial map ensure-entity-form))
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
(<datoms-after db 0))
|
||||
|
||||
(defn- <shallow-entity [db eid]
|
||||
;; TODO: make this actually be <entity. Handle :db.cardinality/many and :db/isComponent.
|
||||
(let [entids (zipmap (vals (dm/idents db)) (keys (dm/idents db)))]
|
||||
(go-pair
|
||||
(->>
|
||||
|
@ -112,6 +113,11 @@
|
|||
:db/unique :db.unique/value
|
||||
:db/valueType :db.type/string}
|
||||
{:db/id :db.part/db :db.install/attribute (dm/id-literal :test -8)}
|
||||
{:db/id (dm/id-literal :test -9)
|
||||
:db/ident :friends
|
||||
:db/cardinality :db.cardinality/many
|
||||
:db/valueType :db.type/ref}
|
||||
{:db/id :db.part/db :db.install/attribute (dm/id-literal :test -9)}
|
||||
])
|
||||
|
||||
(deftest-async test-add-one
|
||||
|
@ -576,3 +582,90 @@
|
|||
|
||||
(finally
|
||||
(<? (dm/close-db db)))))))
|
||||
|
||||
(deftest-async test-explode-sequences
|
||||
(with-tempfile [t (tempfile)]
|
||||
(let [c (<? (s/<sqlite-connection t))
|
||||
db (<? (dm/<db-with-sqlite-connection c test-schema))
|
||||
conn (dm/connection-with-db db)
|
||||
{tx0 :tx} (<? (dm/<transact! conn test-schema))]
|
||||
(try
|
||||
(testing ":db.cardinality/many sequences are accepted"
|
||||
(<? (dm/<transact! conn [{:db/id 101 :aka ["first" "second"]}]))
|
||||
(is (= (<? (<datoms-after (dm/db conn) tx0))
|
||||
#{[101 :aka "first"]
|
||||
[101 :aka "second"]})))
|
||||
|
||||
(testing ":db.cardinality/many sequences are recursively applied, allowing unexpected sequence nesting"
|
||||
(<? (dm/<transact! conn [{:db/id 102 :aka [[["first"]] ["second"]]}]))
|
||||
(is (= (<? (<datoms-after (dm/db conn) tx0))
|
||||
#{[101 :aka "first"]
|
||||
[101 :aka "second"]
|
||||
[102 :aka "first"]
|
||||
[102 :aka "second"]})))
|
||||
|
||||
(testing ":db.cardinality/one sequences fail"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo #"Sequential values"
|
||||
(<? (dm/<transact! conn [{:db/id 101 :email ["@1" "@2"]}])))))
|
||||
|
||||
(finally
|
||||
(<? (dm/close-db db)))))))
|
||||
|
||||
(deftest-async test-explode-maps
|
||||
(with-tempfile [t (tempfile)]
|
||||
(let [c (<? (s/<sqlite-connection t))
|
||||
db (<? (dm/<db-with-sqlite-connection c test-schema))
|
||||
conn (dm/connection-with-db db)
|
||||
{tx0 :tx} (<? (dm/<transact! conn test-schema))]
|
||||
(try
|
||||
(testing "nested maps are accepted"
|
||||
(<? (dm/<transact! conn [{:db/id 101 :friends {:name "Petr"}}]))
|
||||
;; TODO: this works only because we have a single friend.
|
||||
(let [{petr :friends} (<? (<shallow-entity (dm/db conn) 101))]
|
||||
(is (= (<? (<datoms-after (dm/db conn) tx0))
|
||||
#{[101 :friends petr]
|
||||
[petr :name "Petr"]}))))
|
||||
|
||||
(testing "recursively nested maps are accepted"
|
||||
(<? (dm/<transact! conn [{:db/id 102 :friends {:name "Ivan" :friends {:name "Petr"}}}]))
|
||||
;; This would be much easier with `entity` and lookup refs.
|
||||
(let [{ivan :friends} (<? (<shallow-entity (dm/db conn) 102))
|
||||
{petr :friends} (<? (<shallow-entity (dm/db conn) ivan))]
|
||||
(is (= (<? (<datoms-after (dm/db conn) tx0))
|
||||
#{[101 :friends petr]
|
||||
[petr :name "Petr"]
|
||||
[102 :friends ivan]
|
||||
[ivan :name "Ivan"]
|
||||
[ivan :friends petr]}))))
|
||||
|
||||
(testing "nested maps without :db.type/ref fail"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo #"\{:db/valueType :db.type/ref\}"
|
||||
(<? (dm/<transact! conn [{:db/id 101 :aka {:name "Petr"}}])))))
|
||||
|
||||
(finally
|
||||
(<? (dm/close-db db)))))))
|
||||
|
||||
(deftest-async test-explode-reverse-refs
|
||||
(with-tempfile [t (tempfile)]
|
||||
(let [c (<? (s/<sqlite-connection t))
|
||||
db (<? (dm/<db-with-sqlite-connection c test-schema))
|
||||
conn (dm/connection-with-db db)
|
||||
{tx0 :tx} (<? (dm/<transact! conn test-schema))]
|
||||
(try
|
||||
(testing "reverse refs are accepted"
|
||||
(<? (dm/<transact! conn [{:db/id 101 :name "Igor"}]))
|
||||
(<? (dm/<transact! conn [{:db/id 102 :name "Oleg" :_friends 101}]))
|
||||
(is (= (<? (<datoms-after (dm/db conn) tx0))
|
||||
#{[101 :name "Igor"]
|
||||
[102 :name "Oleg"]
|
||||
[101 :friends 102]})))
|
||||
|
||||
(testing "reverse refs without :db.type/ref fail"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo #"\{:db/valueType :db.type/ref\}"
|
||||
(<? (dm/<transact! conn [{:db/id 101 :_aka 102}])))))
|
||||
|
||||
(finally
|
||||
(<? (dm/close-db db)))))))
|
||||
|
|
Loading…
Reference in a new issue