diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e516266..9ff76656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/datascript/db.cljc b/src/datascript/db.cljc index 3edcef3b..ffac4a4d 100644 --- a/src/datascript/db.cljc +++ b/src/datascript/db.cljc @@ -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] diff --git a/test/datascript/test/upsert.cljc b/test/datascript/test/upsert.cljc index d5c1b528..9340b1bb 100644 --- a/test/datascript/test/upsert.cljc +++ b/test/datascript/test/upsert.cljc @@ -10,164 +10,206 @@ (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}]))))) @@ -175,26 +217,26 @@ ;; 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]}] @@ -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]] @@ -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]