We are able to do upserting a single entity, having a tuple key, mad of :db.type/ref
attrs, IF we use concrete entity IDs:
[{:ref/x 79164837199951,
:ref/y 79164837199952,
:key [79164837199951 79164837199952]}]
However, if we are trying to upsert the referenced entities at the same time, hence using tempids, instead of concrete entity IDs, we are getting :db.error/unique-conflict
:
[{:db/id "x", :x/id "x"}
{:db/id "y", :y/id "y"}
{:ref/x "x", :ref/y "y", :key ["x" "y"]}]
Q1. Is this expected behaviour?
Q2. Is it some intentional restriction?
Q3. Are there some use-cases, where this approach would be ambiguous?
This seems like a bug or at least a missing feature, unless I’m missing something.
Without this capability, we had to break the atomicity of our transaction, by splitting it into 2 transactions.
Related literature
- :db.unique/identity does not work for tuple attributes
- Upsert behavior with composite tuple key
- Troubles with upsert on composite tuples
Complete example
Requires Datomic dev-local
.
(ns repl.composite-ref-key
(:require [datomic.client.api :as d]
[clojure.pprint :refer [pprint]]))
(defn ? [x] (println) (pprint x) x)
(defn tx! "Pretty-printing d/transact" [conn tx-data]
(let [?? (if (-> tx-data meta :silent) identity ?)]
(-> conn
(d/transact {:tx-data (?? tx-data)})
(doto (-> (dissoc :db-before :db-after)
(update :tx-data (partial into []))
??)))))
(defn mk-conn [schema]
(let [db-ref {:db (str (gensym "db-"))}]
(-> {:server-type :dev-local
:storage-dir :mem
:system (str (gensym "in-mem-sys-"))}
d/client
(doto (d/create-database db-ref))
(d/connect db-ref)
(doto (tx! schema)))))
(def schema
^:silent
[{:db/ident :x/id
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity}
{:db/ident :y/id
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity}
{:db/ident :ref/x
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/one}
{:db/ident :ref/y
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/one}
{:db/ident :key
:db/valueType :db.type/tuple
:db/tupleAttrs [:ref/x :ref/y]
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity}
{:db/ident :attr
:db/valueType :db.type/long
:db/cardinality :db.cardinality/one}])
(comment
;;; Using concrete entity IDs works
(let [conn (mk-conn schema)
txr0 (tx! conn [{:db/id "x" :x/id "x"}
{:db/id "y" :y/id "y"}])
{:strs [x y]} (-> txr0 :tempids)
ent {:db/id "ent"
:ref/x x
:ref/y y
:key [x y]}]
(tx! conn [(-> ent (merge {:attr 1}))])
(tx! conn [(-> ent (merge {:attr 2}))]))
;[{:db/id "x", :x/id "x"} {:db/id "y", :y/id "y"}]
;
;{:tx-data
; [#datom[13194139533319 50 #inst "2023-02-15T03:24:33.321-00:00" 13194139533319 true]
; #datom[101155069755471 73 "x" 13194139533319 true]
; #datom[101155069755472 74 "y" 13194139533319 true]],
; :tempids {"x" 101155069755471, "y" 101155069755472}}
;
;[{:db/id "ent",
; :ref/x 101155069755471,
; :ref/y 101155069755472,
; :key [101155069755471 101155069755472],
; :attr 1}]
;
;{:tx-data
; [#datom[13194139533320 50 #inst "2023-02-15T03:24:33.323-00:00" 13194139533320 true]
; #datom[87960930222161 75 101155069755471 13194139533320 true]
; #datom[87960930222161 76 101155069755472 13194139533320 true]
; #datom[87960930222161 78 1 13194139533320 true]
; #datom[87960930222161 77 [101155069755471 101155069755472] 13194139533320 true]],
; :tempids {"ent" 87960930222161}}
;
;[{:db/id "ent",
; :ref/x 101155069755471,
; :ref/y 101155069755472,
; :key [101155069755471 101155069755472],
; :attr 2}]
;
;{:tx-data
; [#datom[13194139533321 50 #inst "2023-02-15T03:24:33.325-00:00" 13194139533321 true]
; #datom[87960930222161 78 2 13194139533321 true]
; #datom[87960930222161 78 1 13194139533321 false]],
; :tempids {"ent" 87960930222161}}
;;; Using tempids fails
(let [conn (mk-conn schema)
ent {:db/id "ent"
:ref/x {:db/id "x" :x/id "x"}
:ref/y {:db/id "y" :y/id "y"}
:key ["x" "y"]}]
(tx! conn [(-> ent (merge {:attr 1}))])
(tx! conn [(-> ent (merge {:attr 2}))]))
;[{:db/id "ent",
; :ref/x {:db/id "x", :x/id "x"},
; :ref/y {:db/id "y", :y/id "y"},
; :key ["x" "y"],
; :attr 1}]
;
;{:tx-data
; [#datom[13194139533319 50 #inst "2023-02-15T03:25:49.663-00:00" 13194139533319 true]
; #datom[83562883711055 75 83562883711056 13194139533319 true]
; #datom[83562883711056 73 "x" 13194139533319 true]
; #datom[83562883711055 76 83562883711057 13194139533319 true]
; #datom[83562883711057 74 "y" 13194139533319 true]
; #datom[83562883711055 78 1 13194139533319 true]
; #datom[83562883711055 77 [83562883711056 83562883711057] 13194139533319 true]],
; :tempids
; {"x" 83562883711056, "y" 83562883711057, "ent" 83562883711055}}
;
;[{:db/id "ent",
; :ref/x {:db/id "x", :x/id "x"},
; :ref/y {:db/id "y", :y/id "y"},
; :key ["x" "y"],
; :attr 2}]
;Execution error (ExceptionInfo) at datomic.core.error/raise (error.clj:55).
;:db.error/unique-conflict Unique conflict: :key,
; value: [83562883711056 83562883711057]
; already held by: 83562883711055
; asserted for: 101155069755474
(let [conn (mk-conn schema)
x-ent {:db/id "x" :x/id "x"}
y-ent {:db/id "y" :y/id "y"}
ent {:db/id "ent"
:ref/x "x"
:ref/y "y"
:key ["x" "y"]}]
(tx! conn [x-ent y-ent (-> ent (merge {:attr 1}))])
(tx! conn [x-ent y-ent (-> ent (merge {:attr 2}))]))
; same end result as above
)