Upsert tupleAttrs containing :db.type/ref, using tempids

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

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

  )

+1 on this, I’ve run into the same issue. Instead of breaking atomicity I worked around it by denormalizing some data into the tuple. This really seems like a bug.

This is the obvious way to model identity for reified relations so it’s pretty unfortunate that this doesn’t work.