Add :db.fn/retractAttribute and :db.fn/retractEntity. Fixes #46.

There's no distinction made for fulltext attributes, since the values
found by the retractAttributes SELECT are already rowids into the
fulltext_values table and therefore need no additional mapping.
This commit is contained in:
Nick Alexander 2016-09-02 13:18:44 -07:00
parent 94bac9704b
commit 362bdb2028
3 changed files with 330 additions and 32 deletions

View file

@ -172,15 +172,64 @@
:table-alias source/gensym-table-alias :table-alias source/gensym-table-alias
:make-constraints nil})) :make-constraints nil}))
;; TODO: make this not do the tx_lookup. We could achieve this by having additional special values
;; of added0, or by separating the tx_lookup table into before and after tables.
(defn- retractAttributes->queries [eas tx]
(let [where-part
"(e = ? AND a = ?)"
repeater (memoize (fn [n] (interpose " OR " (repeat n where-part))))]
(map
(fn [chunk]
(cons
(apply str
"INSERT INTO tx_lookup (e0, a0, v0, tx0, added0, value_type_tag0, sv, svalue_type_tag)
SELECT e, a, v, ?, 0, value_type_tag, v, value_type_tag
FROM datoms
WHERE "
(repeater (count chunk)))
(cons
tx
(mapcat (fn [[_ e a]]
[e a])
chunk))))
(partition-all (quot (dec max-sql-vars) 2) eas))))
;; TODO: make this not do the tx_lookup. We could achieve this by having additional special values
;; of added0, or by separating the tx_lookup table into before and after tables.
(defn- retractEntities->queries [es tx]
(let [ref-tag (sqlite-schema/->tag :db.type/ref)
;; TODO: include index_vaet flag here, so we can use that index to speed up the deletion.
where-part
(str "e = ? OR (v = ? AND value_type_tag = " ref-tag ")") ;; Retract the entity and all refs to the entity.
repeater (memoize (fn [n] (interpose " OR " (repeat n where-part))))]
(map
(fn [chunk]
(cons
(apply str
"INSERT INTO tx_lookup (e0, a0, v0, tx0, added0, value_type_tag0, sv, svalue_type_tag)
SELECT e, a, v, ?, 0, value_type_tag, v, value_type_tag
FROM datoms
WHERE "
(repeater (count chunk)))
(cons
tx
(mapcat (fn [[_ e]]
[e e])
chunk))))
(partition-all (quot (dec max-sql-vars) 2) es))))
(defn- retractions->queries [retractions tx fulltext? ->SQLite] (defn- retractions->queries [retractions tx fulltext? ->SQLite]
(let (let
[f-q [f-q
"WITH vv AS (SELECT rowid FROM fulltext_values WHERE text = ?) "WITH vv AS (SELECT rowid FROM fulltext_values WHERE text = ?)
INSERT INTO tx_lookup (e0, a0, v0, tx0, added0, value_type_tag0, sv, svalue_type_tag) INSERT INTO tx_lookup (e0, a0, v0, tx0, added0, value_type_tag0, sv, svalue_type_tag)
VALUES (?, ?, (SELECT rowid FROM vv), ?, 0, ?, (SELECT rowid FROM vv), ?)" VALUES (?, ?, (SELECT rowid FROM vv), ?, 0, ?, (SELECT rowid FROM vv), ?)"
non-f-q non-f-q
"INSERT INTO tx_lookup (e0, a0, v0, tx0, added0, value_type_tag0, sv, svalue_type_tag) "INSERT INTO tx_lookup (e0, a0, v0, tx0, added0, value_type_tag0, sv, svalue_type_tag)
VALUES (?, ?, ?, ?, 0, ?, ?, ?)"] VALUES (?, ?, ?, ?, 0, ?, ?, ?)"]
(map (map
(fn [[_ e a v]] (fn [[_ e a v]]
@ -394,7 +443,7 @@
SELECT e, a, v, ?, 0, value_type_tag SELECT e, a, v, ?, 0, value_type_tag
FROM tx_lookup FROM tx_lookup
WHERE added0 IS 2 AND ((sv IS NOT NULL) OR (sv IS NULL AND v0 IS NOT v)) AND v IS NOT NULL" tx] ;; TODO: get rid of magic value 2. WHERE added0 IS 2 AND ((sv IS NOT NULL) OR (sv IS NULL AND v0 IS NOT v)) AND v IS NOT NULL" tx] ;; TODO: get rid of magic value 2.
] ]
(go-pair (go-pair
(doseq [q [build-indices insert-into-tx-lookup (doseq [q [build-indices insert-into-tx-lookup
t-datoms-not-already-present t-datoms-not-already-present
@ -431,9 +480,12 @@
queries (atom []) queries (atom [])
operations (group-by first entities)] operations (group-by first entities)]
(when-not (clojure.set/subset? (keys operations) #{:db/retract :db/add}) ;; Belt and braces. At this point, we should have already errored out if op is not known.
(raise (str "Unknown operations " (keys operations)) (let [known #{:db/retract :db/add :db.fn/retractAttribute :db.fn/retractEntity}]
{:error :transact/syntax, :operations (dissoc operations :db/retract :db/add)})) (when-not (clojure.set/subset? (keys operations) known)
(let [unknown (apply dissoc operations known)]
(raise (str "Unknown operations " (apply sorted-set (keys unknown)))
{:error :transact/syntax, :operations (apply sorted-set (keys unknown))}))))
;; We can turn all non-FTS operations into simple SQL queries that we run serially. ;; We can turn all non-FTS operations into simple SQL queries that we run serially.
;; FTS queries require us to get a rowid from the FTS table and use that for ;; FTS queries require us to get a rowid from the FTS table and use that for
@ -441,6 +493,14 @@
;; We can't just freely use `go-pair` here, because this function is so complicated ;; We can't just freely use `go-pair` here, because this function is so complicated
;; that ClojureScript blows the stack trying to compile it. ;; that ClojureScript blows the stack trying to compile it.
(when-let [eas (:db.fn/retractAttribute operations)]
(swap!
queries concat (retractAttributes->queries eas tx)))
(when-let [es (:db.fn/retractEntity operations)]
(swap!
queries concat (retractEntities->queries es tx)))
(when-let [retractions (:db/retract operations)] (when-let [retractions (:db/retract operations)]
(swap! (swap!
queries concat (retractions->queries retractions tx fulltext? ->SQLite))) queries concat (retractions->queries retractions tx fulltext? ->SQLite)))

View file

@ -119,12 +119,13 @@
entity)) entity))
(defn maybe-ident->entid [db [op e a v tx :as orig]] (defn maybe-ident->entid [db [op e a v tx :as orig]]
;; We have to handle all ops, including those when a or v are not defined.
(let [e (db/entid db e) (let [e (db/entid db e)
a (db/entid db a) a (db/entid db a)
v (if (ds/kw? (db/schema db) a) ;; TODO: decide if this is best. We could also check for ref and numeric types. v (if (and a (ds/kw? (db/schema db) a)) ;; TODO: decide if this is best. We could also check for ref and numeric types.
v v
(db/entid db v))] (db/entid db v))]
(when-not (integer? a) (when (and a (not (integer? a)))
(raise "Unknown attribute " a (raise "Unknown attribute " a
{:form orig :attribute a})) {:form orig :attribute a}))
[op e a v tx])) [op e a v tx]))
@ -138,34 +139,63 @@
;; n.b., this must not be symbolic -- it's inserted after we map idents -> entids. ;; n.b., this must not be symbolic -- it's inserted after we map idents -> entids.
[:db/add tx (db/entid db :db/txInstant) txInstant])) [:db/add tx (db/entid db :db/txInstant) txInstant]))
(defn ensure-entity-form [[op e a v & rest :as entity]] (defn ensure-entity-form [entity]
(cond (when-not (sequential? entity)
(not (sequential? entity))
(raise "Bad entity " entity ", should be sequential at this point" (raise "Bad entity " entity ", should be sequential at this point"
{:error :transact/bad-entity, :entity entity}) {:error :transact/bad-entity, :entity entity}))
(not (contains? #{:db/add :db/retract} op)) (let [[op] entity]
(raise "Unrecognized operation " op " expected one of :db/add :db/retract at this point" (case op
{:error :transact/bad-operation :entity entity }) (:db/add :db/retract)
(let [[_ e a v & rest] entity]
(cond
(nil? e)
(raise "Bad entity: nil e in " entity
{:error :transact/bad-entity :entity entity })
(nil? e) (nil? a)
(raise "Bad entity: nil e in " entity (raise "Bad entity: nil a in " entity
{:error :transact/bad-entity :entity entity }) {:error :transact/bad-entity :entity entity })
(nil? a) (nil? v)
(raise "Bad entity: nil a in " entity (raise "Bad entity: nil v in " entity
{:error :transact/bad-entity :entity entity }) {:error :transact/bad-entity :entity entity })
(nil? v) (some? rest)
(raise "Bad entity: nil v in " entity (raise "Bad entity: too long " entity
{:error :transact/bad-entity :entity entity }) {:error :transact/bad-entity :entity entity })))
(some? rest) :db.fn/retractAttribute
(raise "Bad entity: too long " entity (let [[_ e a & rest] entity]
{:error :transact/bad-entity :entity entity }) (cond
(nil? e)
(raise "Bad entity: nil e in " entity
{:error :transact/bad-entity :entity entity })
true (nil? a)
entity)) (raise "Bad entity: nil a in " entity
{:error :transact/bad-entity :entity entity })
(some? rest)
(raise "Bad entity: too long " entity
{:error :transact/bad-entity :entity entity })))
:db.fn/retractEntity
(let [[_ e & rest] entity]
(cond
(nil? e)
(raise "Bad entity: nil e in " entity
{:error :transact/bad-entity :entity entity })
(some? rest)
(raise "Bad entity: too long " entity
{:error :transact/bad-entity :entity entity })))
;; Default
(raise "Unrecognized operation " op " expected one of :db/add :db/retract :db/fn.retractAttribute :db/fn.retractEntity at this point"
{:error :transact/bad-operation :entity entity })))
entity)
(defn- tx-instant? [db [op e a & _]] (defn- tx-instant? [db [op e a & _]]
(and (= op :db/add) (and (= op :db/add)
@ -373,7 +403,8 @@
(go-pair (go-pair
(let [schema (db/schema db)] (let [schema (db/schema db)]
(doseq [[op e a v] (:entities report)] (doseq [[op e a v] (:entities report)]
(ds/ensure-valid-value schema a v))) (if (and e a v)
(ds/ensure-valid-value schema a v))))
report)) report))
(defn <transact-tx-data (defn <transact-tx-data

View file

@ -574,4 +574,211 @@
ExceptionInfo #"unique constraint" ExceptionInfo #"unique constraint"
(<? (d/<transact! conn [{:db/id (d/id-literal :db.part/user -1) :test/x 12345 :test/y 99999}]))))))) (<? (d/<transact! conn [{:db/id (d/id-literal :db.part/user -1) :test/x 12345 :test/y 99999}])))))))
(def retract-schema
[{:db/id (d/id-literal :db.part/user -1)
:db/ident :test/long
:db/cardinality :db.cardinality/many
:db/valueType :db.type/long
:db.install/_attribute :db.part/db}
{:db/id (d/id-literal :db.part/user -2)
:db/ident :test/fulltext
:db/cardinality :db.cardinality/many
:db/valueType :db.type/string
:db/fulltext true
:db.install/_attribute :db.part/db}
{:db/id (d/id-literal :db.part/user -3)
:db/ident :test/ref
:db/valueType :db.type/ref
:db.install/_attribute :db.part/db}])
(deftest-db test-retract-attribute conn
(let [tx0 (:tx (<? (d/<transact! conn retract-schema)))]
(testing "retractAttribute"
(let [report (<? (d/<transact! conn [{:db/id (d/id-literal :db.part/user -1) :test/long [12345 123456]}
{:db/id (d/id-literal :db.part/user -2) :test/ref (d/id-literal :db.part/user -1)}]))
eid1 (get-in report [:tempids (d/id-literal :db.part/user -1)])
eid2 (get-in report [:tempids (d/id-literal :db.part/user -2)])]
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid1 :test/long 12345]
[eid1 :test/long 123456]
[eid2 :test/ref eid1]}))
(testing "retractAttribute with no matching datoms succeeds"
(<? (d/<transact! conn [[:db.fn/retractAttribute eid1 :test/ref]]))
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid1 :test/long 12345]
[eid1 :test/long 123456]
[eid2 :test/ref eid1]})))
(testing "retractAttribute retracts datoms"
(let [{tx1 :tx} (<? (d/<transact! conn [[:db.fn/retractAttribute eid2 :test/ref]]))]
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid1 :test/long 12345]
[eid1 :test/long 123456]})))
(let [{tx2 :tx} (<? (d/<transact! conn [[:db.fn/retractAttribute eid1 :test/long]]))]
(is (= (<? (<datoms-after (d/db conn) tx0))
#{}))))))))
(deftest-db test-retract-attribute-multiple conn
(let [tx0 (:tx (<? (d/<transact! conn retract-schema)))]
(let [report (<? (d/<transact! conn [{:db/id (d/id-literal :db.part/user -1) :test/long [12345 123456]}
{:db/id (d/id-literal :db.part/user -1) :test/fulltext ["first fulltext value" "second fulltext value"]}]))
eid1 (get-in report [:tempids (d/id-literal :db.part/user -1)])]
(is (= (<? (<fulltext-values (d/db conn)))
[[1 "first fulltext value"]
[2 "second fulltext value"]]))
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid1 :test/fulltext 1]
[eid1 :test/fulltext 2]
[eid1 :test/long 12345]
[eid1 :test/long 123456]}))
(testing "multiple retractAttribute in one transaction"
(let [{tx1 :tx} (<? (d/<transact! conn [[:db.fn/retractAttribute eid1 :test/long]
[:db.fn/retractAttribute eid1 :test/fulltext]]))]
(is (= (<? (<datoms-after (d/db conn) tx0))
#{})))))))
(deftest-db test-retract-attribute-fulltext conn
(let [tx0 (:tx (<? (d/<transact! conn retract-schema)))]
(testing "retractAttribute, fulltext"
(let [report (<? (d/<transact! conn [{:db/id (d/id-literal :db.part/user -1) :test/fulltext ["first fulltext value" "second fulltext value"]}
{:db/id (d/id-literal :db.part/user -2) :test/ref (d/id-literal :db.part/user -1)}]))
eid1 (get-in report [:tempids (d/id-literal :db.part/user -1)])
eid2 (get-in report [:tempids (d/id-literal :db.part/user -2)])]
(is (= (<? (<fulltext-values (d/db conn)))
[[1 "first fulltext value"]
[2 "second fulltext value"]]))
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid1 :test/fulltext 1]
[eid1 :test/fulltext 2]
[eid2 :test/ref eid1]}))
(testing "retractAttribute retracts datoms, fulltext"
(let [{tx1 :tx} (<? (d/<transact! conn [[:db.fn/retractAttribute eid2 :test/ref]]))]
;; fulltext values are not purged.
(is (= (<? (<fulltext-values (d/db conn)))
[[1 "first fulltext value"]
[2 "second fulltext value"]]))
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid1 :test/fulltext 1]
[eid1 :test/fulltext 2]})))
(let [{tx2 :tx} (<? (d/<transact! conn [[:db.fn/retractAttribute eid1 :test/fulltext]]))]
;; fulltext values are not purged.
(is (= (<? (<fulltext-values (d/db conn)))
[[1 "first fulltext value"]
[2 "second fulltext value"]]))
(is (= (<? (<datoms-after (d/db conn) tx0))
#{}))))))))
(deftest-db test-retract-entity conn
(let [tx0 (:tx (<? (d/<transact! conn retract-schema)))]
(testing "retractEntity"
(let [report (<? (d/<transact! conn [{:db/id (d/id-literal :db.part/user -1) :test/long [12345 123456]}
{:db/id (d/id-literal :db.part/user -2) :test/ref (d/id-literal :db.part/user -1)}
{:db/id (d/id-literal :db.part/user -3) :test/long 0xdeadbeef}]))
eid1 (get-in report [:tempids (d/id-literal :db.part/user -1)])
eid2 (get-in report [:tempids (d/id-literal :db.part/user -2)])
eid3 (get-in report [:tempids (d/id-literal :db.part/user -3)])]
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid1 :test/long 12345]
[eid1 :test/long 123456]
[eid2 :test/ref eid1]
[eid3 :test/long 0xdeadbeef]}))
(testing "retractEntity with no matching datoms succeeds"
(<? (d/<transact! conn [[:db.fn/retractEntity 0xdeadbeef]]))
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid1 :test/long 12345]
[eid1 :test/long 123456]
[eid2 :test/ref eid1]
[eid3 :test/long 0xdeadbeef]})))
(testing "retractEntity retracts datoms"
(let [{tx1 :tx} (<? (d/<transact! conn [[:db.fn/retractEntity eid3]]))]
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid1 :test/long 12345]
[eid1 :test/long 123456]
[eid2 :test/ref eid1]}))))
(testing "retractEntity retracts datoms and references"
(let [{tx2 :tx} (<? (d/<transact! conn [[:db.fn/retractEntity eid1]]))]
(is (= (<? (<datoms-after (d/db conn) tx0))
;; [eid2 :test/ref eid1] is gone, since the ref eid1 is gone.
#{}))))))))
(deftest-db test-retract-entity-multiple conn
(let [tx0 (:tx (<? (d/<transact! conn retract-schema)))]
(let [report (<? (d/<transact! conn [{:db/id (d/id-literal :db.part/user -1) :test/long [12345 123456]}
{:db/id (d/id-literal :db.part/user -2) :test/fulltext ["first fulltext value" "second fulltext value"]}]))
eid1 (get-in report [:tempids (d/id-literal :db.part/user -1)])
eid2 (get-in report [:tempids (d/id-literal :db.part/user -2)])]
(is (= (<? (<fulltext-values (d/db conn)))
[[1 "first fulltext value"]
[2 "second fulltext value"]]))
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid2 :test/fulltext 1]
[eid2 :test/fulltext 2]
[eid1 :test/long 12345]
[eid1 :test/long 123456]}))
(testing "multiple retractEntity in one transaction"
(let [{tx1 :tx} (<? (d/<transact! conn [[:db.fn/retractEntity eid1]
[:db.fn/retractEntity eid2]]))]
(is (= (<? (<datoms-after (d/db conn) tx0))
#{})))))))
(deftest-db test-retract-entity-fulltext conn
(let [tx0 (:tx (<? (d/<transact! conn retract-schema)))]
(testing "retractEntity, fulltext"
(let [report (<? (d/<transact! conn [{:db/id (d/id-literal :db.part/user -1) :test/fulltext ["first fulltext value" "second fulltext value"]}
{:db/id (d/id-literal :db.part/user -2) :test/ref (d/id-literal :db.part/user -1)}
{:db/id (d/id-literal :db.part/user -3) :test/fulltext "other fulltext value"}]))
eid1 (get-in report [:tempids (d/id-literal :db.part/user -1)])
eid2 (get-in report [:tempids (d/id-literal :db.part/user -2)])
eid3 (get-in report [:tempids (d/id-literal :db.part/user -3)])]
(is (= (<? (<fulltext-values (d/db conn)))
[[1 "first fulltext value"]
[2 "second fulltext value"]
[3 "other fulltext value"]]))
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid1 :test/fulltext 1]
[eid1 :test/fulltext 2]
[eid2 :test/ref eid1]
[eid3 :test/fulltext 3]}))
(testing "retractEntity with no matching datoms succeeds, fulltext"
(<? (d/<transact! conn [[:db.fn/retractEntity 0xdeadbeef]]))
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid1 :test/fulltext 1]
[eid1 :test/fulltext 2]
[eid2 :test/ref eid1]
[eid3 :test/fulltext 3]})))
(testing "retractEntity retracts datoms, fulltext"
(let [{tx1 :tx} (<? (d/<transact! conn [[:db.fn/retractEntity eid3]]))]
;; fulltext values are not purged.
(is (= (<? (<fulltext-values (d/db conn)))
[[1 "first fulltext value"]
[2 "second fulltext value"]
[3 "other fulltext value"]]))
(is (= (<? (<datoms-after (d/db conn) tx0))
#{[eid1 :test/fulltext 1]
[eid1 :test/fulltext 2]
[eid2 :test/ref eid1]}))))
(testing "retractEntity retracts datoms and references, fulltext"
(let [{tx2 :tx} (<? (d/<transact! conn [[:db.fn/retractEntity eid1]]))]
(is (= (<? (<datoms-after (d/db conn) tx0))
;; [eid2 :test/ref eid1] is gone, since the ref eid1 is gone.
#{}))))))))
#_ (time (t/run-tests)) #_ (time (t/run-tests))