From 9497d69b44a8163857663007a7ecd272aa120dad Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Thu, 28 Jul 2016 15:30:46 -0700 Subject: [PATCH] Respect :db/unique constraints; test upserts. This version includes SQLite-level unique indexes; these should never be needed. I've included them as a fail-safe while testing; they'll help us catch errors in the transaction layer above. --- src/datomish/db.cljc | 226 ++++++++++++++++++++------------ src/datomish/schema.cljc | 4 + src/datomish/sqlite_schema.cljc | 10 +- test/datomish/db_test.cljc | 198 ++++++++++++++++++++++++++-- 4 files changed, 343 insertions(+), 95 deletions(-) diff --git a/src/datomish/db.cljc b/src/datomish/db.cljc index d9dc9ee6..806660db 100644 --- a/src/datomish/db.cljc +++ b/src/datomish/db.cljc @@ -77,7 +77,7 @@ (let [e (:e row) a (:a row) v (:v row)] - (Datom. e a (ds/<-SQLite schema a v) (:tx row) (:added row)))) + (Datom. e a (ds/<-SQLite schema a v) (:tx row) (and (some? (:added row)) (not= 0 (:added row)))))) (defrecord DB [sqlite-connection schema idents current-tx] ;; idents is map {ident -> entid} of known idents. See http://docs.datomic.com/identity.html#idents. @@ -98,7 +98,7 @@ v (and v (ds/->SQLite schema a v))] ;; We assume e and a are always given. (go-pair (->> - {:select [:*] ;; e :a :v :tx] ;; TODO: generalize columns. + {:select [:e :a :v :tx [1 :added]] ;; TODO: generalize columns. :from [:datoms] :where (cons :and (map #(vector := %1 %2) [:e :a :v :tx] (take-while (comp not nil?) [e a v tx])))} ;; Must drop nils. (sql/format) @@ -113,7 +113,9 @@ v (ds/->SQLite schema a v)] (go-pair (->> - {:select [:*] :from [:datoms] :where [:and [:= :a a] [:= :v v]]} + {:select [:e :a :v :tx [1 :added]] ;; TODO: generalize columns. + :from [:datoms] + :where [:and [:= :a a] [:= :v v] [:= :index_avet 1]]} (sql/format) (s/all-rows (:sqlite-connection db)) @@ -134,8 +136,12 @@ ;; Update materialized datom view. (if (.-added datom) (DB {:sqlite-connection sqlite-connection :idents idents @@ -245,11 +264,6 @@ (defn connection-with-db [db] (map->Connection {:current-db (atom db)})) -(defrecord TxReport [db-before db-after entities tx-data tempids]) - -(defn- report? [x] - (and (instance? TxReport x))) - ;; ;; TODO: persist max-tx and max-eid in SQLite. (defn maybe-datom->entity [entity] @@ -265,21 +279,28 @@ true entity)) -(defn maybe-explode [schema entity] ;; TODO db? schema? - (cond - (map? entity) - ;; TODO: reverse refs, lists, nested maps - (let [eid (or (:db/id entity) - (id-literal :db.part/temp))] ;; Must upsert if no ID given. TODO: check if this fails in Datomic/DS. - (for [[a v] (dissoc entity :db/id)] - [:db/add eid a v])) +(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 - ;; (raise "Map entities are not yet supported, got " entity - ;; {:error :transact/syntax - ;; :op entity }) + (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 })) - true - [entity])) + true + (recur (util/conj-in report [:entities] entity) entities)))))) (defn maybe-ident->entid [db [op & entity :as orig]] ;; TODO: use something faster than `into` here. @@ -302,32 +323,30 @@ (defn preprocess [db report] {:pre [(db? db) (report? report)]} - (let [initial-es (conj (or (:entities report) []) (tx-entity db))] + (let [initial-es (or (:entities report) [])] (when-not (sequential? initial-es) (raise "Bad transaction data " initial-es ", expected sequential collection" {:error :transact/syntax, :tx-data initial-es})) - (->> - (-> - (comp - ;; Track the provenance of each assertion for error reporting. - (map #(with-meta % {:source %})) + ;; TODO: find an approach that generates less garbage. + (-> + report - ;; Normalize Datoms into :db/add or :db/retract vectors. - (map maybe-datom->entity) + (update :entities conj (tx-entity db)) - ;; 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 maybe-explode (schema db))) + ;; Normalize Datoms into :db/add or :db/retract vectors. + (update :entities (partial map maybe-datom->entity)) - ;; Replace idents with entids where possible. - (map (partial maybe-ident->entid db)) + ;; 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))) - ;; Add tx if not given. - (map (partial maybe-add-current-tx (current-tx db)))) - (transduce conj [] initial-es)) - (assoc-in report [:entities])))) + ;; Replace idents with entids where possible. + (update :entities (partial map (partial maybe-ident->entid db))) + + ;; Add tx if not given. + (update :entities (partial map (partial maybe-add-current-tx (current-tx db))))))) (defn- lookup-ref? [x] (and (sequential? x) @@ -376,16 +395,6 @@ field))))) (assoc-in report [:entities])))) ;; TODO: meta. -(defonce -eid (atom (- 0x200 1))) - -;; TODO: better here. -(defn- next-eid [db] - (swap! -eid inc)) - -(defn- allocate-eid - [report id-literal eid] - (assoc-in report [:tempids id-literal] eid)) - (declare datom. + (doseq [[e a v tx added :as datom] (:tx-data report)] + + (when added + ;; Check for violated :db/unique constraint between datom and existing store. + (when (ds/unique? (schema db) a) + (when-let [found (first (tx-data [db report] {:pre [(db? db) (report? report)]} (go-pair (let [initial-report report] @@ -546,19 +600,24 @@ {:pre [(db? db) (report? report)]} (go-pair - (->> report - (preprocess db) + (->> + report + (preprocess db) - (tx-data db) + (> + (s/all-rows (:sqlite-connection db) ["SELECT a, v FROM datoms WHERE e = ?" eid]) + (