@jarenjs/db 0.73.0 → 0.75.0
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 +50 -7
- package/README.md +44 -6
- package/docs/LIVE-FORMAT.md +48 -13
- package/docs/MODEL-FORMAT.md +31 -14
- package/docs/REPLICATION-FORMAT.md +19 -13
- package/package.json +4 -4
- package/src/algebra.js +9 -3
- package/src/derive.js +14 -3
- package/src/dialect.js +2 -0
- package/src/dialects/check-read.js +151 -0
- package/src/dialects/postgres.js +14 -2
- package/src/dialects/sqlite.js +5 -1
- package/src/emit.js +54 -13
- package/src/introspect.js +37 -5
- package/src/live-nested.js +27 -10
- package/src/live.js +47 -135
- package/src/plan.js +163 -46
- package/src/query.js +31 -8
- package/types/index.d.ts +1 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -177,9 +177,19 @@ stable code, an object and a detail, sorted, once:
|
|
|
177
177
|
| `unmapped-table` | a table with neither a document column nor a key |
|
|
178
178
|
| `unmapped-column` | a generated column whose expression is not a member path this dialect wrote |
|
|
179
179
|
| `unmapped-index` | an index over an expression, a predicate, or a column no member path explains |
|
|
180
|
+
| `unmapped-constraint` | a CHECK that is not a complete scalar enum of a mapped entity column |
|
|
180
181
|
| `unmapped-type` | a column type no schema type maps back from — the member is derived untyped |
|
|
181
182
|
| `ambiguous-relation` | a foreign key says which entity it points at; which side declared the edge, and whether the other holds many, is not in the shape |
|
|
182
183
|
|
|
184
|
+
Scalar entity enum CHECKs are recovered on both dialects. SQLite reads its
|
|
185
|
+
stored CREATE text; PostgreSQL reads `pg_constraint` expressions, including
|
|
186
|
+
its `ANY (ARRAY[...])` rendering and numeric literal casts. The parser accepts
|
|
187
|
+
only the entire enum expression. Unknown predicates, NULL lists, non-binary SQLite collations, nondeterministic
|
|
188
|
+
PostgreSQL collations and casts whose meaning is not proven remain `unmapped-constraint`. Partial indexes and
|
|
189
|
+
expression terms retain their catalog flags; they are never converted into
|
|
190
|
+
unconditional uniqueness. Optional `introspect.checks`/`readChecks` hooks leave
|
|
191
|
+
injected dialects compatible with the earlier catalog surface.
|
|
192
|
+
|
|
183
193
|
`strict: true` refuses instead of returning a partial model, because a
|
|
184
194
|
caller about to diff the result against a declared model needs to know
|
|
185
195
|
the difference is real.
|
|
@@ -351,7 +361,8 @@ A single-binding FLWOR over the collection (`$for: { <name>: '$[*]' }`
|
|
|
351
361
|
— the examples here write `it`, but the binding is the **document's** to
|
|
352
362
|
name and nothing translates differently under another one)
|
|
353
363
|
with: comparison predicates (`$eq $ne $lt $le $gt $ge`) between a
|
|
354
|
-
singular member path and a literal or external
|
|
364
|
+
singular member path and a literal or external, or two paths in the same
|
|
365
|
+
non-null number/string family; `$and`/`$or`/`$not`
|
|
355
366
|
composition; `$exists`/`$empty`; `$starts-with`/`$ends-with`/
|
|
356
367
|
`$contains` on schema-typed string paths with literal patterns;
|
|
357
368
|
`$orderby` over singular schema-typed paths (`$dir`, `$empty`, no
|
|
@@ -359,11 +370,33 @@ collation); a top-level `$subsequence` window with literal bounds; the
|
|
|
359
370
|
top-level aggregates `$count` and `$sum`/`$avg`/`$min`/`$max` over a
|
|
360
371
|
singular schema-typed path; the whole-document projection that returns
|
|
361
372
|
the bare binding; ONE member path, projected as its value beside its
|
|
362
|
-
JSON type; and a nested SHAPE of objects, arrays, literals and member
|
|
373
|
+
JSON type; and a nested SHAPE of objects, arrays, literals, whole collection bindings and member
|
|
363
374
|
paths, projected as one value/type pair per distinct leaf and rebuilt
|
|
364
375
|
by the decoder — never by parsing a JSON text the database assembled,
|
|
365
376
|
which could not tell an absent member from a present `null`.
|
|
366
377
|
|
|
378
|
+
Constant projection trees fetch a row marker instead of a document. A
|
|
379
|
+
window over a single path removes absent members before its SQL limit;
|
|
380
|
+
a window over an opaque projection stays set-residual because that projection
|
|
381
|
+
may emit zero or several items per source row.
|
|
382
|
+
|
|
383
|
+
General scalar grouping preserves absent and null keys separately through
|
|
384
|
+
value/type pairs. Null and boolean keys may group, but ordering them stays
|
|
385
|
+
residual to preserve `JQ2005`. Explicit group ordering accepts orderable keys and proven count/min/max
|
|
386
|
+
expressions, including aggregates absent from the return; first appearance
|
|
387
|
+
breaks ties. SUM/AVG ordering remains residual: compensated or differently
|
|
388
|
+
ordered SQL accumulation can change which group sorts first. Literal windows lower over singleton group constructors;
|
|
389
|
+
`$count` over such constructors with only row-count aggregates counts a grouped
|
|
390
|
+
subquery. `$distinct` over a typed scalar projection uses the same key relation, with
|
|
391
|
+
absent members removed first. Unordered inputs retain first appearance; an
|
|
392
|
+
ordering composed solely of the projected path lowers to group-key order.
|
|
393
|
+
|
|
394
|
+
Entity projection trees, constants, counts and single-path windows follow the
|
|
395
|
+
same cardinality rules. An equijoin graph can additionally filter through
|
|
396
|
+
boolean trees whose leaves belong to individual bindings. The entity emitter
|
|
397
|
+
resolves each leaf's binding alias before composing the total predicates;
|
|
398
|
+
a disjunction never supplies a mandatory join edge.
|
|
399
|
+
|
|
367
400
|
Plus the spatial predicates a **derived** index makes decidable:
|
|
368
401
|
`$bbox-intersects` against a literal or external region, a geohash
|
|
369
402
|
prefix no longer than a `derive: 'geohash'` column's precision, and the
|
|
@@ -396,15 +429,16 @@ mode, `knn`, beside native, row and set.
|
|
|
396
429
|
| construct | reason |
|
|
397
430
|
|---|---|
|
|
398
431
|
| `$let` bindings, `$fold`, positional/window bindings | no equivalence proof exists yet; residual by default |
|
|
399
|
-
| a `$groupby` whose key is untyped
|
|
400
|
-
| a window over
|
|
432
|
+
| a `$groupby` whose key is untyped, whose `$return` reads the binding, or whose `$orderby` names anything but an orderable key or count/min/max | SQL's grouping and the engine's need not agree on an untyped key; after a grouping the binding holds the group's ROWS, which an object member cannot take |
|
|
433
|
+
| a window over a group return that may omit an item, or a group aggregate beyond the proven constructor count | SQL group cardinality must equal the projected item cardinality; numeric and error semantics need their own proof |
|
|
401
434
|
| a `$for` binding nothing joins to — a cartesian product | the engine builds the product; a plan that emitted one by accident is the thing an equi-join graph exists to prevent |
|
|
402
435
|
| non-singular path expansion | one relation per binding in this version |
|
|
403
436
|
| `$match` and other unlisted operators, `$call` | no native spelling proven equivalent |
|
|
404
437
|
| `$orderby` with a `$collation` | a collation the dialect cannot reproduce is refused, not approximated |
|
|
405
|
-
| a projection the tree cannot rebuild: an operator over a member, a reference to the binding itself, a non-singular path
|
|
438
|
+
| a projection the tree cannot rebuild: an operator over a member, a reference to the binding itself, a non-singular path | the WHOLE projection runs per row (the row residual) — pushed and ordered rows, projected by the engine; a window over these items runs in the set residual; promoting the part that composes would answer a shape nobody asked for |
|
|
406
439
|
| string operators with an external pattern | the pattern's type is unknowable at plan time and the engine ERRORS on non-string patterns |
|
|
407
|
-
| comparisons
|
|
440
|
+
| path comparisons with untyped, nullable, boolean or differing comparison families | the total typed comparison proof does not cover these shapes |
|
|
441
|
+
| `$distinct` over untyped/compound projections or ordering by other paths | first-occurrence order and structural equality need additional lowering |
|
|
408
442
|
| array/object literals in comparisons | deep-equality has no guarded native form |
|
|
409
443
|
| `$within` over a `derive: 'bbox'` column | a bounding-box pre-filter is pushed; exact containment refines in the engine |
|
|
410
444
|
| a bounded `$distance` over a `derive: 'bbox'` column | a geodesic-circle box pre-filter is pushed; the exact distance refines in the engine |
|
|
@@ -788,7 +822,7 @@ exactly what an honest explain may not print.
|
|
|
788
822
|
### The two residual modes
|
|
789
823
|
|
|
790
824
|
- **Row residual** — only the projection is untranslated: predicates,
|
|
791
|
-
|
|
825
|
+
and ordering are fully pushed, with no output window; each fetched row runs
|
|
792
826
|
`{ $for: { <the document's own binding>: '$[*]' },
|
|
793
827
|
$return: [ <the document's $return> ] }` (the array wrapper keeps
|
|
794
828
|
array-valued items unambiguous) and the items concatenate in row
|
|
@@ -1116,3 +1150,12 @@ by refreshing affected parents from bounded leaves. Source and result payloads
|
|
|
1116
1150
|
consume row and byte credits; unsupported shapes keep named reruns. See
|
|
1117
1151
|
[the replication contract](docs/REPLICATION-FORMAT.md) and
|
|
1118
1152
|
[the strategy matrix](docs/LIVE-FORMAT.md).
|
|
1153
|
+
|
|
1154
|
+
Parameterized distance bounds use the closed `circleAxis` derived parameter:
|
|
1155
|
+
`{ kind: 'circleAxis', centre: { external } | { literal }, radius: { external }
|
|
1156
|
+
| { literal }, axis: 'w' | 's' | 'e' | 'n' }`. Both inputs are named in every
|
|
1157
|
+
edge slot. The binder uses the shared geographic kernel and diverts the whole
|
|
1158
|
+
query for missing/invalid values, negative radii, polar or antimeridian boxes.
|
|
1159
|
+
The exact distance predicate still refines the candidates. The shared oracle
|
|
1160
|
+
covers SQLite column/R*Tree indexes and PostgreSQL, including repeated cached
|
|
1161
|
+
calls with different bound values.
|
package/README.md
CHANGED
|
@@ -214,7 +214,7 @@ no continuation to emit.
|
|
|
214
214
|
`explain().projection` names the path or the leaf `paths`, and
|
|
215
215
|
reading a shape no longer reads every document. A shape the tree
|
|
216
216
|
cannot rebuild — an operator over a member, a reference to the
|
|
217
|
-
binding
|
|
217
|
+
whole entity binding, a non-singular path — refuses WHOLE and
|
|
218
218
|
runs per row, with `explain().residualProjection` naming what stayed
|
|
219
219
|
behind: promoting the half that composes would answer a shape nobody
|
|
220
220
|
asked for. Every `explain()` also carries `budget`: the profile
|
|
@@ -227,6 +227,10 @@ no continuation to emit.
|
|
|
227
227
|
arithmetic deviation declared rather than hidden (MODEL-FORMAT §10.6: SQLite's
|
|
228
228
|
compensated `SUM` and the engine's naive one differ in the last
|
|
229
229
|
bit). `strict: true` turns any residual into a compile error.
|
|
230
|
+
Constant trees fetch only a row marker. Counts over entity projection trees
|
|
231
|
+
lower too; a single-path count or window excludes absent members before
|
|
232
|
+
counting items. A window over a projection that can emit several items runs
|
|
233
|
+
in the engine.
|
|
230
234
|
- **Registered operators, correct in the residual, pushed where it
|
|
231
235
|
pays.** Open with a registry (`operators:
|
|
232
236
|
createJsltRegistry().use(mathPack).use(financePack)`) and a query may
|
|
@@ -254,6 +258,12 @@ no continuation to emit.
|
|
|
254
258
|
`openStore` by name — and the promotion still needs a numeric member
|
|
255
259
|
the schema forbids `null` on, because SQL cannot tell a stored `null`
|
|
256
260
|
from an absent one and the engine can.
|
|
261
|
+
- **Typed path comparisons and distinct scalar projections.** Comparisons
|
|
262
|
+
between two paths in the same non-null number/string family lower without
|
|
263
|
+
a UDF. `$distinct` over a typed scalar projection groups by its value and JSON
|
|
264
|
+
type, excluding missing members. Unordered inputs retain first occurrence;
|
|
265
|
+
ordering solely by the projected path also lowers. Literal windows apply
|
|
266
|
+
to the distinct items.
|
|
257
267
|
- **Grouping and joins lower whole, or not at all.** A `$groupby` over
|
|
258
268
|
schema-typed member keys becomes a real `GROUP BY`: the keys come back
|
|
259
269
|
with their JSON types beside them, so a group whose key is ABSENT
|
|
@@ -262,13 +272,20 @@ no continuation to emit.
|
|
|
262
272
|
in SQL under the ENGINE's empty rules (`0` for a count or a sum, no
|
|
263
273
|
member at all for the other three), and the groups come out in the
|
|
264
274
|
engine's own order of first appearance unless an `$orderby` over the
|
|
265
|
-
keys says otherwise. Entity queries join any number of bindings: every
|
|
275
|
+
keys or count/min/max says otherwise. Entity queries join any number of bindings: every
|
|
266
276
|
binding past the first must be attached by a column equality to one
|
|
267
277
|
already joined, which is what makes the plan a nested loop the engine
|
|
268
278
|
can be compared against — a binding nothing attaches would be a
|
|
269
279
|
cartesian product, so it is the residual, named, and `strict: true`
|
|
270
280
|
refuses it. `explain()` lists the join order with the equalities that
|
|
271
281
|
attached each binding, and the group's keys, aggregates and order.
|
|
282
|
+
Nullable scalar keys also group; ordering null or boolean keys remains
|
|
283
|
+
residual to preserve the engine's error. Windows over singleton group
|
|
284
|
+
constructors lower, and counts over constructors containing only keys,
|
|
285
|
+
literals and row counts count the grouped subquery. Ties under a partial
|
|
286
|
+
group ordering retain first appearance.
|
|
287
|
+
Boolean predicates may span joined bindings when each leaf belongs to one
|
|
288
|
+
binding; the equality graph still establishes every join.
|
|
272
289
|
A join predicate that is not an equality — a range between two mapped
|
|
273
290
|
columns of one family — refines a match it never makes: the anchor is
|
|
274
291
|
still an equality, so a range alone stays the residual. A projected
|
|
@@ -1177,6 +1194,13 @@ be declared (`unmapped-view`), a foreign key cannot say which side
|
|
|
1177
1194
|
declared the edge (`ambiguous-relation`). `strict: true` refuses rather
|
|
1178
1195
|
than answering a partial model.
|
|
1179
1196
|
|
|
1197
|
+
Scalar enum CHECKs on mapped entity columns are recovered from both
|
|
1198
|
+
catalogs. A partial index or an expression term is reported as `unmapped-index`
|
|
1199
|
+
without inventing unconditional uniqueness. CHECKs outside the complete scalar
|
|
1200
|
+
enum grammar, including NULL lists, collation-dependent equality and unproven
|
|
1201
|
+
type conversions, remain
|
|
1202
|
+
`unmapped-constraint`.
|
|
1203
|
+
|
|
1180
1204
|
The derived model is usable as a migration's `from`: read a database,
|
|
1181
1205
|
plan against your declared model, and an unchanged shape plans nothing.
|
|
1182
1206
|
SQLite and PostgreSQL derive the same logical model from equivalent
|
|
@@ -1309,10 +1333,10 @@ its side-effect-free status read are in
|
|
|
1309
1333
|
capture after a disposable live probe. Failed session probes select journal
|
|
1310
1334
|
capture. OPFS, IndexedDB snapshots and memory have explicit capability and
|
|
1311
1335
|
durability differences; see [execution hosts](docs/HOSTS.md).
|
|
1312
|
-
- **
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1336
|
+
- **Remaining boundaries**: untyped and opaque grouped/projection shapes,
|
|
1337
|
+
federation beyond two sources, richer incremental live queries, portable
|
|
1338
|
+
capture and introspection outside the model vocabulary (MODEL-FORMAT §10.6
|
|
1339
|
+
and the roadmap).
|
|
1316
1340
|
|
|
1317
1341
|
The normative formats are
|
|
1318
1342
|
[docs/MODEL-FORMAT.md](docs/MODEL-FORMAT.md) (storage §§1–7, safe
|
|
@@ -1368,3 +1392,17 @@ The output is one atomically published collection bundle; read it with
|
|
|
1368
1392
|
`--format collections`. Assertion plans expose provider, ordered-fold and
|
|
1369
1393
|
bounded materialization strategies through `onAssertionPlan`.
|
|
1370
1394
|
See [MIGRATION-FORMAT §6 and §11](docs/MIGRATION-FORMAT.md).
|
|
1395
|
+
|
|
1396
|
+
Collection constructor projections can include a whole row beside member
|
|
1397
|
+
paths. Group ordering lowers count/min/max expressions, including aggregates
|
|
1398
|
+
used only for sorting; SUM/AVG ordering remains in the engine to preserve
|
|
1399
|
+
floating-point accumulation order. Parameterized distance bounds bind both
|
|
1400
|
+
centre and radius through the spatial index, with full-query fallback for
|
|
1401
|
+
invalid, polar and antimeridian probes.
|
|
1402
|
+
|
|
1403
|
+
Live collection groups now maintain multiple keys, unordered typed scalar
|
|
1404
|
+
distinct projections, and count/sum/avg/min/max over canonical group results.
|
|
1405
|
+
Only affected groups are reevaluated in source order; a final aggregate folds
|
|
1406
|
+
the retained group outputs in first-appearance order. Input documents and
|
|
1407
|
+
group outputs consume both state-entry and byte credits. See
|
|
1408
|
+
[LIVE-FORMAT](docs/LIVE-FORMAT.md) for the supported shapes and measured costs.
|
package/docs/LIVE-FORMAT.md
CHANGED
|
@@ -192,11 +192,11 @@ Choosing how much history to keep is the host's decision
|
|
|
192
192
|
(`retention`); what the reader owes is that when rows go, it reports
|
|
193
193
|
the gap instead of hiding it.
|
|
194
194
|
|
|
195
|
-
**
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
**The log alone is not replication.** Replica identity, causal frontiers,
|
|
196
|
+
durable receipts, conflicts and bounded reset snapshots belong to the opt-in
|
|
197
|
+
[replication subsystem](REPLICATION-FORMAT.md). Its protocol builds on these
|
|
198
|
+
watermarks; a change-log cursor by itself supplies no conflict policy or causal
|
|
199
|
+
ordering across writers.
|
|
200
200
|
|
|
201
201
|
## 6. Cross-connection behaviour and non-claims
|
|
202
202
|
|
|
@@ -208,8 +208,8 @@ which changes when ANOTHER connection commits; poll it and treat a
|
|
|
208
208
|
change as "re-read what you care about". Cross-tab delivery is §7's
|
|
209
209
|
story (the live-query layer).
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
Capture-layer non-claims: no conflict resolution or capture of writes made
|
|
212
|
+
by other connections, no capture on stores
|
|
213
213
|
opened without `capture`, and no statement-level ordering within a
|
|
214
214
|
commit (§2).
|
|
215
215
|
|
|
@@ -255,9 +255,8 @@ the maintenance are this table's — an entity chain re-runs, declared —
|
|
|
255
255
|
and this document stays the only place they are decided.
|
|
256
256
|
|
|
257
257
|
**This table is normative.** Every row is implemented and tested;
|
|
258
|
-
nothing outside it is attempted. Classification
|
|
259
|
-
|
|
260
|
-
what the pushdown planner already means by it.
|
|
258
|
+
nothing outside it is attempted. Classification combines canonical document shapes with the compiled selection
|
|
259
|
+
plan; "extractable" below means what the pushdown planner already proves.
|
|
261
260
|
|
|
262
261
|
| Construct (as planned) | Strategy | Maintained state |
|
|
263
262
|
|---|---|---|
|
|
@@ -265,7 +264,10 @@ what the pushdown planner already means by it.
|
|
|
265
264
|
| the same with a per-row `select` projection (row-mode plan) | **incremental rows**: the affected row alone is recomputed; a source row may project to several items | result rows, grouped by source key |
|
|
266
265
|
| `orderBy` over extractable paths, optional `limit`, offset 0 | **maintained window**: a sorted structure; ties broken by the collection key, appended as the final sort term; an insert sorting beyond a full window is a no-op | the window rows and their sort keys |
|
|
267
266
|
| whole-query `count` / `sum` / `avg` / `min` / `max` (the plan's aggregate), optional `where` | **running accumulator** plus a per-row contribution map — a delete can only be answered from retained contributions (§3: a `remove` carries no old value). `min`/`max` removal of the last extremum holder FALLS BACK to a recompute over the retained contributions; the accumulator alone cannot answer, and this fallback is the documented cost | one contribution per matching row |
|
|
268
|
-
| single-level `groupBy` with
|
|
267
|
+
| single-level `groupBy` with one or more keys and canonical returns below | **group maintenance**: reevaluate affected groups through the engine in source-row order; first surviving source occurrence determines group order | source documents and group outputs, with entry and byte credits |
|
|
268
|
+
| unordered SQL-native typed scalar distinct projections | **distinct maintenance**: keep the source holders of each value; deleting its earliest holder may move the value in first-occurrence order | source documents and unique outputs, with entry and byte credits |
|
|
269
|
+
| count/sum/avg/min/max over canonical unwindowed groups | **group maintenance** followed by an engine fold over the retained group outputs in first-occurrence order | source documents and group outputs, with entry and byte credits |
|
|
270
|
+
| ordered/windowed distinct or other aggregates over groups | **re-run on invalidation** | the previous result, for diffing |
|
|
269
271
|
| `where` whose spatial predicate is **refined** (the plan pushed a bounding-box or cell-range pre-filter and left the exact `$within`, bounded `$distance` or over-long cell prefix to the residual — `explain().prefilters` with `exact: false`), no order, no aggregate; optional per-row `select` | **incremental rows** — the geofence: the initial fetch narrows through the derived index, and every touched row is re-evaluated by the engine's EXACT predicate, so a point emits `add` when it enters the region, `remove` when it leaves, and nothing while it moves within (a whole-document return sees a `replace` carrying the new position) | the result rows |
|
|
270
272
|
| a refined spatial predicate over a collection with **no document key** (`key: null`, rowid identity) | **re-run on invalidation** — the per-row strategy tracks a row by its declared key, and a rowid is not one; the reason says `rows without a document key cannot be tracked` | the previous result, for diffing |
|
|
271
273
|
| `orderBy` beside a refined spatial predicate — over `$distance` (not a path) or over a member (the set residual drops the planner's order terms) | **re-run on invalidation**, the ordering named as the reason | the previous result, for diffing |
|
|
@@ -326,8 +328,8 @@ nested groups have a separate bounded strategy below):
|
|
|
326
328
|
|
|
327
329
|
After `$groupby`, `$it` is the group's item sequence and `$g` its key;
|
|
328
330
|
return members are the group key or an aggregate over `$it` (a path
|
|
329
|
-
below it selects the aggregated member).
|
|
330
|
-
|
|
331
|
+
below it selects the aggregated member). A bare key or a single aggregate is also canonical; other return shapes
|
|
332
|
+
re-run. Multiple declared keys follow the same rule.
|
|
331
333
|
|
|
332
334
|
## 8. Invalidation
|
|
333
335
|
|
|
@@ -657,3 +659,36 @@ Count, sum, average, minimum and maximum recompute from only the affected
|
|
|
657
659
|
parent's bounded leaves. An offset, an unsupported operator, a global input to
|
|
658
660
|
the nested group, or a group-of-groups LINQ emission remains a named rerun.
|
|
659
661
|
Replicated writes enter the same committed capture stream as local writes.
|
|
662
|
+
|
|
663
|
+
### Group maintenance details
|
|
664
|
+
|
|
665
|
+
Canonical single-level groups accept one or more key expressions over one
|
|
666
|
+
collection binding, an optional fully translated filter, and either a bare
|
|
667
|
+
key, one count/sum/avg/min/max expression, or an object containing key,
|
|
668
|
+
`$default: [key, null]`, and aggregate members. Global-root reads, ordering,
|
|
669
|
+
and windows remain rerun shapes. Aggregate expressions are evaluated by the
|
|
670
|
+
query engine, including empty results and errors; maintenance does not
|
|
671
|
+
substitute JavaScript arithmetic for query operators.
|
|
672
|
+
|
|
673
|
+
Keys preserve structural equality and distinguish missing from null. Source
|
|
674
|
+
holders retain physical row positions: changing a holder preserves its
|
|
675
|
+
position, and deleting a group's earliest holder may move that group's
|
|
676
|
+
output. Unaffected group outputs retain reference identity. Only affected
|
|
677
|
+
groups are reevaluated, followed by an optional final aggregate over all group
|
|
678
|
+
outputs. This costs work proportional to the affected groups plus group-output
|
|
679
|
+
ordering/folding, rather than constant-time arithmetic deltas.
|
|
680
|
+
|
|
681
|
+
`live.maxMaintained` counts source holders and group output items;
|
|
682
|
+
`live.maxBytes` counts their serialized documents. This includes document
|
|
683
|
+
members omitted from a small aggregate result. Exceeding a bound refuses
|
|
684
|
+
registration or closes an active query with `JD2060`; the last delivered
|
|
685
|
+
result remains unchanged. Engine failures follow the same invalidation path.
|
|
686
|
+
|
|
687
|
+
`node --expose-gc benchmark/changeflow.js` compares every mutation against a
|
|
688
|
+
fresh query and a patch-only consumer. The
|
|
689
|
+
[generated comparison table](REPLICATION-FORMAT.md#measurements) includes
|
|
690
|
+
multiple-key groups, an aggregate of groups and distinct beside forced reruns.
|
|
691
|
+
It reports initialization allocations and both median and tail mutation time;
|
|
692
|
+
heap deltas before collection are not precise retained-state sizes. The fixture
|
|
693
|
+
has 200 initial rows and 15 mutations per case, so these measurements establish
|
|
694
|
+
correctness and costs for that fixture rather than a universal crossover.
|
package/docs/MODEL-FORMAT.md
CHANGED
|
@@ -1503,10 +1503,10 @@ The token is a promise about the FOLD, and three rules enforce it:
|
|
|
1503
1503
|
the promotion needs the same schema-typed path the core `$sum` and
|
|
1504
1504
|
`$avg` need. An absent member contributes nothing on either side.
|
|
1505
1505
|
|
|
1506
|
-
A
|
|
1507
|
-
|
|
1508
|
-
a window. Both answer what the engine answers,
|
|
1509
|
-
the reason. As with the scalar hatch, a profiled document triggers no
|
|
1506
|
+
A registered fold inside a `$groupby` return is not promoted — the
|
|
1507
|
+
grouped SQL fold currently handles the built-in aggregate set only — and
|
|
1508
|
+
neither is an aggregate under a window. Both answer what the engine answers,
|
|
1509
|
+
and `explain()` names the reason. As with the scalar hatch, a profiled document triggers no
|
|
1510
1510
|
registration: the same `$mean` under a profile folds in the residual,
|
|
1511
1511
|
and `strict: true` refuses it by name (`JD0010`).
|
|
1512
1512
|
|
|
@@ -1826,16 +1826,24 @@ paths stay residual.
|
|
|
1826
1826
|
|
|
1827
1827
|
### 10.2 Joins
|
|
1828
1828
|
|
|
1829
|
-
|
|
1830
|
-
become an INNER equijoin — exactly the engine's
|
|
1829
|
+
Bindings connected by equalities between their column references
|
|
1830
|
+
become an INNER equijoin graph — exactly the engine's
|
|
1831
1831
|
cross-product-plus-filter semantics. Result order is deterministic:
|
|
1832
|
-
any `$orderby` keys first, then
|
|
1832
|
+
any `$orderby` keys first, then all bindings' row identities in
|
|
1833
1833
|
binding order, which is the engine's nested-loop order. `explain()`
|
|
1834
1834
|
reports the join (`{ left, right }`) and the `EXPLAIN QUERY PLAN`
|
|
1835
1835
|
narrative; the paired foreign key carries an index (every foreign key
|
|
1836
1836
|
does — unique for a strict one-to-one, plain otherwise), so the probe
|
|
1837
1837
|
side of the join is a `SEARCH`, never a second scan.
|
|
1838
1838
|
|
|
1839
|
+
Projection trees of singular member paths and constants lower over one or
|
|
1840
|
+
several bindings. Counts over those trees count their items; a single-path
|
|
1841
|
+
count or window first excludes rows where that path is absent. Boolean
|
|
1842
|
+
predicates may span bindings when each leaf uses one binding's proven total
|
|
1843
|
+
forms. The mandatory column equalities still establish the join graph;
|
|
1844
|
+
a disjunction is never treated as a mandatory join edge. Compatible path
|
|
1845
|
+
comparisons within one binding also lower for non-null numbers and strings.
|
|
1846
|
+
|
|
1839
1847
|
On the `load` surface the join KIND is derived from the schema
|
|
1840
1848
|
(§10.4): a `oneToOne` include reports `inner (fk required)` when the
|
|
1841
1849
|
`via` property is in `required`, `left (fk optional)` otherwise —
|
|
@@ -2034,17 +2042,26 @@ a number appears only where `capabilities.rowEstimates` is filled):
|
|
|
2034
2042
|
must never emit by accident; every binding past the first attaches by
|
|
2035
2043
|
a column equality to one already joined, and a graph that does not
|
|
2036
2044
|
close is the engine's;
|
|
2037
|
-
- non-equality join
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2045
|
+
- a non-equality predicate used as the only join anchor, or a boolean
|
|
2046
|
+
predicate whose individual leaf cannot be assigned to one binding;
|
|
2047
|
+
- collection grouping with untyped keys, row-binding returns, or ordering by
|
|
2048
|
+
anything except an orderable key or count/min/max expression; entity grouping; aggregates over groups
|
|
2049
|
+
beyond a count of singleton constructors containing keys, literals and row
|
|
2050
|
+
counts; windows over group returns that may omit an item;
|
|
2051
|
+
- projection trees containing operators, whole entity binding references or
|
|
2052
|
+
non-singular paths; windows over those opaque projections;
|
|
2053
|
+
- `$distinct` over untyped/compound projections or ordering by other paths;
|
|
2054
|
+
typed scalar projections lower when unordered or ordered solely by the
|
|
2055
|
+
projected path, preserving first occurrence or that declared order;
|
|
2044
2056
|
- externals against document paths; booleans and `null` at bind time;
|
|
2045
2057
|
- everything phase A already listed (§8 of `QUERY-FORMAT.md`
|
|
2046
2058
|
notwithstanding, the truth table is the contract).
|
|
2047
2059
|
|
|
2060
|
+
Collection group keys may include null or boolean values, but ordering those
|
|
2061
|
+
keys stays residual to preserve `JQ2005`. Group ordering is stable under ties.
|
|
2062
|
+
Literal windows over singleton group constructors lower directly, and the
|
|
2063
|
+
proven constructor count wraps the grouped statement in `COUNT(*)`.
|
|
2064
|
+
|
|
2048
2065
|
Two deviations between a pushed answer and the engine's are DECLARED
|
|
2049
2066
|
rather than refused, because in both the database is right by its own
|
|
2050
2067
|
arithmetic:
|
|
@@ -184,25 +184,31 @@ allocations observed after initialization, not precise retained-heap sizes.
|
|
|
184
184
|
|
|
185
185
|
<!--fact:db.changeflow-->
|
|
186
186
|
|
|
187
|
-
Measured 2026-09-
|
|
187
|
+
Measured 2026-09-09, v24.20.0, AMD Ryzen 9 5900HX with Radeon Graphics; 15 mutations per case.
|
|
188
188
|
|
|
189
189
|
| Shape | Strategy | Initialize ms | Mutation p50 ms | Mutation p95 ms | Initialization heap bytes |
|
|
190
190
|
|---|---|---:|---:|---:|---:|
|
|
191
|
-
| selective join | join |
|
|
192
|
-
| selective join | rerun |
|
|
193
|
-
| high fan-out join | join |
|
|
194
|
-
| high fan-out join | rerun | 0.
|
|
195
|
-
| graph | graph | 7.
|
|
196
|
-
| graph | rerun | 4.
|
|
197
|
-
|
|
|
198
|
-
|
|
|
199
|
-
|
|
|
200
|
-
|
|
|
191
|
+
| selective join | join | 15.740 | 0.349 | 2.173 | 4854712 |
|
|
192
|
+
| selective join | rerun | 1.040 | 0.850 | 1.321 | 347640 |
|
|
193
|
+
| high fan-out join | join | 8.115 | 1.984 | 3.040 | 4769632 |
|
|
194
|
+
| high fan-out join | rerun | 0.744 | 0.973 | 1.303 | 353768 |
|
|
195
|
+
| graph | graph | 7.742 | 0.429 | 0.728 | 4811904 |
|
|
196
|
+
| graph | rerun | 4.606 | 4.170 | 7.953 | 1418576 |
|
|
197
|
+
| multiple-key groups | group | 8.417 | 0.285 | 0.882 | 2107240 |
|
|
198
|
+
| multiple-key groups | rerun | 0.663 | 0.405 | 0.464 | 116960 |
|
|
199
|
+
| group aggregate | group | 3.839 | 0.236 | 0.398 | 1932544 |
|
|
200
|
+
| group aggregate | rerun | 0.846 | 0.449 | 0.670 | 275560 |
|
|
201
|
+
| distinct | distinct | 3.731 | 0.179 | 0.317 | 1616800 |
|
|
202
|
+
| distinct | rerun | 0.480 | 0.238 | 0.333 | 86128 |
|
|
203
|
+
| nested groups | nested-group | 4.565 | 0.206 | 0.377 | 1757528 |
|
|
204
|
+
| nested groups | rerun | 0.779 | 0.551 | 0.675 | 293560 |
|
|
205
|
+
| offset groups | rerun | 1.274 | 0.474 | 0.623 | 383792 |
|
|
206
|
+
| offset groups | rerun | 0.773 | 0.527 | 1.158 | 299416 |
|
|
201
207
|
|
|
202
208
|
| Capture | Envelopes | Operations | Bytes | Apply p50 ms | Replay p50 ms | Conflict p50 ms |
|
|
203
209
|
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
204
|
-
| session | 15 | 15 | 3169 | 0.
|
|
205
|
-
| journal | 15 | 15 | 3169 | 0.
|
|
210
|
+
| session | 15 | 15 | 3169 | 0.463 | 0.119 | 0.213 |
|
|
211
|
+
| journal | 15 | 15 | 3169 | 0.333 | 0.097 | 0.160 |
|
|
206
212
|
|
|
207
213
|
Selective maintenance avoids repeated full SQL evaluation. Initialization and high-fan-out maintenance can cost more than rerunning; the table includes both. Offset groups remain rerun in both requested modes.
|
|
208
214
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/db",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.75.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
@@ -84,9 +84,9 @@
|
|
|
84
84
|
"prepack": "npm run build:types"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@jarenjs/core": "^0.
|
|
88
|
-
"@jarenjs/json": "^0.
|
|
89
|
-
"@jarenjs/validate": "^0.
|
|
87
|
+
"@jarenjs/core": "^0.75.0",
|
|
88
|
+
"@jarenjs/json": "^0.75.0",
|
|
89
|
+
"@jarenjs/validate": "^0.75.0"
|
|
90
90
|
},
|
|
91
91
|
"bin": {
|
|
92
92
|
"jaren-db": "./src/cli.js"
|
package/src/algebra.js
CHANGED
|
@@ -50,13 +50,17 @@ export const PLAN_VERSION = 2;
|
|
|
50
50
|
* { p: 'not', item: PlanPredicate } |
|
|
51
51
|
* { p: 'cmp', op: 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge',
|
|
52
52
|
* ref: PlanRef, operand: PlanOperand } |
|
|
53
|
+
* { p: 'refCmp', op: 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge',
|
|
54
|
+
* left: PlanRef, right: PlanRef } |
|
|
53
55
|
* { p: 'typeIs', ref: PlanRef, types: string[], positive: boolean } |
|
|
54
56
|
* { p: 'strop', kind: 'starts' | 'ends' | 'contains',
|
|
55
57
|
* ref: PlanRef, operand: PlanOperand } |
|
|
56
58
|
* { p: 'const', value: boolean } |
|
|
57
59
|
* { p: 'udf', name: string, key: string } |
|
|
58
60
|
* { p: 'bboxOverlap', columns: { w: string, s: string, e: string,
|
|
59
|
-
* n: string }, probe: { box: number[] } | { ext: string }
|
|
61
|
+
* n: string }, probe: { box: number[] } | { ext: string } |
|
|
62
|
+
* { circle: { centre: { external: string } | { literal: unknown },
|
|
63
|
+
* radius: { external: string } | { literal: unknown } } } } |
|
|
60
64
|
* { p: 'cellIn', column: string, cells: string[], keepEmpty?: boolean } |
|
|
61
65
|
* { p: 'cellPrefix', column: string, prefix: string } |
|
|
62
66
|
* { p: 'interval', columns: { start: string, end: string },
|
|
@@ -123,7 +127,8 @@ export const PLAN_VERSION = 2;
|
|
|
123
127
|
* aggregates: { as: string, fn: 'rows' | 'sum' | 'avg' | 'min' | 'max',
|
|
124
128
|
* ref: PlanRef | null, empty: 'zero' | 'omit' | 'null' }[],
|
|
125
129
|
* tree: PlanProjectionNode,
|
|
126
|
-
* order: 'first-seen' | { index
|
|
130
|
+
* order: 'first-seen' | { index?: number, aggregate?: number,
|
|
131
|
+
* desc: boolean, nullsFirst: boolean }[]
|
|
127
132
|
* }} PlanGroup
|
|
128
133
|
* The GENERAL `GROUP BY`: one key per declared grouping name, the
|
|
129
134
|
* closed aggregate set over the group's rows, and the projection tree
|
|
@@ -133,7 +138,8 @@ export const PLAN_VERSION = 2;
|
|
|
133
138
|
* an `agg` node reads one aggregate under its `empty` rule. `order` is
|
|
134
139
|
* `'first-seen'`, the engine's own order of first appearance, or the
|
|
135
140
|
* group-key ordering an `$orderby` declared. A plan carrying a group
|
|
136
|
-
* carries no `bucket
|
|
141
|
+
* carries no `bucket` or `rank`. A `count` aggregate may wrap a
|
|
142
|
+
* singleton group constructor, counting groups through a subquery.
|
|
137
143
|
*
|
|
138
144
|
* @typedef {{ ref: PlanRef, every: number, origin: number, as: string,
|
|
139
145
|
* order: 'asc' | 'desc' | 'first-seen',
|
package/src/derive.js
CHANGED
|
@@ -383,12 +383,23 @@ export function cellNeighbourhood(cell) {
|
|
|
383
383
|
* external's bounding box, computed at bind time because a GeoJSON
|
|
384
384
|
* object is not a value any database can bind. `null` when the value
|
|
385
385
|
* has no box, which is what tells the caller to divert.
|
|
386
|
-
* @param {
|
|
387
|
-
* @param {any} value - the bound external
|
|
386
|
+
* @param {any} derived
|
|
387
|
+
* @param {any} value - the bound external for bboxAxis; all externals for circleAxis
|
|
388
388
|
* @returns {number | null}
|
|
389
389
|
*/
|
|
390
390
|
export function derivedSlotValue(derived, value) {
|
|
391
|
-
|
|
391
|
+
let box;
|
|
392
|
+
if (derived.kind === 'bboxAxis') box = bboxOf(value);
|
|
393
|
+
else if (derived.kind === 'circleAxis') {
|
|
394
|
+
const read = (input) => 'external' in input ? value[input.external] : input.literal;
|
|
395
|
+
const at = probePosition(read(derived.centre));
|
|
396
|
+
const radius = read(derived.radius);
|
|
397
|
+
if (at === null || typeof radius !== 'number' || !Number.isFinite(radius) || radius < 0)
|
|
398
|
+
return null;
|
|
399
|
+
box = probeCircleBox(at, radius);
|
|
400
|
+
if (box !== null && (box[0] < -180 || box[2] > 180)) return null;
|
|
401
|
+
}
|
|
402
|
+
else throw new TypeError(`unknown derived parameter kind '${derived.kind}'`);
|
|
392
403
|
return box === null ? null : box[BBOX_AT[derived.axis]];
|
|
393
404
|
}
|
|
394
405
|
|
package/src/dialect.js
CHANGED
|
@@ -185,6 +185,7 @@ function normalizeCapabilities(declared) {
|
|
|
185
185
|
* memberPathOf?: (expression: string) => (JsonPathSegment[] | null),
|
|
186
186
|
* expressionOf?: (expression: string, byName: Record<string, string>) => (any | null),
|
|
187
187
|
* readGenerated?: (rows: any[]) => { name: string, expression: string }[],
|
|
188
|
+
* readChecks?: (rows: any[]) => { name: string, column?: string, values?: any[] }[],
|
|
188
189
|
* introspect: { version: () => string, compileOptions: () => string,
|
|
189
190
|
* pragma: (name: string) => string,
|
|
190
191
|
* tableExists: () => string, columns: (table: string) => string,
|
|
@@ -640,6 +641,7 @@ export function createDialect(spec) {
|
|
|
640
641
|
* one that can read it.
|
|
641
642
|
*/
|
|
642
643
|
readGenerated: spec.readGenerated,
|
|
644
|
+
readChecks: spec.readChecks,
|
|
643
645
|
explainQuery: spec.explainQuery,
|
|
644
646
|
/** The plan narrative, one line per row the engine answered. */
|
|
645
647
|
explainLines: spec.explainLines,
|