datascript

seepel 2024-04-16T17:18:10.034789Z

As I’ve been fleshing out my application, I’ve found that I keep ending up having something like an impedance mismatch where I have some functions that take an map, some functions that take a lookup, some functions that return maps, and some functions that return lookups. I don’t always have the right object. Is there a good rule of thumb for when to work with maps vs lookups?

seepel 2024-05-15T00:50:04.176959Z

I just discovered why I got so confused. There is an error when trying to transact a nested entity that is also unique. I setup a schema that has one attribute that is a plain old ref and one that is a ref that is also unique.

(def conn (d/create-conn {:unique-ref {:db/valueType :db.type/ref
                                       :db/unique :db.unique/identity}
                          :just-a-ref {:db/valueType :db.type/ref}}))
If I transact the plain old ref with a nested entity there is no problem
(d/transact! conn [{:just-a-ref {:foo 1}}])
{:db-before
 #datascript/DB {:schema
                 {:unique-ref
                  #:db{:valueType :db.type/ref, :unique :db.unique/identity},
                  :just-a-ref #:db{:valueType :db.type/ref}},
                 :datoms []},
 :db-after
 #datascript/DB {:schema
                 {:unique-ref
                  #:db{:valueType :db.type/ref, :unique :db.unique/identity},
                  :just-a-ref #:db{:valueType :db.type/ref}},
                 :datoms [[1 :just-a-ref 2 536870913] [2 :foo 1 536870913]]},
 :tx-data
 [#datascript/Datom [2 :foo 1 536870913 true]
  #datascript/Datom [1 :just-a-ref 2 536870913 true]],
 :tempids {1 1, 2 2, :db/current-tx 536870913},
 :tx-meta nil}
If I try to transact a nested unique ref I get an error that I need to provide an eid or lookup ref.
(d/transact! conn [{:unique-ref {:foo 1}}])
; Execution error (ExceptionInfo) at datascript.db/entid (db.cljc:1204).
; Expected number or lookup ref for entity id, got {:foo 1}
Is this a bug or a necessary constraint?

Niki 2024-04-16T20:00:18.763359Z

what do you mean return lookups?

seepel 2024-04-16T20:03:17.722879Z

I end up with stubs that look like

{:db/id 1}
When what I want is a lookup, so I end up pulling the id, but sometimes the map hasn’t been transacted yet and doesn’t have an id so I need to go hunting for a unique identifier attribute.

Niki 2024-04-16T20:23:00.348849Z

Okay I still don’t understand. What do you mean you get stub but want lookup? What do you mean by lookup? You get these from where?

seepel 2024-04-16T21:02:31.347949Z

Maybe I’m using the wrong term. What I mean by lookup is a vector where the first element is a keyword for a unique identity attribute, and the second element is the value to find. Here is a contrived example where I run into this is. I first pull an entity, and then later want to pull a related entity by ref attribute.

(def schema
  {:guid {:db/unique :db.unique/identity}
   :child {:db/valueType :db.type/ref}})

(let [lookup [:guid some-guid]
      parent (d/pull db '[*] lookup)
      parent-child (:child parent)
      child (d/pull db '[*] (:db/id child-parent))])
The first pull returns the child as {:db/id } because I didn’t specify it. As a result I have a lot of checks to see if an entity has been pulled already or not when I want to process that entity later. It feels like I’m doing something wrong. Where this really comes in is when the two fetches are separated by time.
(defn pull-parents []
  (d/q '[:find (pull ?e '[*])
         :where [?e :child _]]
       db))

(def pull-child [parent]
  (d/pull db '[* {:child ...}] (:db/id parent)))
On the other side there are transactions where I have an entity and I want to assign another entity to a ref attribute and I end up having to create a “lookup” with a unique identity attribute.
(d/transact conn [{:child [:guid (:guid child))}}])
Hopefully those examples make my question a little more clear.

Niki 2024-04-16T21:14:31.470509Z

Ok I think I see

Niki 2024-04-16T21:15:00.154879Z

So sometimes you use your own “external” id that needs to be paired with attribute

Niki 2024-04-16T21:15:10.535459Z

and sometimes you have entity id

Niki 2024-04-16T21:15:37.624739Z

rule of thumb is not to expose entity ids to external systems, even to your own api

Niki 2024-04-16T21:16:20.283679Z

so use them in code but if you have better id stored as attribute too then rely on those when talking over module boundaries

seepel 2024-04-16T21:19:54.033539Z

Maybe my problem is that I don’t have clear module boundaries. By that do literally mean clojure namespaces or do you mean some abstract module boundary?

Niki 2024-04-16T21:20:19.691939Z

abstract

Niki 2024-04-16T21:20:28.641299Z

like api

Niki 2024-04-16T21:21:09.771349Z

but all in all, just be mindful and maybe have var naming scheme so you remember what is what

Niki 2024-04-16T21:21:34.502369Z

I sometimes also not sure if function should accept entity or db + eid

Niki 2024-04-16T21:21:55.076209Z

have not figured out rule of thumb there

seepel 2024-04-16T21:22:24.930859Z

Got it, I think my question may ultimately boil down to that in the end as well 🤔

Niki 2024-04-16T21:23:25.705259Z

or maybe you are talking about that datascript functions should accept {:db/id …} maps where they expect eid or lookup?

Niki 2024-04-16T21:23:41.427459Z

I am not sure whether that idea is good or bad

Niki 2024-04-16T21:24:20.634789Z

at least what we have now is simple (but not super easy)

seepel 2024-04-16T21:28:31.273409Z

For what it’s worth, my vote would be no. I wrote a quick function that would basically check the inputs and find the best value for a given key and fall back to db/id. I found it incredibly difficult to understand what shape my data was in at any given function call.

👍 1
Niki 2024-04-16T21:29:13.186249Z

That’s my feeling to

seepel 2024-04-16T21:42:00.119039Z

Thanks again for the insights! 🙏

Sam Ferrell 2024-04-16T21:59:04.974919Z

Experience report... mistakenly used an empty map as a value for an attribute whose value type was a reference; seems to work OK until it is serialized and deserialized, at which point it took on the value of a reference to another nearby entity

Sam Ferrell 2024-04-16T21:59:49.720209Z

(serialization via datascript-transit)

Niki 2024-04-16T22:08:29.536469Z

If you can turn that into an issue with code example I might look into it

Niki 2024-04-16T22:08:41.879159Z

Sounds like a check is needed somewhere

Sam Ferrell 2024-04-16T22:08:51.764809Z

I'll look into that 👍

Sam Ferrell 2024-04-17T00:45:33.370149Z

https://github.com/tonsky/datascript/issues/463

🙏 1