From 611d44fcceab96c42140162270d963507a668b04 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Fri, 9 Sep 2016 15:26:13 -0700 Subject: [PATCH] Process lookup-refs in batches. Fixes #25. This uses a common table expression and multiple SQL calls rather than a temporary table, since transactions with huge numbers of distinct lookup-refs are likely to be very rare. We mark lookup-refs with `lookup-ref`, which is a little awkward because binding `(let [[a v] lookup-ref] ...)` doesn't directly work, but avoids some ambiguity present in Datomic and DataScript around interpreting lookup-refs as multiple value lists. (Which bit the tests in an earlier version of this patch!) --- src/common/datomish/api.cljc | 2 + src/common/datomish/db.cljc | 79 +++++++++++++++++++++++ src/common/datomish/db/debug.cljc | 9 +++ src/common/datomish/transact.cljc | 71 +++++++++----------- src/common/datomish/transact/explode.cljc | 1 + test/datomish/db_test.cljc | 59 ++++++++++++++++- 6 files changed, 178 insertions(+), 43 deletions(-) diff --git a/src/common/datomish/api.cljc b/src/common/datomish/api.cljc index 6ceb93e4..567335e0 100644 --- a/src/common/datomish/api.cljc +++ b/src/common/datomish/api.cljc @@ -32,6 +32,8 @@ (def id-literal db/id-literal) +(def lookup-ref db/lookup-ref) + (def db transact/db) (def entid db/entid) diff --git a/src/common/datomish/db.cljc b/src/common/datomish/db.cljc index 005ece4e..fd926685 100644 --- a/src/common/datomish/db.cljc +++ b/src/common/datomish/db.cljc @@ -63,6 +63,23 @@ (defn id-literal? [x] (instance? TempId x)) +(defrecord LookupRef [a v]) + +(defn lookup-ref + [a v] + (if (and + (or (keyword? a) + (integer? a)) + v) + (->LookupRef a v) + (raise (str "Lookup-ref with bad attribute " a " or value " v + {:error :transact/bad-lookup-ref, :a a, :v v})))) + +(defn lookup-ref? [x] + "Return `x` if `x` is like [:attr value], nil otherwise." + (when (instance? LookupRef x) + x)) + (defprotocol IClock (now [clock] @@ -105,6 +122,13 @@ [db a v] "Search for a single matching datom using the AVET index.") + ( searchid. + av->searchid + (into {} (map vector avs (range))) + + ;; Each query takes 4 variables per item. So we partition into max-sql-vars / 4. + qs + (map + (fn [chunk] + (cons + ;; Query string. + (apply str "WITH t(searchid, a, v, value_type_tag) AS (VALUES " + (apply str (repeater (count chunk))) ;; TODO: join? + ") SELECT t.searchid, d.e + FROM t, datoms AS d + WHERE d.index_avet IS NOT 0 AND d.a = t.a AND d.value_type_tag = t.value_type_tag AND d.v = t.v") + + ;; Bindings. + (mapcat (fn [[[a v] searchid]] + (let [a (entid db a) + [v tag] (ds/->SQLite schema a v)] + [searchid a v tag])) + chunk))) + + (partition-all (quot max-sql-vars 4) av->searchid)) + + ;; Map searchid -> e. There's a generic reduce that takes [pair-chan] lurking in here. + searchid->e + (loop [coll {} + qs qs] + (let [[q & qs] qs] + (if q + (let [rs (e) av->searchid)))) + (= [db tx] + (go-pair + (->> + (s/all-rows (:sqlite-connection db) ["SELECT e, a, v, tx FROM datoms WHERE tx >= ?" tx]) + (> (update-txInstant db*))))) -(defn- lookup-ref? [x] - "Return `x` if `x` is like [:attr value], false otherwise." - (and (sequential? x) - (= (count x) 2) - (or (keyword? (first x)) - (integer? (first x))) - x)) - (defn av (fn [r] ;; Conditional (juxt :a :v) that passes through nil. + (when r [(:a r) (:v r)]))] (go-pair - (if (empty? entities) - report - (assoc-in - report [:entities] - ;; We can't use `for` because go-pair doesn't traverse function boundaries. - ;; Apologies for the tortured nested loop. - (loop [[op & entity] (first entities) - next (rest entities) - acc []] - (if (nil? op) - acc - (recur (first next) - (rest next) - (conj acc - (loop [field (first entity) - rem (rest entity) - acc [op]] - (if (nil? field) - acc - (recur (first rem) - (rest rem) - (conj acc - (if-let [[a v] (lookup-ref? field)] - (or - ;; The lookup might fail! If so, throw. - (:e (e (av (db/lookup-ref? field))] + (if-not (unique-identity? (db/entid db a)) + (raise "Lookup-ref found with non-unique-identity attribute " a " and value " v + {:error :transact/lookup-ref-with-non-unique-identity-attribute + :a a + :v v}) + (or + (get av->e [a v]) + (raise "No entity found for lookup-ref with attribute " a " and value " v + {:error :transact/lookup-ref-not-found + :a a + :v v}))) + field)) + resolve (fn [entity] + (mapv resolve1 entity))] + (assoc + report + :entities + (concat + entities + (map resolve (apply concat (vals to-resolve))))))))) (declare = schema in)) - expected))))) + (is (= (map #(dissoc %1 :db/id) (datomish.simple-schema/simple-schema->schema in)) + expected))))) +(deftest-db test-lookup-refs conn + (let [{tx0 :tx} (= (d/db conn) tx1)))))) + + (testing "Looks up value refs" + (let [{tx :tx} (= (d/db conn) tx)))))) + + (testing "Looks up entity refs in maps" + (let [{tx :tx} (= (d/db conn) tx)))))) + + (testing "Looks up value refs in maps" + (let [{tx :tx} (= (d/db conn) tx)))))) + + (testing "Looks up value refs in sequences in maps" + (let [{tx :tx} (= (d/db conn) tx)))))) + + (testing "Fails for missing entities" + (is (thrown-with-msg? + ExceptionInfo #"No entity found for lookup-ref" + (