Skip to content

Commit f575f01

Browse files
committed
Tuples on read path #364 #427
1 parent 5b1d708 commit f575f01

7 files changed

Lines changed: 278 additions & 125 deletions

File tree

‎.gitignore‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ web/out
2828
web/target-cljs
2929
.rebl
3030
.shadow-cljs
31-
.clj-kondo
31+
.clj-kondo
32+
.lsp

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# WIP
22

3-
- Support heterogeneous (`:db/tupleTypes`) and homogeneous (`:db/tupleType`) tuples #364
3+
- Support heterogeneous (`:db/tupleTypes`) and homogeneous (`:db/tupleType`) tuples #364 #427
44

55
# 1.7.8 - Oct 11, 2025
66

‎docs/tuples.md‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,18 @@ Slot types other than `:db.type/ref` are not enforced — DataScript is dynamica
152152
[[:db/add 2 :ref+long [[:name "Ivan"] 8]]]) ; lookup ref works too
153153
```
154154

155-
Refs inside tuples are resolved on write, but are not full-fledged references otherwise: they don’t show up in reverse index, don’t get retracted when the referenced entity is retracted (the eid stays in the tuple, dangling), can’t be `:db/isComponent`, and entity API returns them as plain numbers, not entities. Same is true in Datomic.
155+
Ref slots are resolved on read, too: lookup refs and idents can be used anywhere a tuple value is expected, and are resolved against the current db.
156+
157+
```
158+
(d/datoms db :avet :ref+long [[:name "Ivan"] 8])
159+
(d/index-range db :ref+long [[:name "Ivan"] 0] [[:name "Ivan"] 100])
160+
(d/q '[:find ?e :where [?e :ref+long [[:name "Ivan"] 8]]] db)
161+
(d/q '[:find ?e :in $ [?t ...] :where [?e :ref+long ?t]] db [[[:name "Ivan"] 8]])
162+
```
163+
164+
This works for composite tuples with ref attributes as well. A lookup ref that resolves to nothing raises, same as it does for plain `:db.type/ref` attributes.
165+
166+
Refs inside tuples are not full-fledged references otherwise: they don’t show up in reverse index, don’t get retracted when the referenced entity is retracted (the eid stays in the tuple, dangling), can’t be `:db/isComponent`, and entity API returns them as plain numbers, not entities. Same is true in Datomic.
156167

157168
Unlike composite tuples, heterogeneous and homogeneous tuples are not indexed by default. Add `:db/index true` or `:db/unique` if you want AVET access. Unique declared tuples work in upserts and lookup refs, same as composite ones:
158169

‎src/datascript/db.cljc‎

Lines changed: 75 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,15 +1163,23 @@
11631163
#?(:clj (declare ref?)
11641164
:cljs (defn ^boolean ref? [db attr]))
11651165

1166+
#?(:clj (declare tuple?)
1167+
:cljs (defn ^boolean tuple? [db attr]))
1168+
1169+
#?(:clj (declare resolve-tuple-refs)
1170+
:cljs (defn resolve-tuple-refs [db a vs context]))
1171+
11661172
(defn+ resolve-datom [db e a v t default-e default-tx]
11671173
(when (some? a)
11681174
(validate-attr a (list 'resolve-datom 'db e a v t)))
11691175
(datom
11701176
(if (some? e) (entid-strict db e) default-e)
11711177
a
1172-
(if (and (some? v) (ref? db a))
1173-
(entid-strict db v)
1174-
v)
1178+
(cond
1179+
(nil? v) v
1180+
(ref? db a) (entid-strict db v)
1181+
(tuple? db a) (resolve-tuple-refs db a v (list 'resolve-datom 'db e a v t))
1182+
:else v)
11751183
(if (some? t) (entid-strict db t) default-tx)))
11761184

11771185
(defn+ components->pattern [db index c0 c1 c2 c3 default-e default-tx]
@@ -1271,38 +1279,38 @@
12711279
(util/raise "Attribute " a " expected a vector, got: " v " in " context
12721280
{:error :transact/syntax, :attribute a, :value v, :context context}))))))
12731281

1274-
(defn resolve-tuple-refs [db a vs context]
1275-
(let [attr (-> db -schema (get a))]
1276-
(validate-tuple db a attr vs context)
1277-
(cond
1278-
(not (sequential? vs)) ;; e.g. nil ov in :db.fn/cas
1279-
vs
1282+
(defn- #?@(:clj [^Boolean tuple-ref-slot?]
1283+
:cljs [^boolean tuple-ref-slot?])
1284+
[db schema-a idx]
1285+
(cond
1286+
(:db/tupleAttrs schema-a) (ref? db (nth (:db/tupleAttrs schema-a) idx nil))
1287+
(:db/tupleTypes schema-a) (= :db.type/ref (nth (:db/tupleTypes schema-a) idx nil))
1288+
:else (= :db.type/ref (:db/tupleType schema-a))))
12801289

1281-
(:db/tupleAttrs attr)
1282-
(mapv
1283-
(fn [el-a v]
1284-
(if (and (ref? db el-a) (sequential? v)) ;; lookup-ref
1285-
(entid-strict db v)
1286-
v))
1287-
(:db/tupleAttrs attr) vs)
1288-
1289-
(:db/tupleTypes attr)
1290-
(mapv
1291-
(fn [el-type v]
1292-
(if (and (= :db.type/ref el-type) (sequential? v)) ;; lookup-ref
1293-
(entid-strict db v)
1294-
v))
1295-
(:db/tupleTypes attr) vs)
1296-
1297-
(= :db.type/ref (:db/tupleType attr))
1298-
(mapv (fn [v]
1299-
(if (sequential? v) ;; lookup-ref
1300-
(entid-strict db v)
1301-
v))
1302-
vs)
1290+
(defn- #?@(:clj [^Boolean resolved-eid?]
1291+
:cljs [^boolean resolved-eid?])
1292+
[x]
1293+
(or
1294+
(nil? x)
1295+
(and (number? x) (not (neg? x)))))
13031296

1304-
:else
1305-
vs)))
1297+
(defn+ resolve-tuple-refs
1298+
"Resolves lookup refs and idents in ref slots of a tuple value.
1299+
Read path: no tempids, unresolved refs raise"
1300+
[db a vs context]
1301+
(let [schema-a (-> db -schema (get a))]
1302+
(validate-tuple db a schema-a vs context)
1303+
(if-not (sequential? vs) ;; e.g. nil ov in :db.fn/cas
1304+
vs
1305+
(into []
1306+
(map-indexed
1307+
(fn [idx v]
1308+
(if (and
1309+
(tuple-ref-slot? db schema-a idx)
1310+
(not (resolved-eid? v)))
1311+
(entid-strict db v)
1312+
v)))
1313+
vs))))
13061314

13071315
(defn+ ^number entid [db eid]
13081316
{:pre [(db? db)]}
@@ -1323,13 +1331,13 @@
13231331
(util/raise "Lookup ref attribute should be marked as :db/unique: " eid
13241332
{:error :lookup-ref/unique, :entity-id eid})
13251333

1334+
(nil? value)
1335+
nil
1336+
13261337
(tuple? db attr)
13271338
(let [value' (resolve-tuple-refs db attr value eid)]
13281339
(-> (-datoms db :avet attr value' nil nil) first :e))
1329-
1330-
(nil? value)
1331-
nil
1332-
1340+
13331341
:else
13341342
(-> (-datoms db :avet attr value nil nil) first :e)))
13351343

@@ -1468,7 +1476,8 @@
14681476
(auto-tempid? x)))
14691477

14701478
(defn- new-eid? [db eid]
1471-
(and (> eid (:max-eid db))
1479+
(and
1480+
(> eid (:max-eid db))
14721481
(< eid tx0))) ;; tx0 is max eid
14731482

14741483
(defn- advance-max-eid [db eid]
@@ -1497,83 +1506,43 @@
14971506
true
14981507
(update :db-after advance-max-eid eid))))
14991508

1500-
(defn- #?@(:clj [^Boolean tuple-slot-resolved?]
1501-
:cljs [^boolean tuple-slot-resolved?])
1502-
[x]
1503-
(or
1504-
(nil? x)
1505-
(and (number? x) (not (neg? x)))))
1506-
1507-
(defn- resolve-tuple-slot [report v]
1508-
(let [db (:db-after report)]
1509-
(cond
1510-
(tuple-slot-resolved? v)
1511-
[report v]
1512-
1513-
(tx-id? v)
1514-
(let [id (current-tx report)]
1515-
[(allocate-eid report v id) id])
1516-
1517-
(tempid? v)
1518-
(if-some [resolved (get (:tempids report) v)]
1519-
[(update report ::value-tempids assoc resolved v) resolved]
1520-
(let [resolved (next-eid db)]
1521-
[(-> report
1522-
(allocate-eid v resolved)
1523-
(update ::value-tempids assoc resolved v))
1524-
resolved]))
1525-
1526-
:else ;; lookup ref or ident
1527-
[report (entid-strict db v)])))
1528-
15291509
(defn- resolve-tuple-value
15301510
"Resoleves refs, tempids and :db/current-tx in ref slots. Returns [report' vs']"
15311511
[report a vs context]
1532-
(let [db (:db-after report)
1533-
attr (-> db -schema (get a))
1534-
_ (validate-tuple db a attr vs context)]
1535-
(cond
1536-
(not (vector? vs))
1512+
(let [db (:db-after report)
1513+
schema-a (-> db -schema (get a))
1514+
_ (validate-tuple db a schema-a vs context)]
1515+
(if-not (vector? vs)
15371516
[report vs]
1538-
1539-
(:db/tupleAttrs attr)
1540-
(let [el-attrs (:db/tupleAttrs attr)]
1541-
(loop [idx 0
1542-
report' report
1543-
vs' (transient [])]
1544-
(if (>= idx (count vs))
1545-
[report' (persistent! vs')]
1546-
(let [v (nth vs idx)]
1547-
(if (ref? db (nth el-attrs idx nil))
1548-
(let [[report'' v'] (resolve-tuple-slot report' v)]
1549-
(recur (inc idx) report'' (conj! vs' v')))
1550-
(recur (inc idx) report' (conj! vs' v)))))))
1551-
1552-
(:db/tupleTypes attr)
1553-
(let [types (:db/tupleTypes attr)]
1554-
(loop [idx 0
1555-
report' report
1556-
vs' (transient [])]
1557-
(if (>= idx (count vs))
1558-
[report' (persistent! vs')]
1559-
(let [v (nth vs idx)]
1560-
(if (= :db.type/ref (nth types idx nil))
1561-
(let [[report'' v'] (resolve-tuple-slot report' v)]
1562-
(recur (inc idx) report'' (conj! vs' v')))
1563-
(recur (inc idx) report' (conj! vs' v)))))))
1564-
1565-
(= :db.type/ref (:db/tupleType attr))
15661517
(loop [idx 0
15671518
report' report
15681519
vs' (transient [])]
15691520
(if (>= idx (count vs))
15701521
[report' (persistent! vs')]
1571-
(let [v (nth vs idx)
1572-
[report'' v'] (resolve-tuple-slot report' v)]
1573-
(recur (inc idx) report'' (conj! vs' v')))))
1574-
1575-
:else
1576-
[report vs])))
1522+
(let [v (nth vs idx)]
1523+
(if (tuple-ref-slot? db schema-a idx)
1524+
(let [[report'' v'] (cond
1525+
(resolved-eid? v)
1526+
[report' v]
1527+
1528+
(tx-id? v)
1529+
(let [id (current-tx report')]
1530+
[(allocate-eid report' v id) id])
1531+
1532+
(tempid? v)
1533+
(if-some [resolved (get (:tempids report') v)]
1534+
[(update report' ::value-tempids assoc resolved v) resolved]
1535+
(let [resolved (next-eid db)]
1536+
[(-> report'
1537+
(allocate-eid v resolved)
1538+
(update ::value-tempids assoc resolved v))
1539+
resolved]))
1540+
1541+
:else ;; lookup ref or ident
1542+
[report' (entid-strict db v)])]
1543+
1544+
(recur (inc idx) report'' (conj! vs' v')))
1545+
(recur (inc idx) report' (conj! vs' v)))))))))
15771546

15781547
;; In context of `with-datom` we can use faster comparators which
15791548
;; do not check for nil (~10-15% performance gain in `transact`)

‎src/datascript/query.cljc‎

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,8 @@
263263
;;
264264

265265
(def ^{:dynamic true
266-
:doc "List of symbols in current pattern that might potentiall be resolved to refs"}
266+
:doc "Map of symbol in current pattern that might potentially be resolved to refs
267+
=> fn that resolves its values"}
267268
*lookup-attrs* nil)
268269

269270
(def ^{:dynamic true
@@ -272,7 +273,7 @@
272273

273274
(defn getter-fn [attrs attr]
274275
(let [idx (attrs attr)]
275-
(if (contains? *lookup-attrs* attr)
276+
(if-some [resolve-fn (get *lookup-attrs* attr)]
276277
(if (int? idx)
277278
(let [idx (int idx)]
278279
(fn contained-int-getter-fn [tuple]
@@ -282,17 +283,17 @@
282283
(nth tuple idx)))]
283284
(cond
284285
(number? eid) eid ;; quick path to avoid fn call
285-
(sequential? eid) (db/entid *implicit-source* eid)
286-
(da/array? eid) (db/entid *implicit-source* eid)
286+
(sequential? eid) (resolve-fn eid)
287+
(da/array? eid) (resolve-fn eid)
287288
:else eid))))
288289
;; If the index is not an int?, the target can never be an array
289290
(fn contained-getter-fn [tuple]
290291
(let [eid #?(:cljs (da/aget tuple idx)
291292
:clj (.valAt ^ILookup tuple idx))]
292293
(cond
293294
(number? eid) eid ;; quick path to avoid fn call
294-
(sequential? eid) (db/entid *implicit-source* eid)
295-
(da/array? eid) (db/entid *implicit-source* eid)
295+
(sequential? eid) (resolve-fn eid)
296+
(da/array? eid) (resolve-fn eid)
296297
:else eid))))
297298
(if (int? idx)
298299
(let [idx (int idx)]
@@ -405,8 +406,19 @@
405406
e' (if (or (lookup-ref? e) (attr? e))
406407
(db/entid-strict source e)
407408
e)
408-
v' (if (and v (attr? a) (db/ref? source a) (or (lookup-ref? v) (attr? v)))
409-
(db/entid-strict source v)
409+
v' (cond
410+
(or (nil? v) (not (attr? a)))
411+
v
412+
413+
(db/ref? source a)
414+
(if (or (lookup-ref? v) (attr? v))
415+
(db/entid-strict source v)
416+
v)
417+
418+
(and (db/tuple? source a) (sequential? v))
419+
(db/resolve-tuple-refs source a v pattern)
420+
421+
:else
410422
v)
411423
tx' (if (lookup-ref? tx)
412424
(db/entid-strict source tx)
@@ -686,15 +698,18 @@
686698
rel))))))))
687699
rel))))
688700

701+
(defn- resolve-eid [eid]
702+
(db/entid *implicit-source* eid))
703+
689704
(defn dynamic-lookup-attrs [source pattern]
690-
(let [[e a v tx] pattern]
691-
(cond-> #{}
692-
(free-var? e) (conj e)
693-
(free-var? tx) (conj tx)
694-
(and
695-
(free-var? v)
696-
(not (free-var? a))
697-
(db/ref? source a)) (conj v))))
705+
(let [[e a v tx] pattern
706+
v-ref? (and (free-var? v) (attr? a) (db/ref? source a))
707+
v-tuple? (and (free-var? v) (attr? a) (db/tuple? source a))]
708+
(cond-> {}
709+
(free-var? e) (assoc e resolve-eid)
710+
(free-var? tx) (assoc tx resolve-eid)
711+
v-ref? (assoc v resolve-eid)
712+
v-tuple? (assoc v #(db/resolve-tuple-refs *implicit-source* a %1 pattern)))))
698713

699714
(defn limit-rel [rel vars]
700715
(when-some [attrs' (not-empty (select-keys (:attrs rel) vars))]

0 commit comments

Comments
 (0)