Transactable entities
Nobody has claimed this yet.
- Dominant language
- Clojure
- Stars
- 5.8k
- Forks
- 318
- PR merge metrics
- No merged PRs in 30d
Description
This is a backwards compatible change that would vastly improve DX in my opinion
I've always wondered: why are entities not transactable? I find myself converting entities to maps all the time solely to transact them. This still causes problems when entities nest other entities. So here are a few simple ideas on how entities could be treated in transactions:
1. Entities could be treated as refs in transactions
(def schema
{:user/friends #:db{:valueType :db.type/ref
:cardinality :db.cardinality/many}})
(def ent (d/touch (d/entity @conn 1)))
ent ; eval
;; =>
{:db/id 1
:user/email "foo@example.com"
:user/friends #{{:db/id 2} {:db/id 3}}} ; <-- nested entities
Now I convert it to a map
(def ent-map (into {:db/id (:db/id ent)} ent))
ent-map ; eval
;; =>
{:db/id 1
:user/email "foo@example.com"
:user/friends #{{:db/id 2} {:db/id 3}}}
;; looks the same but nested entities (under :user/friends) have not been converted
I try to transact it
(d/transact! conn [(assoc ent-map :user/email "BAR@example.com")])
;; throws:
;; Execution error (ExceptionInfo) at datascript.db/entid (db.cljc:385).
;; Expected number or lookup ref for entity id, got #:db{:id 2}
So I can either dissoc the :user/friends map-entry or convert contained entities to refs
(d/transact! conn [(-> ent-map
(dissoc :genstyle.project/population)
;; OR (update :user/friends #(mapv :db/id %))
(assoc :user/email "BAR@example.com"))])
We could spare ourselves from this by treating entities as refs in transactions. The database already walks nested data-structures to resolve refs so why not resolve entities as refs, also?
2. Entities to return maps on update
datascript.impl.entity/Entity implements clojure.lang.Associative which currently only throws errors:
clojure.lang.Associative
;; some methods elided
(empty [e] (throw (UnsupportedOperationException.)))
(assoc [e k v] (throw (UnsupportedOperationException.)))
(cons [e [k v]] (throw (UnsupportedOperationException.)))
Instead assoc could return a hashmap
(deftype Entity [db eid touched cache]
;; elided
clojure.lang.Associative
(assoc [e k v]
(let [e-map (cond-> {:db/id eid}
; add other kvals if touched
touched (into e))]
(assoc e-map k v))))
This would also make update possible. Together this means that the change of email to ent from above, could look like this:
(d/transact! conn [(assoc ent :user/email "BAR@example.com")])
I would've already implemented this for my own projects but unfortunately Clojure (unlike ClojureScript) doesn't allow to overwrite a Type's methods. To achieve this one would have to for DataScript and change the code of datascript.impl.entity/Entity so I wanted to raise the issue here first and see what @tonsky's thoughts are.
This would also unlock a more straightforward use of libraries like meander or specter to walk and update entities.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
The issue points to datascript.db/entid in db.cljc and datascript.impl.entity/Entity; start by reading those transaction and Associative paths. Clarify which entity-as-ref and assoc semantics are accepted, then define coverage showing nested entities transact correctly and updates produce the intended transaction data.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- clojure
- Domain
- databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100