Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
(defmacro datomic-fn
"Creates a datomic function given (fn [db e a ...] code...)"
[[_ args & body]]
`(d/function '{:lang "clojure"
:params ~args
:code (do ~@body)}))
(defn trinity-new
"1. If a is a ref:
Ensures that
- ONLY the given refs exist for the attribute
- Others are retracted
- New ones are inserted
- Existing ones are not changed
- If the entity to be ref'd does not exist then tempid MUST be specified since
you hopefully insert it in the same transaction :)
<tempid> := \"str-temp-id\" | tempid-obj | nil
<ref> := lookup-ref | ident | eid | nil
<fallback> := [ <tempid> <ref> ] ;; A CLJ vector
<accepted> := <tempid> | <ref> | <fallback>
1. If the attribute is a REF:
(trinity-new <accepted> :foo/bar [<accepted>*])
NOTE: The <fallback> is the safest since we can get a Datom
transaction conflict when we DONT know if we create a new entity beforehand or not.
(Such an entity would be given a tempid)
If only using tempid the code would remove the existing one and then later
try to add it (b/c of the given tempid) -> Conflict.
By providing both the code will first try to look it up
and not remove it if the entity already exists in the db. If it doesn't find it,
the tempid is fine since it's a new entity and doesn't lead to an error.
Pseudo example:
(tx! [{:id 1} {:id 2}]) ;; create two entities
(tx! [{:id 1, :db/id tempid} ;; We DONT know if [:id 1] exists here but it does!
(trinity-new [:id 2] :some-ref [[tempid [:id 1]]])]
=> Works for both, existing and missing [:id 1] entity!
2. If the attribute is NOT a ref but eg. a long+many:
(trinity-new <accepted> :foo/bar [2 4])
NOTE: Both (1&2) are nil value safe and will simply ignore them!
NOTE: Both can be used on setting refs of entities with tempid where you don't know
if they exist or not. Example:
(tx [{:id 2, :db/id \"new\"}, (trinity-new [\"new\" [:id 2]] ref ...)
=> Works for existing {:id 2} entity or missing.
(tx [{:user/name \"john\", :db/id \"new-user\"}
(trinity-new [:user/name \"alice\"] :user/friends
[[:user/name \"bob\"]
[\"new-user\", [:user/name \"john\"]]
;; or just \"new-user\" if you know 100% it's a new user.
- Use 0 arity version to insert the function into datomic
- Use 3 arity version for your transactions
- Use 4 arity version with db to debug what this function would generate."
{:db/id (d/tempid :db.part/db)
:db/ident :db.srs.fn/trinity-new
(fn [db e a entry-vec]
(let [tempid? (fn [x]
(or (instance? datomic.db.DbId x)
(string? x)))
fallback+eid (fn [x]
(if (and (vector? x)
(not (keyword? (first x))))
;; Not a lookup ref => both provided
[(first x) (d/entid db (second x))]
;; Either a tempid or some lookup-ref/eid/ident
(if (tempid? x)
[x nil]
[nil (d/entid db x)])))
finish! (fn [tx eid to-remove]
(into tx (map (partial vector :db/retract eid a)) to-remove))
[tempid eid] (fallback+eid e)
ref? (= :db.type/ref (:db/valueType (d/entity db a)))
existing-vals (when (some? eid)
(set (d/q '[:find [?v ...] :in $ ?e ?a :where [?e ?a ?v]]
db eid a)))
eid (or eid tempid)]
(when (nil? eid)
(throw (ex-info "Couldn't find given ref and nor tempid given"
{:value [e a]})))
(if ref?
(loop [[x & r] (vec entry-vec)
to-remove existing-vals
tx []]
(if (nil? x)
(if (nil? r)
;; We're finished, we retract the rest that is left:
(finish! tx eid to-remove)
;; Ignore the nil value:
(recur r to-remove tx))
(if (tempid? x)
(recur r to-remove (conj tx [:db/add eid a x]))
;; We either have some-ref or [tempid some-ref]
(let [[tempid ent-to-add] (fallback+eid x)]
(if (some? ent-to-add)
(if (contains? existing-vals ent-to-add)
;; it's already there, nothing to add:
(recur r (disj to-remove ent-to-add) tx)
(recur r (disj to-remove ent-to-add)
(conj tx [:db/add eid a ent-to-add])))
;; We couldn't find the entity, it must be a new one:
(if (some? tempid)
(recur r to-remove (conj tx [:db/add eid a tempid]))
(throw (ex-info "Couldn't find given ref and nor tempid given"
{:value [e a x]}))))))))
(loop [entry-vec (vec entry-vec)
to-remove existing-vals
tx []]
(let [[v & r] entry-vec]
(if (nil? v)
(if (nil? r)
;; We're finished, we retract the rest that is left:
(finish! tx eid to-remove)
;; Just ignore nil values:
(recur r to-remove tx))
(if (contains? existing-vals v)
;; it's already there, nothing to add:
(recur r (disj to-remove v) tx)
(recur r (disj to-remove v) (conj tx [:db/add eid a v]))))))))))})
([e a entries]
{:pre [(keyword? a)]}
[:db.srs.fn/trinity-new e a entries])
([db e a entries]
((:db/fn (trinity-new)) db e a entries)))

This comment has been minimized.

Copy link
Owner Author

@rauhs rauhs commented Feb 8, 2017

The motivation to also allow temp-ids is this, let's say we have:

[{:db/id "post-title"
  :post/id "some-uuid"
  :post/title "Great stuff"}
 (trinity-new [:user/id "some-uuid"] :user.posts/watching [["post-title" [:post/id "some-uuid"]]])]
  • If the post already exists, this will properly upsert and add the post to the watching list
  • If the post does NOT exist, both will be inserted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment