@jarenjs/db 0.43.3 → 0.46.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +157 -13
- package/README.md +319 -33
- package/dist/types/algebra.d.ts +32 -2
- package/dist/types/ddl.d.ts +31 -6
- package/dist/types/derive.d.ts +105 -16
- package/dist/types/dialect.d.ts +11 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/knn.d.ts +69 -0
- package/dist/types/migrate.d.ts +2 -1
- package/dist/types/plan.d.ts +11 -4
- package/dist/types/query.d.ts +11 -0
- package/docs/LIVE-FORMAT.md +30 -0
- package/docs/MIGRATION-FORMAT.md +10 -0
- package/docs/MODEL-FORMAT.md +298 -27
- package/package.json +4 -4
- package/schemas/jaren-migration.draft-07.schema.json +8 -2
- package/schemas/jaren-migration.schema.json +8 -2
- package/schemas/jaren-model.draft-07.schema.json +16 -2
- package/schemas/jaren-model.schema.json +16 -2
- package/src/algebra.js +20 -2
- package/src/ddl.js +234 -26
- package/src/derive.js +158 -19
- package/src/dialect.js +134 -2
- package/src/dialects/sqlite.js +33 -4
- package/src/emit.js +30 -6
- package/src/index.js +5 -3
- package/src/knn.js +96 -0
- package/src/live.js +87 -7
- package/src/migrate.js +55 -4
- package/src/plan.js +280 -36
- package/src/query.js +177 -44
- package/src/store.js +151 -60
package/ARCHITECTURE.md
CHANGED
|
@@ -153,6 +153,14 @@ member the schema types as an array or an object. Those are exact; the
|
|
|
153
153
|
spatial predicates that only NARROW are rows in the residual table, and
|
|
154
154
|
the implied-conjunct table below carries every proof.
|
|
155
155
|
|
|
156
|
+
Plus one ORDERING a **`derive: 'vector'`** column makes cheap without
|
|
157
|
+
making it native: the k-nearest composition — `$orderby` on a
|
|
158
|
+
`$similarity` key, descending, `$empty: 'least'`, under a `$subsequence`
|
|
159
|
+
window with a finite limit — over the member the column stores. It is
|
|
160
|
+
never pushed as an `ORDER BY`: the column CUTS the candidate set and the
|
|
161
|
+
engine DECIDES the order ("The k-nearest plan" below), which is a fourth
|
|
162
|
+
mode, `knn`, beside native, row and set.
|
|
163
|
+
|
|
156
164
|
### The deliberate-residual table
|
|
157
165
|
|
|
158
166
|
| construct | reason |
|
|
@@ -170,6 +178,9 @@ the implied-conjunct table below carries every proof.
|
|
|
170
178
|
| a geohash prefix LONGER than the column's precision | a cell-range pre-filter over the derived column's precision is pushed; the longer prefix refines in the engine |
|
|
171
179
|
| a spatial predicate over a member with no matching derived index, or one the schema does not type as an array or an object | nothing is proven; the whole predicate runs in the engine (the deterministic-function hatch may still take it) |
|
|
172
180
|
| an unbounded `$distance` (`>= r`), a circle reaching a pole or crossing the antimeridian, a probe with no bounding box | no conservative box exists — pushing nothing is correct, pushing a wrong box is not |
|
|
181
|
+
| the k-nearest ordering over a `derive: 'vector'` column | the column cuts the candidates; the engine orders them (mode `knn`, "The k-nearest plan" below) — engine work, so `strict: true` refuses it |
|
|
182
|
+
| `$similarity` anywhere else — a threshold in `$where`, a score in `$return`, a second ordering key | no native spelling; runs in the residual over whatever the rest of the document pushed |
|
|
183
|
+
| the k-nearest shape with no finite window, ascending, `$empty: 'greatest'`, a probe that is neither a literal vector nor an external, a literal probe of another width, or a selection not pushed whole (a residual conjunct, an implied one, a `$let` before the where) | nothing is proven; the whole document runs in the engine, and `explain()` names which precondition failed — a k-nearest query never falls to the full scan silently |
|
|
173
184
|
|
|
174
185
|
### The type truth table
|
|
175
186
|
|
|
@@ -273,17 +284,31 @@ member as an array or an object **and nothing else** — §8.14 answers
|
|
|
273
284
|
|
|
274
285
|
| Jaren predicate | pushed | exact? | why it is implied |
|
|
275
286
|
|---|---|---|---|
|
|
276
|
-
| `$bbox-intersects(<path>, <literal\|external>)` | `w <= L_e AND e >= L_w AND s <= L_n AND n >= L_s` | **exact** | the derived columns ARE `B(row)`, so box overlap is fully decidable. `<=`/`>=`, not `<`/`>`: the kernel counts touching edges as intersecting, and a strict comparison would disagree on every shared edge |
|
|
277
|
-
|
|
|
287
|
+
| `$bbox-intersects(<path>, <literal\|external>)`, `physical: 'columns'` | `w <= L_e AND e >= L_w AND s <= L_n AND n >= L_s` | **exact** | the derived columns ARE `B(row)`, so box overlap is fully decidable. `<=`/`>=`, not `<`/`>`: the kernel counts touching edges as intersecting, and a strict comparison would disagree on every shared edge |
|
|
288
|
+
| the same, `physical: 'rtree'` | the same four comparisons, spelled as `rowid IN (SELECT id FROM <c>_rtree WHERE minx <= L_e AND maxx >= L_w AND miny <= L_n AND maxy >= L_s)` | implied | an R\*Tree stores coordinates as **32-bit floats rounded OUTWARD**, so what it holds is `B(row)` widened — a superset, by ~3 cm in x and ~84 cm in y at 52°N. A superset has no false negatives, which is all a pre-filter needs; it is not a decision, so the exact box test refines and `strict: true` is `JD0010` here where the column mapping was native |
|
|
289
|
+
| `$within(<path>, <literal\|external>)` | the same four comparisons against `B(area)`, in whichever spelling the mapping takes | implied | the representative position is inside `B(subject)` — a bare position IS the box, and a centroid is a mean of positions, which lies within their min/max — and inside the area's surface implies inside `B(area)`; so the two boxes share at least that position ∎ |
|
|
278
290
|
| `{$le\|$lt: [{$distance: [<path>, <literal>]}, r]}` | the same four comparisons against `circleBounds(probe, r)` | implied | every position within `r` metres lies inside the circle's box, which the kernel computes on the same sphere and the same `EARTH_RADIUS` the engine measures with — so the two cannot disagree by model. `$ge`/`$gt` is NOT promoted: no box narrows "farther than r" |
|
|
279
291
|
| `{$starts-with: [{$geohash: [<path>, k]}, "<cell>"]}`, cell length ≤ k | `<c> IN ("<cell>")` or `<c> >= "<cell>" AND <c> < successor` | **exact** | the column HOLDS `$geohash(row, k)`, and geohash is a prefix code, so a prefix test on the expression is the same test on the column |
|
|
280
292
|
| the same, cell length > k | the cell truncated to k | implied | the column can only confirm its own first k characters |
|
|
281
293
|
| `{$exists: {$index-of: [{$geohash-neighbours: "<cell>"}, {$geohash: [<path>, k]}]}}`, cell length = k | `<c> IN (…the nine cells…)` | **exact** | the membership test compares whole strings and the column is exactly one of them. Nine cells, never one: two points ten metres apart can differ in the FIRST character of their cell (D7), so a single prefix is bucketing and only the neighbourhood is proximity |
|
|
282
294
|
|
|
295
|
+
**The proof is a proof about BOXES, not about SQL**, so the physical
|
|
296
|
+
mapping (MODEL-FORMAT §2.1, `physical`) does not enter it: the same box
|
|
297
|
+
is computed either way and only its spelling changes. The `rtree`
|
|
298
|
+
spelling is a `rowid` subquery — a conjunct on the collection table, so
|
|
299
|
+
the `FROM` clause, the residual machinery and `prefilters` are all
|
|
300
|
+
untouched. It is not a join (which would need join support the emitter
|
|
301
|
+
does not have, for 6 % ) and emphatically not a correlated `EXISTS`,
|
|
302
|
+
which defeats the virtual table's index entirely and measured 85× worse
|
|
303
|
+
than the subquery.
|
|
304
|
+
|
|
283
305
|
The implied forms carry no `json_type` guard — a derived column IS the
|
|
284
306
|
value — but each is TOTAL through its own `IS NOT NULL`, so a row with
|
|
285
307
|
no box answers `FALSE` rather than SQL's `NULL` and negation composes
|
|
286
|
-
classically.
|
|
308
|
+
classically. The `rtree` form is total for the same reason by a
|
|
309
|
+
different route: a row with no box was never inserted into the virtual
|
|
310
|
+
table (the sync trigger's guard, MODEL-FORMAT §3.2), so it is simply not
|
|
311
|
+
in the list. An implied conjunct may not be negated at all: negating a
|
|
287
312
|
superset is a subset, and that drops rows. That guard also makes the
|
|
288
313
|
leading term a two-sided range, which is what SQLite will actually
|
|
289
314
|
**seek**: with a one-sided range it prefers a table scan, and a scan over
|
|
@@ -299,6 +324,100 @@ item). This is the same class as the string-operator and aggregate
|
|
|
299
324
|
preconditions above, and the same answer: keep `compileSchema` injected
|
|
300
325
|
if that distinction matters to you.
|
|
301
326
|
|
|
327
|
+
### The k-nearest plan (vector)
|
|
328
|
+
|
|
329
|
+
A `derive: 'vector'` column (MODEL-FORMAT §2.1) holds each row's member
|
|
330
|
+
as a packed, l2-normalized binary32 vector. Nothing in SQL ranks over
|
|
331
|
+
it, and the reasons are portability and correctness rather than speed:
|
|
332
|
+
none of the `ORDER BY`-over-a-function spellings runs where no function
|
|
333
|
+
can be registered, and an ordering decided in SQL cannot break a tie by
|
|
334
|
+
the document's own secondary keys — which the engine executor has to,
|
|
335
|
+
for the three executors to agree. (On speed the two are close: re-measured
|
|
336
|
+
against the real column with the probe hoisted out of the per-row call,
|
|
337
|
+
`ORDER BY` over a registered function sits at rough parity with fetching
|
|
338
|
+
the column and ranking in the engine — `benchmark/vector.js` publishes
|
|
339
|
+
the band, and the store does not emit it anyway.) The k-nearest
|
|
340
|
+
composition is therefore planned as **a cut the engine finishes**, the
|
|
341
|
+
implied-conjunct pattern applied to an ordering instead of a predicate:
|
|
342
|
+
|
|
343
|
+
| stage | what | where |
|
|
344
|
+
|---|---|---|
|
|
345
|
+
| narrow | the pushed `$where` conjuncts, exactly as in every other mode | SQL |
|
|
346
|
+
| fetch | `(row identity, packed column)` for every narrowed row — no `ORDER BY`, no `LIMIT`, no similarity call; the probe never binds into the statement | SQL |
|
|
347
|
+
| score | every column unpacked and dotted with the l2-normalized probe (the cosine of the raw vectors, up to binary32 rounding); a `NULL` column scores nothing | engine (`@jarenjs/core/vector`, through `derive.js`) |
|
|
348
|
+
| cut | with `m = offset + limit`: every scored row whose score is within `margin` (`1e-6`) of the m-th best is a candidate; when fewer than `m` rows scored, EVERY row is | engine (`knn.js`) |
|
|
349
|
+
| fetch | the candidates' documents, by identity, in identity order, through the dialect's by-identities statement (batched under every build's parameter cap) | SQL |
|
|
350
|
+
| decide | the ORIGINAL document — its whole `$orderby` (the `$similarity` key over the raw member, every secondary key, `$empty`), its window, its `$return` — as the set residual over exactly those documents | engine |
|
|
351
|
+
|
|
352
|
+
**Why the cut is exact.** The column's score and the engine's key are
|
|
353
|
+
not the same number: one is a dot product over binary32-normalized
|
|
354
|
+
forms, the other the cosine of the raw doubles, and they differ by up
|
|
355
|
+
to ~1e-8 (measured over the corpus and over thousands of random 768-d
|
|
356
|
+
pairs). A plan that CUT by the column's score alone could therefore
|
|
357
|
+
pick a different m-th row than the engine whenever two true cosines lie
|
|
358
|
+
within that distance. With a margin of at least twice the divergence
|
|
359
|
+
the engine's top `m` is a subset of the candidates: if a row the engine
|
|
360
|
+
ranks inside the window were cut, some candidate the engine ranks
|
|
361
|
+
outside it would have to score higher by the column and lower by the
|
|
362
|
+
engine, which two scores within half the margin of each other cannot
|
|
363
|
+
do. `1e-6` is a hundred times the bound; in practice it admits only true
|
|
364
|
+
ties, and the residual re-ranks a handful of documents — microseconds.
|
|
365
|
+
|
|
366
|
+
**What the engine's decision buys.** Ties break by the document's OWN
|
|
367
|
+
secondary keys, never by row identity, which the engine executor cannot
|
|
368
|
+
see (row identity serves the fetch, never the order — a stable sort
|
|
369
|
+
over the candidates in identity order sees what it would have seen over
|
|
370
|
+
the whole collection). Offsets, nested windows and every secondary key
|
|
371
|
+
compose for free. And the unrankable tail is right by construction:
|
|
372
|
+
`$empty: 'least'` places a row whose key is empty LAST, so a window
|
|
373
|
+
wider than the scored rows must produce those rows in the engine's own
|
|
374
|
+
secondary order — which is exactly why the cut takes every row when
|
|
375
|
+
fewer than `m` scored (the collection is then no larger than the window)
|
|
376
|
+
rather than dropping what the column cannot rank.
|
|
377
|
+
|
|
378
|
+
**What can raise, and where** — the spatial ERRORS rule, restated for
|
|
379
|
+
the probe path. §8.15 raises `JQ2001` for a member that is not an array
|
|
380
|
+
(a string, an object, a typed array held as an object); the column is
|
|
381
|
+
`NULL` for such a member (MODEL-FORMAT §3.2), so below the scored cut
|
|
382
|
+
the row is never fetched and never raises, and past it (the full fetch)
|
|
383
|
+
the engine raises as it would have. An ABSENT member is not that case:
|
|
384
|
+
its key is the empty sequence before the operand is checked, the column
|
|
385
|
+
is `NULL`, and both executors place the row in the tail. A store that
|
|
386
|
+
wants the engine's refusal for every row keeps `compileSchema` injected
|
|
387
|
+
— the vector column only exists over a member the schema types `array`
|
|
388
|
+
and nothing else, which is what makes the two agree on every row it
|
|
389
|
+
does fetch.
|
|
390
|
+
|
|
391
|
+
**The probe.** A literal vector must be the column's width at plan time
|
|
392
|
+
(another width is not recognized, and the reason says both widths). An
|
|
393
|
+
external probe is checked at call time by the binder's own rule: a
|
|
394
|
+
bound value that is not a vector of the column's width — another width,
|
|
395
|
+
a non-finite component, not an array at all — DIVERTS the call to the
|
|
396
|
+
full-collection residual, where the engine answers what it answers
|
|
397
|
+
everywhere (empty keys, so the secondary keys order every row; or its
|
|
398
|
+
own `JQ2001` for a non-array). The plan never raises on the engine's
|
|
399
|
+
behalf.
|
|
400
|
+
|
|
401
|
+
That diversion is correct and it is expensive: the residual reads every
|
|
402
|
+
document, and `explain()` still reports `knn`, because the plan is the
|
|
403
|
+
shape and the bound value is not part of it. So the fallback is
|
|
404
|
+
COUNTED — `collection.stats().knn.diverted` — which is the only surface
|
|
405
|
+
on which a probe arriving at the wrong width from a model or a form is
|
|
406
|
+
distinguishable from a query that ran the cut. A rising `diverted` beside
|
|
407
|
+
a flat `queries` is a caller embedding through the wrong model.
|
|
408
|
+
|
|
409
|
+
**Preconditions, each named in `explain()`.** The selection must be
|
|
410
|
+
pushed whole — every `$where` conjunct exact, nothing before it — because
|
|
411
|
+
a conjunct left to the residual could drop a candidate the cut counted,
|
|
412
|
+
and an implied conjunct narrows to a superset the residual then shrinks;
|
|
413
|
+
either could leave the window short. The ordering's first key must be
|
|
414
|
+
the `$similarity`, descending, under `$empty: 'least'`; the window must
|
|
415
|
+
have a finite limit (an unbounded ranking is a full sort, and the engine
|
|
416
|
+
does it over the whole collection, said so). Further keys are the
|
|
417
|
+
engine's business. With `strict: true` the shape is `JD0010` naming the
|
|
418
|
+
rank: the order is engine work, the same honesty as a spatial
|
|
419
|
+
refinement.
|
|
420
|
+
|
|
302
421
|
### The two residual modes
|
|
303
422
|
|
|
304
423
|
- **Row residual** — only the projection is untranslated: predicates,
|
|
@@ -315,25 +434,40 @@ if that distinction matters to you.
|
|
|
315
434
|
does not), and the WHOLE original compiled document runs over the
|
|
316
435
|
materialized candidate array. Re-applying pushed conjuncts is
|
|
317
436
|
idempotent, so pushdown is pure narrowing. Reported as a barrier.
|
|
437
|
+
- **Set residual over a cut** (`knn`) — the same whole-document
|
|
438
|
+
re-run, over a candidate set an ORDERING chose rather than a
|
|
439
|
+
predicate (the k-nearest plan above). A barrier too, and
|
|
440
|
+
`stats().knn` counts the rows scored and the candidates kept per
|
|
441
|
+
query, so a collection of many exact duplicates is visible rather
|
|
442
|
+
than merely slow.
|
|
318
443
|
|
|
319
444
|
### `explain()`
|
|
320
445
|
|
|
321
446
|
Extends `compileJsonQuery(...).explain()`'s shape — `{ externals,
|
|
322
|
-
operators, functions, collations, limits }` — with `{ sql, params,
|
|
323
|
-
indexes, prefilters, residual, barriers, scanNarrative }`. `
|
|
447
|
+
operators, functions, collations, limits }` — with `{ mode, sql, params,
|
|
448
|
+
indexes, prefilters, rank, residual, barriers, scanNarrative }`. `mode`
|
|
449
|
+
is `'native' | 'row' | 'set' | 'knn'`. `params`
|
|
324
450
|
lists the bound slots in order (external names, literal markers, and
|
|
325
451
|
derived slots naming the external and box axis they compute — values
|
|
326
452
|
are ALWAYS bound, never interpolated). `indexes` names the declared
|
|
327
453
|
indexes whose generated columns the pushed predicates and ordering
|
|
328
454
|
touch, and the `scanNarrative` is the database's own `EXPLAIN QUERY
|
|
329
455
|
PLAN` prose so the claim is checkable against the engine that will run
|
|
330
|
-
it. `prefilters` is the implied conjuncts —
|
|
331
|
-
exact }` each — because whether a declared
|
|
332
|
-
not readable from `sql` alone, and because
|
|
333
|
-
predicate TOUCHES while the narrative says what
|
|
334
|
-
DO. `
|
|
456
|
+
it. `prefilters` is the implied conjuncts —
|
|
457
|
+
`{ construct, via, columns, exact }` each — because whether a declared
|
|
458
|
+
index is earning its keep is not readable from `sql` alone, and because
|
|
459
|
+
`indexes` says what a predicate TOUCHES while the narrative says what
|
|
460
|
+
the database will DO. `via` is `'columns'` or `'rtree'`: which physical
|
|
461
|
+
realization of a `bbox` column set actually ran, which is not always
|
|
462
|
+
what the model declared — a build without the R\*Tree module falls back
|
|
463
|
+
and this is where it says so (MODEL-FORMAT §4). Under `'rtree'` the
|
|
464
|
+
`columns` member names the virtual table's own columns and `indexes`
|
|
465
|
+
names the virtual table. `rank` is `null` or the k-nearest stage —
|
|
466
|
+
`{ column, dims, probe, limit, offset, margin, decides: 'engine' }` —
|
|
467
|
+
what the fetch reads, the window the cut serves, the margin it keeps,
|
|
468
|
+
and who decides the order (always the engine). `estimatedRows` is ABSENT on SQLite drivers — the capability slot
|
|
335
469
|
is empty and no number is fabricated. `residual` is `null` or
|
|
336
|
-
`{ mode: 'row' | 'set', reasons: [{ construct, reason }] }` with
|
|
470
|
+
`{ mode: 'row' | 'set' | 'knn', reasons: [{ construct, reason }] }` with
|
|
337
471
|
reasons drawn from the deliberate-residual table. With
|
|
338
472
|
`strict: true`, any residual is instead the compile error `JD0010`
|
|
339
473
|
naming the forcing construct.
|
|
@@ -457,7 +591,13 @@ delete inside the visible slice answered without re-query), running
|
|
|
457
591
|
over contributions when the extremum's holder leaves), per-group
|
|
458
592
|
**deltas** (the accumulator machinery once per group), and **re-run**
|
|
459
593
|
for everything else — declared, reported through `live.mode`, never
|
|
460
|
-
silent.
|
|
594
|
+
silent. A plan that fell to the set residual ONLY for a spatial
|
|
595
|
+
refinement (a pushed box or cell range with the exact predicate left
|
|
596
|
+
to the engine) is still the rows strategy — the geofence: the fetch is
|
|
597
|
+
index-narrowed and per-row re-evaluation IS the exact test — while an
|
|
598
|
+
ordering or an aggregate beside such a refinement re-runs, the
|
|
599
|
+
refinement never being the reason named. Invalidation matches a record
|
|
600
|
+
by table plus pointer prefix,
|
|
461
601
|
over-approximating toward re-evaluation (a missed update would be a
|
|
462
602
|
correctness bug; an extra one is only slower). Emissions preserve
|
|
463
603
|
reference identity for untouched rows — the O(k) renderer's contract —
|
|
@@ -497,4 +637,8 @@ the ENVIRONMENT — OPFS persistence across reloads, the owner topology
|
|
|
497
637
|
(one context holds the sole connection, tabs are clients over a
|
|
498
638
|
BroadcastChannel), and the second-writer refusal — across Chromium,
|
|
499
639
|
Firefox and WebKit, with the memory fallback stated where OPFS is
|
|
500
|
-
absent
|
|
640
|
+
absent — and the spatial corpus, run entry by entry through the data
|
|
641
|
+
studio's throwaway-store operation, holds the wasm build to the
|
|
642
|
+
JavaScript engine's recorded answers in every one of those engines,
|
|
643
|
+
OPFS or not, because an entry seeds its own store and needs execution,
|
|
644
|
+
not persistence.
|
package/README.md
CHANGED
|
@@ -101,21 +101,75 @@ const adults = await users.execute({
|
|
|
101
101
|
`$distance` push a bounding box the truth table proves they imply,
|
|
102
102
|
and the exact predicate re-runs over the narrowed candidates —
|
|
103
103
|
`explain().prefilters` says which, over what columns, and whether it
|
|
104
|
-
decided or merely narrowed.
|
|
104
|
+
decided or merely narrowed. The worked example, the geofence and the
|
|
105
|
+
measured numbers are in [Spatial storage](#spatial-storage--the-model-the-plan-the-fence-the-numbers)
|
|
106
|
+
below.
|
|
107
|
+
- **Migrations are documents.** `planMigration` diffs two models into
|
|
108
|
+
rendered-DDL + JSLT-transform + assertion steps; a shadow database
|
|
109
|
+
replays the whole chain before the real store is touched; a
|
|
110
|
+
checksummed history refuses edited or reordered migrations; a
|
|
111
|
+
narrowing without an adequate transform is refused against the REAL
|
|
112
|
+
data, inside the transaction.
|
|
113
|
+
- **The safe profile.** Untrusted query documents run under composed
|
|
114
|
+
bounds: engine limits on the residual, a mandatory row bound that
|
|
115
|
+
refuses rather than truncates, reference allow-lists, optional
|
|
116
|
+
full-scan refusal, and per-collection mandatory predicates no
|
|
117
|
+
document shape can shed. Read-only stores refuse writes at the
|
|
118
|
+
driver.
|
|
119
|
+
- **Writes validate** through an injected hook; without one,
|
|
120
|
+
`store.capabilities.validated` is `false` and the docs say what that
|
|
121
|
+
costs. The public API is asynchronous (the browser's OPFS story
|
|
122
|
+
forces it) with a promise-free `store.sync` twin where the driver is
|
|
123
|
+
synchronous.
|
|
124
|
+
|
|
125
|
+
## Spatial storage — the model, the plan, the fence, the numbers
|
|
126
|
+
|
|
127
|
+
A collection stores GeoJSON as it is — a position is an array, a
|
|
128
|
+
geometry is an object, nothing is wrapped — and declares what to index
|
|
129
|
+
over it:
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
const store = await openStore({
|
|
133
|
+
$model: '0.1',
|
|
134
|
+
collections: {
|
|
135
|
+
places: {
|
|
136
|
+
schema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
id: { type: 'string' },
|
|
140
|
+
// typed as geography: a spatial predicate is only pushed onto a
|
|
141
|
+
// member the schema types as an array or an object and nothing else
|
|
142
|
+
at: { type: ['array', 'object'] },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
key: '/id',
|
|
146
|
+
indexes: [
|
|
147
|
+
{ name: 'by_box', path: '$.at', derive: 'bbox' },
|
|
148
|
+
{ name: 'by_cell', path: '$.at', derive: 'geohash', precision: 6 },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
}, { driver: nodeDriver() });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`derive: 'bbox'` materializes the member's bounding box as four
|
|
156
|
+
columns and `derive: 'geohash'` its cell, both computed by
|
|
157
|
+
`@jarenjs/core/geo` — generated columns over registered deterministic
|
|
158
|
+
functions where the driver can index one, stored columns the store
|
|
159
|
+
writes where it cannot (MODEL-FORMAT §§2.1, 3). The query a consumer
|
|
160
|
+
writes then narrows in SQLite and refines in the engine, and
|
|
161
|
+
`explain()` says so:
|
|
105
162
|
|
|
106
163
|
```js
|
|
107
|
-
// the collection declares indexes: [{ name: 'by_box', path: '$.at',
|
|
108
|
-
// derive: 'bbox' }] and types that member at: { type: ['array', 'object'] }
|
|
109
|
-
// — a predicate is only PUSHED onto a member the schema types as geography
|
|
110
164
|
const places = store.collection('places');
|
|
111
|
-
const
|
|
165
|
+
const inside = {
|
|
112
166
|
$for: { p: '$[*]' },
|
|
113
167
|
$where: { $within: ['$p.at', '$region'] },
|
|
114
168
|
$return: '$p',
|
|
115
169
|
};
|
|
116
|
-
await places.execute(
|
|
170
|
+
await places.execute(inside, { externals: { region } });
|
|
117
171
|
|
|
118
|
-
const how = await places.explain(
|
|
172
|
+
const how = await places.explain(inside, { externals: { region } });
|
|
119
173
|
how.prefilters;
|
|
120
174
|
// [{ construct: '$within',
|
|
121
175
|
// columns: ['gx_at_bbox_w', 'gx_at_bbox_e', 'gx_at_bbox_s', 'gx_at_bbox_n'],
|
|
@@ -126,30 +180,261 @@ how.scanNarrative;
|
|
|
126
180
|
// 'SEARCH places USING INDEX places_by_box (gx_at_bbox_w>? AND gx_at_bbox_w<?); …'
|
|
127
181
|
```
|
|
128
182
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
183
|
+
The region arrives as a bound parameter: a GeoJSON object is not a
|
|
184
|
+
value any database can bind, so what binds is one edge of its box per
|
|
185
|
+
slot, computed at bind time from the same kernel the stored columns
|
|
186
|
+
came from. `$bbox-intersects` and a geohash cell test are exact and
|
|
187
|
+
need no refinement; `$within` and a bounded `$distance` push the box
|
|
188
|
+
they provably imply and re-run the exact predicate over the narrowed
|
|
189
|
+
candidates. A circle that reaches a pole or crosses the antimeridian
|
|
190
|
+
pushes **nothing** — there is no single box to push — and the answer is
|
|
191
|
+
the same, reached by reading more rows. A proximity probe is **nine
|
|
192
|
+
cells** (`$geohash-neighbours`), never one prefix: two points ten
|
|
193
|
+
metres apart can differ in the first character of their cell, so a
|
|
194
|
+
single-cell range is bucketing, not proximity.
|
|
195
|
+
|
|
196
|
+
**The geofence.** Register the same document as a live query and it is
|
|
197
|
+
maintained as writes arrive — the initial fetch narrows through the
|
|
198
|
+
index, and every touched row is re-evaluated by the engine's *exact*
|
|
199
|
+
predicate:
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
const fence = await places.live([{
|
|
203
|
+
$for: { p: '$[*]' },
|
|
204
|
+
$where: { $within: ['$p.at', '$region'] },
|
|
205
|
+
$return: '$p.id',
|
|
206
|
+
}], { externals: { region } });
|
|
207
|
+
fence.mode; // { strategy: 'rows', mode: 'incremental' }
|
|
208
|
+
fence.subscribe(({ patch }) => {
|
|
209
|
+
// add when a point enters the region
|
|
210
|
+
// remove when it leaves
|
|
211
|
+
// nothing while it moves within (a whole-document return sees a replace)
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
That is per-row evaluation, not an incremental spatial index: every
|
|
216
|
+
write runs `$within` against the region once, on the store's
|
|
217
|
+
connection, and a large region at a high write rate pays for it on
|
|
218
|
+
every write (LIVE-FORMAT §7 states the cost). An ordering by
|
|
219
|
+
`$distance` or a spatial aggregate re-runs on invalidation with the
|
|
220
|
+
reason in `live.mode` — declared, never silent.
|
|
221
|
+
|
|
222
|
+
**The numbers, the loss included.** `benchmark/spatial.js` stores <!--bm:spatial.corpus-->50,000 points<!--/bm-->
|
|
223
|
+
over the Netherlands and probes one box at <!--bm:spatial.rows-->258 of 50,000 (0.5 %)<!--/bm--> selectivity,
|
|
224
|
+
asserting every plan case of the committed spatial corpus and every timed shape against the JavaScript
|
|
225
|
+
engine before a single timing is printed. The `$within` a consumer writes went from <!--bm:spatial.scan-->80 ms<!--/bm-->
|
|
226
|
+
as a full scan to <!--bm:spatial.within-->2 ms<!--/bm--> over the `bbox` index (<!--bm:spatial.scanVsIndexed-->40.0<!--/bm-->×);
|
|
227
|
+
`$bbox-intersects` is <!--bm:spatial.bboxIntersects-->1.9 ms<!--/bm-->, a bounded `$distance` <!--bm:spatial.distance-->1.5 ms<!--/bm-->;
|
|
228
|
+
one geohash cell answers in <!--bm:spatial.cellOne-->0.0068 ms for 0 row(s)<!--/bm--> and the honest nine-cell
|
|
229
|
+
probe in <!--bm:spatial.cellNine-->0.026 ms for 2 row(s)<!--/bm-->. The row the store had to win is the same
|
|
230
|
+
`$within` in the in-memory engine over the parsed array, no database at all: <!--bm:spatial.engine-->32 ms<!--/bm-->.
|
|
231
|
+
The indexed store is now <!--bm:spatial.engineVsIndexed-->16.1× faster than<!--/bm--> it — but the un-indexed scan
|
|
232
|
+
is not, and the comparison is not an even one either way: the engine starts from parsed objects where the
|
|
233
|
+
store starts from bytes on a page and pays JSON materialisation for every row it returns. Both rows stay
|
|
234
|
+
published. The deterministic-UDF hatch takes a literal `$within` on a collection with no derived index, and
|
|
235
|
+
its profile is measured on the same rows in MODEL-FORMAT §8.2 (a loss as a sole predicate, a large win
|
|
236
|
+
beside a selective conjunct or a `LIMIT`).
|
|
237
|
+
|
|
238
|
+
**Two shapes on disk for one declaration.** `derive: 'bbox'` has a
|
|
239
|
+
second physical realization: `physical: 'rtree'` keeps the same four
|
|
240
|
+
derived columns and stores the boxes in a SQLite R\*Tree beside the
|
|
241
|
+
collection, synced by three declared triggers, with no B-tree over the
|
|
242
|
+
columns (MODEL-FORMAT §2.1). The logical model is unchanged — the
|
|
243
|
+
spatial corpus runs every entry under both mappings, in all three
|
|
244
|
+
executors, with no special-cased entry — and the pushed conjunct becomes
|
|
245
|
+
a `rowid` subquery over the virtual table. Through the store the same
|
|
246
|
+
`$within` measures <!--bm:spatial.rtreeStore-->0.46 ms against 2 ms — 4.3× in the R\*Tree's favour<!--/bm-->; loading the same rows
|
|
247
|
+
costs <!--bm:spatial.rtreeLoad-->718 ms against 399 ms for 50,000 documents in one transaction — 1.8× the write cost<!--/bm-->, because the R\*Tree is a
|
|
248
|
+
second table written inside every write transaction. Isolated from the
|
|
249
|
+
store on a raw connection the probe is <!--bm:spatial.rtree-->0.3 ms against 1.9 ms — 6.4× in the R\*Tree's favour<!--/bm-->.
|
|
250
|
+
Both halves are published because both are the price. One honest
|
|
251
|
+
difference comes with it: an R\*Tree stores 32-bit floats rounded
|
|
252
|
+
outward, so its box is a superset and `$bbox-intersects` is refined
|
|
253
|
+
rather than exact there — same rows, and `strict: true` says so.
|
|
254
|
+
|
|
255
|
+
**One document, three executors, proven to agree.** The same spatial
|
|
256
|
+
query document runs in three places — the JavaScript engine
|
|
257
|
+
(`compileJsonQuery`), SQLite through the Node driver, and SQLite
|
|
258
|
+
compiled to wasm in a real browser tab — and one committed corpus holds
|
|
259
|
+
all three to the same answers. `test/json/fixtures/spatial-corpus.json`
|
|
260
|
+
records what the engine answers for every case (generated, never
|
|
261
|
+
hand-typed); `test/db/spatial-oracle.test.js` runs every entry through
|
|
262
|
+
the Node driver under all three mappings — the derived indexes as
|
|
263
|
+
columns, the same indexes as R\*Trees, and none; and
|
|
264
|
+
`packages/website/e2e/spatial-agreement.spec.js` drives the data
|
|
265
|
+
studio's Store pane to run the same entries through the wasm build in
|
|
266
|
+
Chromium, Firefox and WebKit, asserting every answer against the
|
|
267
|
+
fixture on disk and the number of entries run against the corpus. Each
|
|
268
|
+
runner names the executor, the entry and the query when it disagrees.
|
|
269
|
+
That is the whole claim — not faster than anyone, not PostGIS — and the
|
|
270
|
+
browser leg's limit is stated with it: **it proves execution, not
|
|
271
|
+
durability.** Where OPFS is unavailable the tab's store is in-memory,
|
|
272
|
+
which is a property of the host, not of the suite.
|
|
273
|
+
|
|
274
|
+
**No head-to-head rival, and saying so.** Nothing else in JavaScript
|
|
275
|
+
stores GeoJSON in SQLite from a JSON query document, so the suite
|
|
276
|
+
invents none. The rivals to know about: MongoDB (`$geoWithin`, `$near`,
|
|
277
|
+
a `2dsphere` index) has a GeoJSON-native query document and a real
|
|
278
|
+
spatial index, and runs on a server — no browser execution, and no
|
|
279
|
+
second engine to agree with; DuckDB-wasm with `spatial` runs in a tab
|
|
280
|
+
with a real index and the overlay operations this store refuses to
|
|
281
|
+
build, and its query is SQL, not a document. Neither runs one document
|
|
282
|
+
through three executors proven to agree, and neither validates ring
|
|
283
|
+
closure in a schema.
|
|
284
|
+
|
|
285
|
+
## Vector storage — the column, the cut, the price, the ceiling
|
|
286
|
+
|
|
287
|
+
A collection can declare that one member is an embedding, and the store
|
|
288
|
+
keeps it as a packed column beside the document:
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
const store = await openStore({
|
|
292
|
+
$model: '0.1',
|
|
293
|
+
collections: {
|
|
294
|
+
memories: {
|
|
295
|
+
schema: {
|
|
296
|
+
type: 'object',
|
|
297
|
+
properties: {
|
|
298
|
+
id: { type: 'string' },
|
|
299
|
+
text: { type: 'string' },
|
|
300
|
+
// typed `array` and nothing else: a column over a member that
|
|
301
|
+
// may also be a string or null is a column that lies about
|
|
302
|
+
// some documents. minItems/maxItems make a wrong-width write
|
|
303
|
+
// a validation error instead of an unrankable row.
|
|
304
|
+
embedding: { type: 'array', items: { type: 'number' },
|
|
305
|
+
minItems: 768, maxItems: 768 },
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
key: '/id',
|
|
309
|
+
indexes: [{ name: 'by_vec', path: '$.embedding', derive: 'vector', dims: 768 }],
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
}, { driver: nodeDriver() });
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`derive: 'vector'` materializes the member **l2-normalized and packed as
|
|
316
|
+
little-endian binary32** — `4·dims` bytes, computed by
|
|
317
|
+
`@jarenjs/core/vector` — into one **stored** column on every driver, with
|
|
318
|
+
**no B-tree over it and no registered function** (MODEL-FORMAT §§2.1,
|
|
319
|
+
3.1). Nothing seeks a blob of floats, so the entry names a column rather
|
|
320
|
+
than an index; and because the column is stored rather than generated, a
|
|
321
|
+
plain `SELECT`, a backup or a foreign tool can read the table without
|
|
322
|
+
registering anything — which is also what lets `bun`, whose SQLite
|
|
323
|
+
binding has no function API at all, store and read the same bytes.
|
|
324
|
+
|
|
325
|
+
**"The k most similar" is not a keyword.** It is the query language's own
|
|
326
|
+
ordering and window (QUERY-FORMAT §8.15) — `$orderby` on a `$similarity`
|
|
327
|
+
key, descending, under a `$subsequence`:
|
|
328
|
+
|
|
329
|
+
```js
|
|
330
|
+
const memories = store.collection('memories');
|
|
331
|
+
const nearest = {
|
|
332
|
+
$subsequence: [{
|
|
333
|
+
$for: { m: '$[*]' },
|
|
334
|
+
$where: { $eq: ['$m.topic', 'deploys'] },
|
|
335
|
+
$orderby: [{ $key: { $similarity: ['$m.embedding', '$q'] }, $dir: 'desc', $empty: 'least' },
|
|
336
|
+
'$m.id'],
|
|
337
|
+
$return: '$m',
|
|
338
|
+
}, 0, 10],
|
|
339
|
+
};
|
|
340
|
+
await memories.execute(nearest, { externals: { q: probe } });
|
|
341
|
+
|
|
342
|
+
const how = await memories.explain(nearest, { externals: { q: probe } });
|
|
343
|
+
how.mode; // 'knn'
|
|
344
|
+
how.rank; // { column: 'gx_embedding_v768', dims: 768,
|
|
345
|
+
// probe: { external: 'q' }, offset: 0, limit: 10,
|
|
346
|
+
// margin: 1e-6, decides: 'engine' }
|
|
347
|
+
how.sql; // SELECT "rowid", "gx_embedding_v768" … WHERE …
|
|
348
|
+
// — no ORDER BY, no LIMIT, no similarity call
|
|
349
|
+
memories.stats().knn; // { queries, rows, candidates, fullFetches, diverted }
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The pushed `$where` narrows in SQL exactly as in any other mode; the
|
|
353
|
+
statement then projects `(row identity, packed column)` and nothing else;
|
|
354
|
+
the **engine** unpacks, scores and keeps every row within `1e-6` of the
|
|
355
|
+
`offset + limit`-th best; the candidates' documents are fetched by
|
|
356
|
+
identity, and the ORIGINAL document — its whole `$orderby`, its window,
|
|
357
|
+
its `$return` — runs over exactly those. So **the column cuts and the
|
|
358
|
+
engine decides**: ties break by the document's own secondary keys,
|
|
359
|
+
offsets and nested windows compose for free, and the rows the column
|
|
360
|
+
cannot rank still appear where `$empty: 'least'` puts them. `strict: true`
|
|
361
|
+
refuses the shape with `JD0010` naming the rank, because the ordering is
|
|
362
|
+
engine work — the same honesty the spatial refinement gets. A probe of
|
|
363
|
+
the wrong width, or one that is not an array of numbers, **diverts** to
|
|
364
|
+
the residual, so a query can never quietly become an O(table) scan nobody
|
|
365
|
+
counted (ARCHITECTURE, "The k-nearest plan"). A *literal* probe of the
|
|
366
|
+
wrong width is refused at plan time and `explain()` says why; an
|
|
367
|
+
**external** one is only knowable when it is bound, so the plan stays
|
|
368
|
+
`knn` and the fallback is counted instead — `stats().knn.diverted` is
|
|
369
|
+
that count, and a consumer who binds probes from a model should watch it.
|
|
370
|
+
|
|
371
|
+
**The numbers, the losses included.** `benchmark/vector.js` measures one
|
|
372
|
+
k-nearest query every physical way it can run — over <!--bm:vector.grid-->10,000 and 50,000 vectors at 384 and 768 dimensions, k = 10, the median of 10 probes<!--/bm--> —
|
|
373
|
+
and asserts that every path returns the identical top-k, ids and order,
|
|
374
|
+
on every probe before a single timing prints. The flagship row is the
|
|
375
|
+
plan a consumer's own document runs, which measures <!--bm:vector.plan-->206 ms at 50,000 × 768<!--/bm-->:
|
|
376
|
+
<!--bm:vector.table-->
|
|
377
|
+
| path (ms) | 10,000 × 384 | 10,000 × 768 | 50,000 × 384 | 50,000 × 768 |
|
|
378
|
+
|---|---:|---:|---:|---:|
|
|
379
|
+
| engine resident sweep (no database) | 3.3 | 6.7 | 17 | 32 |
|
|
380
|
+
| **the k-nearest plan (the store's own)** | 22 | 33 | 139 | 206 |
|
|
381
|
+
| raw fetch + engine sweep (the plan's statement) | 20 | 31 | 130 | 202 |
|
|
382
|
+
| `ORDER BY` over a registered function | 18 | 31 | 121 | 180 |
|
|
383
|
+
| JSON-doc sweep (no vector column) | 249 | 501 | — | — |
|
|
384
|
+
| sqlite-vec | 3.6 | 7.5 | 18 | 38 |
|
|
385
|
+
<!--/bm-->
|
|
386
|
+
|
|
387
|
+
The row the column exists to beat is the last one that has no column: the
|
|
388
|
+
same query document over a collection that stores the embedding only
|
|
389
|
+
inside the document costs <!--bm:vector.jsonDoc-->15.0× the plan at 10,000 × 768<!--/bm-->,
|
|
390
|
+
because every row's vector is parsed out of JSON before it can be
|
|
391
|
+
compared. The row the store **cannot** beat is the one with no database
|
|
392
|
+
in it: the same top-k over a resident `Float32Array` is <!--bm:vector.resident-->32 ms, which the plan is 6.4× slower than<!--/bm-->.
|
|
393
|
+
That comparison is not an even one and the direction is the point — the
|
|
394
|
+
sweep starts from decoded floats in RAM and pays nothing for durability,
|
|
395
|
+
for filters that compose with the ranking, or for a process that can
|
|
396
|
+
restart — but it stays published, because a store that is worth its
|
|
397
|
+
price should be able to say what the price is.
|
|
398
|
+
|
|
399
|
+
**Both halves of the price.** The column costs on the way in as well as
|
|
400
|
+
saving on the way out: writing the same documents with
|
|
401
|
+
the index costs <!--bm:vector.write-->10.6 s against 5.0 s for 50,000 documents in one transaction — 2.1× the write cost<!--/bm-->,
|
|
402
|
+
because every write pays a JSON round trip of the member plus a
|
|
403
|
+
normalize and a pack. On disk one vector is <!--bm:vector.storage-->3,072 B packed against 16,141 B as a JSON number array inside the document — 5.3× smaller<!--/bm--> —
|
|
404
|
+
smaller, but *added*, since the document still carries the member the
|
|
405
|
+
column is derived from.
|
|
406
|
+
|
|
407
|
+
**Pushing the rank into SQL, re-measured.** A registered similarity
|
|
408
|
+
function inside an `ORDER BY … LIMIT k` is the obvious alternative, and
|
|
409
|
+
the suite measures it against the real column with the probe hoisted out
|
|
410
|
+
of the per-row call: <!--bm:vector.udf-->180 ms against 202 ms at 50,000 × 768, and 0.87–1.00× the fetch-and-rank across the grid — rough parity on speed<!--/bm-->.
|
|
411
|
+
The plan does not emit it, and after that measurement the reasons are not
|
|
412
|
+
speed: `bun` has no user-function API, so a plan that needed one would
|
|
413
|
+
exclude an executor outright; and an ordering decided in SQL cannot break
|
|
414
|
+
a tie by the document's own secondary keys, which is what the three
|
|
415
|
+
executors have to agree on.
|
|
416
|
+
|
|
417
|
+
**The rival, and the ceiling.** `sqlite-vec` is the extension built for
|
|
418
|
+
exactly this, and it is measured rather than described: it answers the
|
|
419
|
+
same probes in <!--bm:vector.rival-->38 ms against 206 ms at 50,000 × 768 — 5.4× in sqlite-vec's favour, out of a database 6.6× smaller that holds no documents<!--/bm-->,
|
|
420
|
+
over <!--bm:vector.agreement-->40 probes, no disagreements<!--/bm-->. It is a
|
|
421
|
+
loadable native extension, which is the one thing this store will not
|
|
422
|
+
require — it would exclude the wasm tab and stock `bun`, half the
|
|
423
|
+
execution story — so the comparison is published as what it is: a faster
|
|
424
|
+
engine you may prefer, and a dependency this one does not take. What
|
|
425
|
+
neither of them is, is an approximate index. Exact brute force is linear
|
|
426
|
+
in `n · d`, and the suite states the envelope as arithmetic rather than
|
|
427
|
+
opinion: <!--bm:vector.ceiling-->5.546 ns per vector component — one query reaches 100 ms at about 22,000 vectors of 768 dimensions and one second at about 234,000<!--/bm-->.
|
|
428
|
+
Past that this design is the wrong tool and no margin changes it; what
|
|
429
|
+
lies beyond is an approximate index, and this store does not have one.
|
|
430
|
+
|
|
431
|
+
**One document, three executors, proven to agree.** As with the spatial
|
|
432
|
+
family, the k-nearest shapes of a committed corpus
|
|
433
|
+
(`test/json/fixtures/vector-corpus.json`) run through the JavaScript
|
|
434
|
+
engine, SQLite through the Node driver, and a real wasm build — indexed
|
|
435
|
+
and unindexed — and every entry must answer identically, including a
|
|
436
|
+
deliberate one-binary32-ulp near-tie and the windows that reach past the
|
|
437
|
+
scored rows into the tail the column cannot rank.
|
|
153
438
|
|
|
154
439
|
## What SQLite-only means, frankly
|
|
155
440
|
|
|
@@ -202,8 +487,9 @@ SQLite's own story (WAL plus a busy timeout, both set and visible on
|
|
|
202
487
|
- **Live queries** (LIVE-FORMAT §§7–12): `collection.live(document)`
|
|
203
488
|
maintains a result as writes arrive and emits patches — incremental
|
|
204
489
|
for `where`/`select`/`orderBy`+`limit`/aggregates/single-level
|
|
205
|
-
`groupBy`
|
|
206
|
-
|
|
490
|
+
`groupBy` and a spatial `where` over a derived index (the geofence;
|
|
491
|
+
the normative maintenance table), re-run for everything else,
|
|
492
|
+
**declared, never silent** (`live.mode` names the reason).
|
|
207
493
|
Unaffected rows stay reference-identical; a seeded oracle holds the
|
|
208
494
|
maintained result equal to a fresh re-query after every mutation.
|
|
209
495
|
- **Durable runs and the job queue** (JOBS-FORMAT, FLOW-FORMAT §7.6):
|