Skip to content

Commit

Permalink
Fixed tempid in unique refs (closes #464)
Browse files Browse the repository at this point in the history
  • Loading branch information
tonsky committed Apr 24, 2024
1 parent 4cdb43c commit 009c628
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 83 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

- Implement “constant substitution” optimization for queries #462
- Fixed :max-eid for dangling entities during reader-based serialization #463
- Fixed tempid in unique refs #464

# 1.6.3

Expand Down
7 changes: 6 additions & 1 deletion src/datascript/db.cljc
Expand Up @@ -1377,7 +1377,12 @@
[db entity]
(if-some [idents (not-empty (-attrs-by db :db.unique/identity))]
(let [resolve (fn [a v]
(:e (first (-datoms db :avet a v nil nil))))
(cond
(not (ref? db a))
(:e (first (-datoms db :avet a v nil nil)))

(not (tempid? v))
(:e (first (-datoms db :avet a (entid db v) nil nil)))))
split (fn [a vs]
(reduce
(fn [acc v]
Expand Down
206 changes: 124 additions & 82 deletions test/datascript/test/upsert.cljc
Expand Up @@ -10,191 +10,233 @@
(def Throwable js/Error))

(deftest test-upsert
(let [db (d/db-with (d/empty-db {:name { :db/unique :db.unique/identity }
:email { :db/unique :db.unique/identity }
:slugs { :db/unique :db.unique/identity
:db/cardinality :db.cardinality/many }})
[{:db/id 1 :name "Ivan" :email "@1"}
{:db/id 2 :name "Petr" :email "@2"}])
touched (fn [tx e] (into {} (d/touch (d/entity (:db-after tx) e))))
tempids (fn [tx] (dissoc (:tempids tx) :db/current-tx))]
(let [ivan {:db/id 1 :name "Ivan" :email "@1"}
petr {:db/id 2 :name "Petr" :email "@2" :ref 3}
dima {:db/id 3 :name "Dima" :email "@3" :ref 4}
olga {:db/id 4 :name "Olga" :email "@4" :ref 1}
db (d/db-with (d/empty-db {:name {:db/unique :db.unique/identity}
:email {:db/unique :db.unique/identity}
:slugs {:db/unique :db.unique/identity
:db/cardinality :db.cardinality/many}
:ref {:db/unique :db.unique/identity
:db/type :db.type/ref}})
[ivan petr dima olga])
pull (fn [tx e]
(d/pull (:db-after tx) ['* {[:ref :xform #(:db/id %)] [:db/id]}] e))
tempids (fn [tx]
(dissoc (:tempids tx) :db/current-tx))]
(testing "upsert, no tempid"
(let [tx (d/with db [{:name "Ivan" :age 35}])]
(is (= (touched tx 1)
{:name "Ivan" :email "@1" :age 35}))
(is (= {:db/id 1 :name "Ivan" :email "@1" :age 35}
(pull tx 1)))
(is (= (tempids tx)
{}))))
{}))))

(testing "upsert by 2 attrs, no tempid"
(let [tx (d/with db [{:name "Ivan" :email "@1" :age 35}])]
(is (= (touched tx 1)
{:name "Ivan" :email "@1" :age 35}))
(is (= {:db/id 1 :name "Ivan" :email "@1" :age 35}
(pull tx 1)))
(is (= (tempids tx)
{}))))
{}))))

(testing "upsert with tempid"
(let [tx (d/with db [{:db/id -1 :name "Ivan" :age 35}])]
(is (= (touched tx 1)
{:name "Ivan" :email "@1" :age 35}))
(is (= {:db/id 1 :name "Ivan" :email "@1" :age 35}
(pull tx 1)))
(is (= (tempids tx)
{-1 1}))))
{-1 1}))))

(testing "upsert with string tempid"
(let [tx (d/with db [{:db/id "1" :name "Ivan" :age 35}
[:db/add "2" :name "Oleg"]
[:db/add "2" :email "@2"]])]
(is (= (touched tx 1)
{:name "Ivan" :email "@1" :age 35}))
(is (= (touched tx 2)
{:name "Oleg" :email "@2"}))
(is (= {:db/id 1 :name "Ivan" :email "@1" :age 35}
(pull tx 1)))
(is (= {:db/id 2 :name "Oleg" :email "@2" :ref 3}
(pull tx 2)))
(is (= (tempids tx)
{"1" 1
"2" 2}))))
{"1" 1
"2" 2}))))

(testing "upsert by 2 attrs with tempid"
(let [tx (d/with db [{:db/id -1 :name "Ivan" :email "@1" :age 35}])]
(is (= (touched tx 1)
{:name "Ivan" :email "@1" :age 35}))
(is (= {:db/id 1 :name "Ivan" :email "@1" :age 35}
(pull tx 1)))
(is (= (tempids tx)
{-1 1}))))
{-1 1}))))

(testing "upsert to two entities, resolve to same tempid"
(let [tx (d/with db [{:db/id -1 :name "Ivan" :age 35}
{:db/id -1 :name "Ivan" :age 36}])]
(is (= (touched tx 1)
{:name "Ivan" :email "@1" :age 36}))
(is (= {:db/id 1 :name "Ivan" :email "@1" :age 36}
(pull tx 1)))
(is (= (tempids tx)
{-1 1}))))
{-1 1}))))

(testing "upsert to two entities, two tempids"
(let [tx (d/with db [{:db/id -1 :name "Ivan" :age 35}
{:db/id -2 :name "Ivan" :age 36}])]
(is (= (touched tx 1)
{:name "Ivan" :email "@1" :age 36}))
(is (= {:db/id 1 :name "Ivan" :email "@1" :age 36}
(pull tx 1)))
(is (= (tempids tx)
{-1 1, -2 1}))))
{-1 1, -2 1}))))

(testing "upsert with existing id"
(let [tx (d/with db [{:db/id 1 :name "Ivan" :age 35}])]
(is (= (touched tx 1)
{:name "Ivan" :email "@1" :age 35}))
(is (= {:db/id 1 :name "Ivan" :email "@1" :age 35}
(pull tx 1)))
(is (= (tempids tx)
{}))))
{}))))

(testing "upsert by 2 attrs with existing id"
(let [tx (d/with db [{:db/id 1 :name "Ivan" :email "@1" :age 35}])]
(is (= (touched tx 1)
{:name "Ivan" :email "@1" :age 35}))
(is (= {:db/id 1 :name "Ivan" :email "@1" :age 35}
(pull tx 1)))
(is (= (tempids tx)
{}))))
{}))))

(testing "upsert by 2 attrs with existing id as lookup ref"
(let [tx (d/with db [{:db/id [:name "Ivan"] :name "Ivan" :email "@1" :age 35}])]
(is (= (touched tx 1)
{:name "Ivan" :email "@1" :age 35}))
(is (= {:db/id 1 :name "Ivan" :email "@1" :age 35}
(pull tx 1)))
(is (= (tempids tx)
{}))))
{}))))

(testing "upsert conflicts with existing id"
(is (thrown-with-msg? Throwable #"Conflicting upsert: \[:name \"Ivan\"\] resolves to 1, but entity already has :db/id 2"
(d/with db [{:db/id 2 :name "Ivan" :age 36}]))))
(d/with db [{:db/id 2 :name "Ivan" :age 36}]))))

(testing "upsert conflicts with non-existing id"
(is (thrown-with-msg? Throwable #"Conflicting upsert: \[:name \"Ivan\"\] resolves to 1, but entity already has :db/id 3"
(d/with db [{:db/id 3 :name "Ivan" :age 36}]))))
(is (thrown-with-msg? Throwable #"Conflicting upsert: \[:name \"Ivan\"\] resolves to 1, but entity already has :db/id 5"
(d/with db [{:db/id 5 :name "Ivan" :age 36}]))))

(testing "upsert by non-existing value resolves as update"
(let [tx (d/with db [{:name "Ivan" :email "@3" :age 35}])]
(is (= (touched tx 1)
{:name "Ivan" :email "@3" :age 35}))
(let [tx (d/with db [{:name "Ivan" :email "@5" :age 35}])]
(is (= {:db/id 1 :name "Ivan" :email "@5" :age 35}
(pull tx 1)))
(is (= (tempids tx)
{}))))
{}))))

(testing "upsert by 2 conflicting fields"
(is (thrown-with-msg? Throwable #"Conflicting upserts: \[:name \"Ivan\"\] resolves to 1, but \[:email \"@2\"\] resolves to 2"
(d/with db [{:name "Ivan" :email "@2" :age 35}]))))
(d/with db [{:name "Ivan" :email "@2" :age 35}]))))

(testing "upsert over intermediate db"
(let [tx (d/with db [{:name "Igor" :age 35}
{:name "Igor" :age 36}])]
(is (= (touched tx 3)
{:name "Igor" :age 36}))
(is (= {:db/id 5 :name "Igor" :age 36}
(pull tx 5)))
(is (= (tempids tx)
{3 3}))))
{5 5}))))

(testing "upsert over intermediate db, tempids"
(let [tx (d/with db [{:db/id -1 :name "Igor" :age 35}
{:db/id -1 :name "Igor" :age 36}])]
(is (= (touched tx 3)
{:name "Igor" :age 36}))
(is (= {:db/id 5 :name "Igor" :age 36}
(pull tx 5)))
(is (= (tempids tx)
{-1 3}))))
{-1 5}))))

(testing "upsert over intermediate db, different tempids"
(let [tx (d/with db [{:db/id -1 :name "Igor" :age 35}
{:db/id -2 :name "Igor" :age 36}])]
(is (= (touched tx 3)
{:name "Igor" :age 36}))
(is (= {:db/id 5 :name "Igor" :age 36}
(pull tx 5)))
(is (= (tempids tx)
{-1 3, -2 3}))))
{-1 5, -2 5}))))

(testing "upsert and :current-tx conflict"
(is (thrown-with-msg? Throwable #"Conflicting upsert: \[:name \"Ivan\"\] resolves to 1, but entity already has :db/id \d+"
(d/with db [{:db/id :db/current-tx :name "Ivan" :age 35}]))))
(d/with db [{:db/id :db/current-tx :name "Ivan" :age 35}]))))

(testing "upsert of unique, cardinality-many values"
(let [tx (d/with db [{:name "Ivan" :slugs "ivan1"}
{:name "Petr" :slugs "petr1"}])
tx2 (d/with (:db-after tx) [{:name "Ivan" :slugs ["ivan1" "ivan2"]}])]
(is (= (touched tx 1)
{:name "Ivan" :email "@1" :slugs #{"ivan1"}}))
(is (= (touched tx2 1)
{:name "Ivan" :email "@1" :slugs #{"ivan1" "ivan2"}}))
(is (= {:db/id 1 :name "Ivan" :email "@1" :slugs ["ivan1"]}
(pull tx 1)))
(is (= {:db/id 1 :name "Ivan" :email "@1" :slugs ["ivan1" "ivan2"]}
(pull tx2 1)))
(is (thrown-with-msg? Throwable #"Conflicting upserts:"
(d/with (:db-after tx) [{:slugs ["ivan1" "petr1"]}])))))

(testing "upsert by ref"
(let [tx (d/with db [{:ref 3 :age 36}])]
(is (= {:db/id 2 :name "Petr" :email "@2" :ref 3 :age 36}
(pull tx 2))))
(let [tx (d/with db [{:ref 4 :age 37}])]
(is (= {:db/id 3 :name "Dima" :email "@3" :ref 4 :age 37}
(pull tx 3))))
(let [tx (d/with db [{:ref 1 :age 38}])]
(is (= {:db/id 4 :name "Olga" :email "@4" :ref 1 :age 38}
(pull tx 4)))))

(testing "upsert by lookup ref"
(let [tx (d/with db [{:ref [:name "Dima"] :age 36}])]
(is (= {:db/id 2 :name "Petr" :email "@2" :ref 3 :age 36}
(pull tx 2))))
(let [tx (d/with db [{:ref [:name "Olga"] :age 37}])]
(is (= {:db/id 3 :name "Dima" :email "@3" :ref 4 :age 37}
(pull tx 3))))
(let [tx (d/with db [{:ref [:name "Ivan"] :age 38}])]
(is (= {:db/id 4 :name "Olga" :email "@4" :ref 1 :age 38}
(pull tx 4)))))

;; https://github.com/tonsky/datascript/issues/464
(testing "not upsert by ref"
(let [tx (d/with db [{:db/id -1 :name "Igor"}
{:db/id -2 :name "Anna" :ref -1}])]
(is (= {:db/id 5 :name "Igor"} (pull tx 5)))
(is (= {:db/id 6 :name "Anna" :ref 5} (pull tx 6))))

(let [tx (d/with db [{:db/id "A" :name "Igor"}
{:db/id "B" :name "Anna" :ref "A"}])]
(is (= {:db/id 5 :name "Igor"} (pull tx 5)))
(is (= {:db/id 6 :name "Anna" :ref 5} (pull tx 6)))))

))


(deftest test-redefining-ids
(let [db (-> (d/empty-db {:name { :db/unique :db.unique/identity }})
(d/db-with [{:db/id -1 :name "Ivan"}]))]
(d/db-with [{:db/id -1 :name "Ivan"}]))]
(let [tx (d/with db [{:db/id -1 :age 35}
{:db/id -1 :name "Ivan" :age 36}])]
(is (= #{[1 :age 36] [1 :name "Ivan"]}
(tdc/all-datoms (:db-after tx))))
(tdc/all-datoms (:db-after tx))))
(is (= {-1 1, :db/current-tx (+ d/tx0 2)}
(:tempids tx)))))
(:tempids tx)))))

(let [db (-> (d/empty-db {:name { :db/unique :db.unique/identity }})
(d/db-with [{:db/id -1 :name "Ivan"}
{:db/id -2 :name "Oleg"}]))]
(d/db-with [{:db/id -1 :name "Ivan"}
{:db/id -2 :name "Oleg"}]))]
(is (thrown-with-msg? Throwable #"Conflicting upsert: -1 resolves both to 1 and 2"
(d/with db [{:db/id -1 :name "Ivan" :age 35}
{:db/id -1 :name "Oleg" :age 36}])))))

;; https://github.com/tonsky/datascript/issues/285
(deftest test-retries-order
(let [db (-> (d/empty-db {:name {:db/unique :db.unique/identity}})
(d/db-with [[:db/add -1 :age 42]
[:db/add -2 :likes "Pizza"]
[:db/add -1 :name "Bob"]
[:db/add -2 :name "Bob"]]))]
(d/db-with [[:db/add -1 :age 42]
[:db/add -2 :likes "Pizza"]
[:db/add -1 :name "Bob"]
[:db/add -2 :name "Bob"]]))]
(is (= {:db/id 1, :name "Bob", :likes "Pizza", :age 42}
(tdc/entity-map db 1))))
(tdc/entity-map db 1))))

(let [db (-> (d/empty-db {:name {:db/unique :db.unique/identity}})
(d/db-with [[:db/add -1 :age 42]
[:db/add -2 :likes "Pizza"]
[:db/add -2 :name "Bob"]
[:db/add -1 :name "Bob"]]))]
(d/db-with [[:db/add -1 :age 42]
[:db/add -2 :likes "Pizza"]
[:db/add -2 :name "Bob"]
[:db/add -1 :name "Bob"]]))]
(is (= {:db/id 2, :name "Bob", :likes "Pizza", :age 42}
(tdc/entity-map db 2)))))
(tdc/entity-map db 2)))))

;; https://github.com/tonsky/datascript/issues/403
(deftest test-upsert-string-tempid-ref
(let [db (-> (d/empty-db {:name {:db/unique :db.unique/identity}
:ref {:db/valueType :db.type/ref}})
(d/db-with [{:name "Alice"}]))
(d/db-with [{:name "Alice"}]))
expected #{[1 :name "Alice"]
[2 :age 36]
[2 :ref 1]}]
Expand All @@ -213,7 +255,7 @@

(deftest test-vector-upsert
(let [db (-> (d/empty-db {:name {:db/unique :db.unique/identity}})
(d/db-with [{:db/id -1, :name "Ivan"}]))]
(d/db-with [{:db/id -1, :name "Ivan"}]))]
(are [tx res] (= res (tdc/all-datoms (d/db-with db tx)))
[[:db/add -1 :name "Ivan"]
[:db/add -1 :age 12]]
Expand All @@ -224,8 +266,8 @@
#{[1 :age 12] [1 :name "Ivan"]}))

(let [db (-> (d/empty-db {:name { :db/unique :db.unique/identity }})
(d/db-with [[:db/add -1 :name "Ivan"]
[:db/add -2 :name "Oleg"]]))]
(d/db-with [[:db/add -1 :name "Ivan"]
[:db/add -2 :name "Oleg"]]))]
(is (thrown-with-msg? Throwable #"Conflicting upsert: -1 resolves both to 1 and 2"
(d/with db [[:db/add -1 :name "Ivan"]
[:db/add -1 :age 35]
Expand Down

0 comments on commit 009c628

Please sign in to comment.