tonsky / datascript

Immutable database and Datalog query engine for Clojure, ClojureScript and JS
Eclipse Public License 1.0
5.5k stars 309 forks source link

Upserting by Unique Composite Tuple works in Datomic but not Datascript #400

Closed caleb closed 3 years ago

caleb commented 3 years ago

Upserting by unique composite tuple doesn't work in Datascript as it does in Datomic.

It was not immediately clear to me how to upsert by unique composite tuple in Datomic by the documentation, but I found this forum post about it:

https://forum.datomic.com/t/db-unique-identity-does-not-work-for-tuple-attributes/1072

The key part is the last post by jaret:

If you do want to identify an entity by a unique key, you must indeed specify that unique key (not its constituents).

Datascript:

(ns ds.core
  (:require [datascript.core :as ds]))

;;
;; Upsert by Unique Composite Tuple
;;
(let [schema {:one+two {:db/tupleAttrs [:one :two]
                        :db/unique :db.unique/identity}}
      conn (ds/create-conn schema)]

  (ds/transact! conn [{:db/id "tmpid"
                       :one+two ["one" "two"]
                       :one "one"
                       :two "two"}
                      {:db/id "tmpid"
                       :one+two ["one" "two"]
                       :one "one"
                       :two "two"}])

  (ds/transact! conn [{:db/id "tmpid"
                       :one+two ["one" "two"]
                       :one "one"
                       :two "two"}
                      {:db/id "tmpid"
                       :one+two ["one" "two"]
                       :one "one"
                       :two "two"}])

  (ds/q '[:find [(pull ?e [*]) ...]
          :where
          [?e :one _]]
        @conn))

Gives this error:

Unhandled clojure.lang.ExceptionInfo
   Can’t modify tuple attrs directly: [:db/add 1 :one+two ["one" "two"]]
   {:error :transact/syntax, :tx-data [:db/add 1 :one+two ["one" "two"]]}

But in Datomic, this works:

(ns ds.datomic
  (:require [datomic.client.api :as d]))

(let [client (d/client {:server-type :dev-local :system "dev"})]
  (d/delete-database client {:db-name "tuples"})
  (d/create-database client {:db-name "tuples"})

  (let [conn (d/connect client {:db-name "tuples"})
        schema [{:db/ident :one
                 :db/valueType :db.type/string
                 :db/cardinality :db.cardinality/one}
                {:db/ident :two
                 :db/valueType :db.type/string
                 :db/cardinality :db.cardinality/one}
                {:db/ident :one+two
                 :db/valueType :db.type/tuple
                 :db/tupleAttrs [:one :two]
                 :db/cardinality :db.cardinality/one
                 :db/unique :db.unique/identity}]]
    (d/transact conn {:tx-data schema})

    (d/transact conn {:tx-data [{:db/id "tmpid"
                                 :one "one"
                                 :two "two"
                                 :one+two ["one" "two"]}
                                {:db/id "tmpid"
                                 :one "one"
                                 :two "two"
                                 :one+two ["one" "two"]}]})

    (d/transact conn {:tx-data [{:db/id "tmpid"
                                 :one "one"
                                 :two "two"
                                 :one+two ["one" "two"]}
                                {:db/id "tmpid"
                                 :one "one"
                                 :two "two"
                                 :one+two ["one" "two"]}]})
    (d/q '[:find (pull ?e [*])
           :where
           [?e :one _]]
         (d/db conn))))

Yields:

[[{:db/id 79164837199948,
   :one "one",
   :two "two",
   :one+two ["one" "two"]}]]
tonsky commented 3 years ago

Thanks for reporting, I’ll take a look at it next week

tonsky commented 3 years ago

Quick answer here would be: you can’t specify tuples when creating new entities, but you can use them to reference existing ones (upserts):

(let [conn (ds/create-conn {:one+two {:db/tupleAttrs [:one :two]
                                      :db/unique :db.unique/identity}})]
  (ds/transact! conn
    [{:one "one" :two "two" :one+two ["one" "two"]}]) ;; doesn’t work

  (ds/transact! conn
    [{:one "one" :two "two"}]) ;; works

  (ds/transact! conn
    [{:one "one" :two "two" :one+two ["one" "two"]}])) ;; now this works too, because :one+two refers to an existing entity

  (ds/transact! conn
    [{:one+two ["one" "two"]}])) ;; works too

Long answer: this is probably not ideal, because it makes upserts a little trickier. After all, idea of upsert is that you write a single transact that works in both cases, when inserting and when updating.

I’ll see if I can do anything about it. For now, you can try not specifying tuples when creating new entities.

tonsky commented 3 years ago

I think I found a way to cover your case. If the tuple content fully matches values already in DB, there will be no error anymore. But if it is somehow different (e.g. values are not in DB yet, or have different value), the same error will be thrown. Check out dd584b4, release coming soon.

caleb commented 3 years ago

Thanks! This version worked in my test playground. I will give it a try in my project to see how it works.