Last active
November 10, 2017 21:02
-
-
Save rauhs/0704f6492674ea79e935a9e01ac3a483 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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 :) | |
Syntax: | |
<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. | |
Examples: | |
(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. | |
1233480972342]]) | |
- 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 | |
:db/fn | |
(datomic-fn | |
(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))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The motivation to also allow temp-ids is this, let's say we have: