Fork me on GitHub
#datomic
<
2022-05-05
>
zalky14:05:52

Hey all: is there an easy way from the repl to see what version of the datomic peer is running?

Ivar Refsdal15:05:51

Is that an on-prem in-process peer library?

zalky16:05:31

@UGJE0MM0W, yes, on-prem in process.

Ivar Refsdal17:05:36

How about:

(some->> (io/resource "META-INF/maven/com.datomic/datomic-pro/pom.xml")
           (slurp)
           (str/split-lines)
           (filter #(str/includes? % "<version>"))
           (first))
=> " <version>1.0.6397</version>"
yeah, that's pretty ugly...

zalky17:05:38

Brilliant, worked like a charm, thanks!

zalky18:05:23

I was just experimenting on some deps aliases and wanted to confirm that the version of datomic I expected to load was in fact the one that was being loaded.

Ivar Refsdal18:05:52

Right :thumbsup: Sometimes there is also pom.properties available. If I need something like this I just unzip -p the-jar and grep for pom https://github.com/metosin/jsonista/issues/22

Ivar Refsdal18:05:18

(and then I know what file to look for)

Ivar Refsdal15:05:23

For on-prem version 1.0.6397 (latest) it seems that db/cas does not resolve tempids that are strings, only explicit (datomic.api/tempid ..). Is this a bug? Could it be solved? Should I report it to support? Reproduced in https://gist.github.com/ivarref/98537d7393d4141fb4dfb2a213756404.

favila16:05:11

The root cause I think is that d/entid works for record tempids (resolves to a negative number, which is the lowest-level representation), but not for string tempids. It’s not clear how it could because strings don’t have enough info in them.

Ivar Refsdal17:05:51

Thanks, but I didn't quite understand your answer.. It works (as expected) to transact:

[[:db/add "ent" :e/version 1]
 {:db/id "ent" :e/id "a" :e/info "a"}]
But
[[:db/cas "ent" :e/version nil 1]
 {:db/id "ent" :e/id "a" :e/info "a"}]
fails. I fail to see why :db/cas shouldn't work, but :db/add works.

Ivar Refsdal17:05:25

Here is another case that fails, but shouldn't:

(deftest nil-test
  (let [tempid (d/tempid :db.part/user)
        {:keys [db-after]} @(d/transact *conn* [[:db/cas tempid :e/version nil 1]
                                                {:db/id tempid :e/id "a" :e/info "1"}])]
    (is (= #:e{:id "a" :version 1 :info "1"} (d/pull db-after [:e/id :e/version :e/info] [:e/id "a"])))
    (let [tempid (d/tempid :db.part/user)
          ; The following transaction success, though it shouldn't:
          {:keys [db-after]} @(d/transact *conn* [[:db/cas tempid :e/version nil 2]
                                                  {:db/id tempid :e/id "a" :e/info "2"}])]
      (is (= #:e{:id "a" :version 2 :info "2"} (d/pull db-after [:e/id :e/version :e/info] [:e/id "a"]))))))
For me it appears that the tempid resolving for :db/cas is broken

Ivar Refsdal17:05:13

actually, it doesn't fails, it just fails to assert that :e/version does not exist.

favila17:05:01

:db/add doesn’t have to read anything; :db/cas has to read

favila17:05:03

So when it gets a tempid, it does not know yet what it resolves to, so it can’t make a read

Ivar Refsdal17:05:31

Shouldn't it complain / throw an exception if it is an invalid state/operation?

favila17:05:08

I think cas should reject any tempid

favila17:05:45

but, the reason it doesn’t for tempids is because d/entid resolves to a long, which works for entity lookup (it just won’t find any data).

favila18:05:20

Try it: (d/entity db the-tempid)

favila18:05:30

or (d/entid db the-tempid)

favila18:05:34

doesn’t work for strings

Ivar Refsdal18:05:25

Hm, well cas does accept tempids

Ivar Refsdal18:05:42

How would one assert that an upsertable entity does not have an attribute set?

Ivar Refsdal18:05:55

As far as I understand it: db/cas accepts d/tempid as entity and nil as old value. This will cause it to write the new value, regardless if the attribute was already set. For any non-nil old value db/cas with d/tempid as entity will throw an exception. This is a bug, no?

Ivar Refsdal18:05:26

I added the following test to the gist linked above:

(deftest this-should-throw-but-does-not
  @(d/transact *conn* [{:e/id "a" :e/version 1}])
  (let [tempid (d/tempid :db.part/user)
        {:keys [db-after]} @(d/transact *conn* [[:db/cas tempid :e/version nil 2]
                                                {:db/id tempid :e/id "a" :e/info "a"}])]
    ; :e/version should not be 2, but it is:
    (is (= 2 (:e/version (d/pull db-after [:e/version] [:e/id "a"]))))))

Ivar Refsdal18:05:04

I am off for the evening. Thanks for your replies and time Favila 🙂

favila18:05:25

> How would one assert that an upsertable entity does not have an attribute set? You need to parameterize the cas entity by the lookup ref for the upsert. So you can’t use stock :db/cas because it fails if it can’t resolve a lookup ref

👍 1
favila18:05:52

so you need some cas-like thing that knows both the tempid and what it should resolve to

Ivar Refsdal18:05:11

Hm, thanks, that makes sense. I will have a look at it tomorrow

Ivar Refsdal08:05:09

As of now I have some code like this:

(defn cas-inner [db e-or-lookup-ref a old-val new-val]
  (cond
    (string? e-or-lookup-ref)
    (d/cancel {:cognitect.anomalies/category :cognitect.anomalies/incorrect
               :cognitect.anomalies/message  "Entity cannot be string"})

    (instance? DbId e-or-lookup-ref)
    (d/cancel {:cognitect.anomalies/category :cognitect.anomalies/incorrect
               :cognitect.anomalies/message  "Entity cannot be tempid/datomic.db.DbId"})

    (and (vector? e-or-lookup-ref)
         (= 4 (count e-or-lookup-ref))
         (keyword? (first e-or-lookup-ref))
         (= :as (nth e-or-lookup-ref 2))
         (string? (last e-or-lookup-ref))
         (is-identity? db (first e-or-lookup-ref)))
    (let [e (vec (take 2 e-or-lookup-ref))]
      (cond
        (some? (:db/id (d/pull db [:db/id] e)))
        [[:db/cas e a old-val new-val]]

        (nil? old-val)
        [[:db/add (last e-or-lookup-ref) a new-val]]

        :else
        (d/cancel {:cognitect.anomalies/category :cognitect.anomalies/incorrect
                   :cognitect.anomalies/message  "Old-val must be nil for new entities"})))

    :else
    (d/cancel {:cognitect.anomalies/category :cognitect.anomalies/incorrect
               :cognitect.anomalies/message  "Unhandled state"})))
which means you can write transactions like this:
[[:ndt/cas [:e/id "a" :as "tempid"] :e/version nil 1]
 {:db/id "tempid" :e/id "a" :e/info "1"}]
Seems to work well enough. I'm also adding support for resolving pure strings (to e.g. [:e/id "a" :as "tempid"]) before running that function in the transactor...

Ivar Refsdal08:05:03

Given that cas-inner is executed by the transactor, all functions, both :ndt/cas and :db/cas, will operate on exactly the same datadatabase, right?

favila11:05:04

That “some?” check is iffy

favila11:05:51

Well maybe not

favila11:05:10

Since you know it’s getting a lookup ref

favila12:05:13

This is more complex than I expected. If d/entid on an entity identifier (of any kind) produces a nat-int? you can emit db/cas on that entity, otherwise you emit a db add on the tempid (which needn’t be a string)

favila12:05:01

Everything else is type checking the arguments

favila12:05:27

It may be clearer and more flexible to keep the ref and tempid separate instead of making one vector

favila12:05:14

But this is correct, this will work

Ivar Refsdal18:05:38

Thanks for nat-int, I hadn't heard about it. Thanks for your reply 🙂

Ivar Refsdal19:05:23

Hi again @U09R86PA4 I ended up (I think!) getting done what I wanted. I wrote a library that "handles" duplicates/abort duplicate transactions: https://github.com/ivarref/double-trouble It's basically a cas function taking a checksum/sha. It also supports tempid strings for its version of cas. If you have any input, I would appreciate it. Here is the code that runs on the transactor: https://github.com/ivarref/double-trouble/blob/main/src/com/github/ivarref/double_trouble/cas.clj Thanks and kind regards.