@jarenjs/db 0.46.5 → 0.56.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 +133 -17
- package/README.md +270 -36
- package/docs/JOBS-FORMAT.md +24 -8
- package/docs/LIVE-FORMAT.md +139 -7
- package/docs/MIGRATION-FORMAT.md +118 -36
- package/docs/MODEL-FORMAT.md +251 -30
- package/package.json +4 -5
- package/schemas/jaren-migration.draft-07.schema.json +73 -0
- package/schemas/jaren-migration.schema.json +73 -0
- package/src/algebra.js +22 -3
- package/src/capture.js +66 -28
- package/src/cli.js +225 -44
- package/src/ddl.js +23 -3
- package/src/dialect.js +13 -0
- package/src/dialects/sqlite.js +21 -1
- package/src/driver.js +63 -16
- package/src/drivers/wasm.js +1 -0
- package/src/emit-model.js +14 -0
- package/src/emit.js +42 -9
- package/src/entity.js +92 -47
- package/src/errors.js +28 -0
- package/src/index.js +2 -2
- package/src/jobs.js +40 -5
- package/src/live-time.js +605 -0
- package/src/live.js +52 -9
- package/src/migrate.js +397 -191
- package/src/model.js +173 -8
- package/src/plan.js +834 -47
- package/src/query.js +296 -22
- package/src/residual.js +15 -6
- package/src/series.js +349 -0
- package/src/store.js +243 -69
- package/src/tracker.js +173 -48
- package/types/index.d.ts +206 -12
- package/types/node.d.ts +3 -1
- package/types/typed.d.ts +58 -2
- package/types/wasm.d.ts +7 -0
- package/dist/types/algebra.d.ts +0 -199
- package/dist/types/app.d.ts +0 -49
- package/dist/types/capture.d.ts +0 -85
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/dag-job.d.ts +0 -40
- package/dist/types/ddl.d.ts +0 -229
- package/dist/types/derive.d.ts +0 -250
- package/dist/types/dialect.d.ts +0 -149
- package/dist/types/dialects/sqlite.d.ts +0 -9
- package/dist/types/driver.d.ts +0 -110
- package/dist/types/drivers/bun.d.ts +0 -47
- package/dist/types/drivers/node.d.ts +0 -37
- package/dist/types/drivers/wasm.d.ts +0 -65
- package/dist/types/emit-model.d.ts +0 -44
- package/dist/types/emit.d.ts +0 -75
- package/dist/types/entity.d.ts +0 -23
- package/dist/types/errors.d.ts +0 -167
- package/dist/types/graph.d.ts +0 -28
- package/dist/types/index.d.ts +0 -37
- package/dist/types/jobs.d.ts +0 -140
- package/dist/types/knn.d.ts +0 -69
- package/dist/types/live.d.ts +0 -62
- package/dist/types/migrate.d.ts +0 -170
- package/dist/types/model.d.ts +0 -36
- package/dist/types/patch-sql.d.ts +0 -37
- package/dist/types/plan.d.ts +0 -140
- package/dist/types/profile.d.ts +0 -80
- package/dist/types/query.d.ts +0 -111
- package/dist/types/residual.d.ts +0 -61
- package/dist/types/store.d.ts +0 -53
- package/dist/types/tracker.d.ts +0 -43
- package/dist/types/typed.d.ts +0 -15
- package/dist/types/types.d.ts +0 -26
- package/dist/types/udf.d.ts +0 -75
- package/dist/types/window.d.ts +0 -52
package/ARCHITECTURE.md
CHANGED
|
@@ -153,6 +153,17 @@ 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 GROUPING a composite instant index makes exact: the
|
|
157
|
+
fixed-width temporal bucket. A `$groupby` whose single key is
|
|
158
|
+
`$time-bucket` over a schema-typed integer epoch path with a literal
|
|
159
|
+
width and origin, projected to the group key plus `$count` of the
|
|
160
|
+
binding and `$sum`/`$avg`/`$min`/`$max` over singular schema-typed
|
|
161
|
+
numeric paths, becomes a `GROUP BY` over integer arithmetic. So does a
|
|
162
|
+
`$resample` whose frozen spec asks for nothing that ladder cannot do —
|
|
163
|
+
a fixed width, `fill: 'omit'`, and one of `sum|mean|min|max|count`.
|
|
164
|
+
Everything else about time is a NAMED refinement ("The temporal plan"
|
|
165
|
+
below).
|
|
166
|
+
|
|
156
167
|
Plus one ORDERING a **`derive: 'vector'`** column makes cheap without
|
|
157
168
|
making it native: the k-nearest composition — `$orderby` on a
|
|
158
169
|
`$similarity` key, descending, `$empty: 'least'`, under a `$subsequence`
|
|
@@ -181,6 +192,7 @@ mode, `knn`, beside native, row and set.
|
|
|
181
192
|
| 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
193
|
| `$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
194
|
| 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 |
|
|
195
|
+
| `$resample` with a fill policy other than `omit`, `$rolling`, `$asof`, `first`/`last`, a calendar width, a named zone, a `$time-bucket` whose width or origin is an EXPRESSION, or an instant the schema does not type as an integer | the index bounds the fetch and `@jarenjs/core/series` decides over what comes back; `explain().series.reasons` carries the CODE — `fill-policy`, `rolling-refinement`, `asof-refinement`, `unsupported-aggregate`, `calendar-width`, `named-zone`, `nonliteral-spec`, `instant-not-integer` — and `strict: true` refuses every one of them |
|
|
184
196
|
|
|
185
197
|
### The type truth table
|
|
186
198
|
|
|
@@ -235,14 +247,21 @@ index-usable spelling and read every row of the collection.
|
|
|
235
247
|
|
|
236
248
|
The guards make the forms sound for typed AND untyped paths alike —
|
|
237
249
|
the schema type's job is choosing the generated COLUMN (the index),
|
|
238
|
-
never weakening the guard. Two documented preconditions
|
|
239
|
-
operators, aggregates
|
|
240
|
-
paths
|
|
241
|
-
|
|
242
|
-
non-string under a string operator —
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
250
|
+
never weakening the guard. Two documented preconditions. String
|
|
251
|
+
operators, aggregates and ordering are only promoted on schema-typed
|
|
252
|
+
paths that cannot hold `null` and are not boolean, because the engine
|
|
253
|
+
ERRORS on non-conforming operands where SQL would coerce or sort — a
|
|
254
|
+
`null` under an ordered key, a non-string under a string operator —
|
|
255
|
+
so a path whose schema admits `null` is a named residual for those
|
|
256
|
+
forms and the two engines keep answering alike over conforming data.
|
|
257
|
+
And the guards protect the JSON TYPE, not the VALUE: a typed
|
|
258
|
+
generated column carries SQLite affinity, so on an unvalidated store a
|
|
259
|
+
row that violates the schema — the string `'020'` under an `integer`
|
|
260
|
+
path — reads as the integer `20` in the column while `json_type` still
|
|
261
|
+
says text, and a pushed comparison answers rows the engine (comparing
|
|
262
|
+
the JSON value) does not. Keep `compileSchema` injected: with the hook
|
|
263
|
+
no such row is ever stored, and without it the pushdown is exact only
|
|
264
|
+
over documents that happen to conform.
|
|
246
265
|
|
|
247
266
|
**Bind-time diversion.** SQLite cannot bind a boolean, and a
|
|
248
267
|
`null`-valued external needs Jaren's null semantics, not SQL's. At
|
|
@@ -418,6 +437,79 @@ engine's business. With `strict: true` the shape is `JD0010` naming the
|
|
|
418
437
|
rank: the order is engine work, the same honesty as a spatial
|
|
419
438
|
refinement.
|
|
420
439
|
|
|
440
|
+
### The temporal plan (series)
|
|
441
|
+
|
|
442
|
+
Time needs no storage kind here. The physical declaration is the
|
|
443
|
+
composite JSONPath index a model already has —
|
|
444
|
+
`{ "name": "by_series_at", "path": ["$.series", "$.at"] }` — over a
|
|
445
|
+
numeric epoch member, and the whole of `src/series.js` is deciding
|
|
446
|
+
which questions that index answers. There is no `derive: 'series'`, no
|
|
447
|
+
column type, no host function and no extension.
|
|
448
|
+
|
|
449
|
+
A B-tree seeks exactly as far as its leading columns are decided: a run
|
|
450
|
+
of equalities, then at most one range. So the index that a temporal
|
|
451
|
+
plan reports is the declared one with the longest leading run of
|
|
452
|
+
columns an equality PINNED whose next column is the instant the query
|
|
453
|
+
ranges over. Three shapes follow from that, and they are closed:
|
|
454
|
+
|
|
455
|
+
| shape | the document | the plan |
|
|
456
|
+
|---|---|---|
|
|
457
|
+
| **range** | every leading column pinned, a half-open range on the instant, ordered by it | native — `SEARCH … (series=? AND at>? AND at<?)` |
|
|
458
|
+
| **as-of** | the same prefix with ONE instant bound, ordered by the instant, under a finite window | native — the index read backwards, one row |
|
|
459
|
+
| **bucket** | a `$groupby` over `$time-bucket`, or a `$resample` whose spec asks for nothing more | native — `GROUP BY` over `at - (((at - origin) % every + every) % every)` |
|
|
460
|
+
|
|
461
|
+
The ladder is integer arithmetic all the way down, and the non-negative
|
|
462
|
+
remainder is why: a truncating division puts an instant before 1970 in
|
|
463
|
+
the bucket AFTER its own. The width and the anchor are read through the
|
|
464
|
+
kernel's own `compileBuckets`, so `'PT1H'` and `3600000` are the same
|
|
465
|
+
ladder and the default anchor is the kernel's rather than a second
|
|
466
|
+
guess at it; an `{ offset }` calendar context folds into the anchor,
|
|
467
|
+
because a constant number of minutes east is arithmetic. A named zone
|
|
468
|
+
never does: its clock is host code the database does not have.
|
|
469
|
+
|
|
470
|
+
Everything else is a **named refinement** — the implied-conjunct
|
|
471
|
+
pattern again, applied to a whole operator. The planner narrows the
|
|
472
|
+
fetch by whatever the frozen spec makes provable and the residual, which
|
|
473
|
+
is the ENGINE running the caller's own document, decides:
|
|
474
|
+
|
|
475
|
+
| operator | what bounds the fetch |
|
|
476
|
+
|---|---|
|
|
477
|
+
| `$resample` (fill, calendar, `first`/`last`) | the spec's own `start`/`end`, plus the operand's `$where` |
|
|
478
|
+
| `$rolling` | the operand's `$where` alone — a window measured in time answers once per input instant |
|
|
479
|
+
| `$asof` with the collection on the RIGHT | the probes' own span (`at <= max` backward, `at >= min` forward, both sides under a `tolerance`) and a membership test over the probes' `by` keys |
|
|
480
|
+
| `$asof` with the collection on the LEFT | nothing — a join answers once per LEFT row, so every left row is needed |
|
|
481
|
+
|
|
482
|
+
The as-of bound is one STATEMENT, whatever the probes number, which is
|
|
483
|
+
what `test/db/statement-count.test.js` pins: the failure mode a batch
|
|
484
|
+
exists to refuse is one seek per left row. Without a `tolerance` a
|
|
485
|
+
backward join can only be bounded ABOVE, so that one statement can read
|
|
486
|
+
most of a long history — `benchmark/series.js` publishes the candidate
|
|
487
|
+
count beside the timing rather than netting it out.
|
|
488
|
+
|
|
489
|
+
**A group with no instant.** A row whose instant member is missing or
|
|
490
|
+
is not a number groups under SQL `NULL`; the kernel REFUSES such a row
|
|
491
|
+
(`JQ2001`). SQL cannot refuse, so a native bucket that meets one hands
|
|
492
|
+
the whole question back — the full-collection residual, where the engine
|
|
493
|
+
answers what it answers everywhere — and the fallback is COUNTED as
|
|
494
|
+
`collection.stats().series.diverted`, the same honesty the k-nearest
|
|
495
|
+
divert has.
|
|
496
|
+
|
|
497
|
+
**`explain().series`** is `null` unless the document asked a temporal
|
|
498
|
+
question, which it does by naming a §8.16 operator or by being the
|
|
499
|
+
closed range/as-of shape over a COMPOSITE instant index. (A singular
|
|
500
|
+
index over an ordinary member cannot make a query temporal: nothing in
|
|
501
|
+
a column says "instant", and inventing one would make every `age > 21`
|
|
502
|
+
a temporal plan.) It carries `{ mode, operation, index, prefix, range,
|
|
503
|
+
ladder, aggregates, refinement, reasons, counts }`. `mode` is
|
|
504
|
+
`'native'` (the statement alone answers), `'hybrid'` (the database
|
|
505
|
+
narrows and a kernel decides) or `'engine'` (the database narrowed
|
|
506
|
+
nothing). `reasons` is `{ code, reason }` where the code is the first
|
|
507
|
+
word of the sentence, so the machine-readable code and the sentence
|
|
508
|
+
`strict: true` prints cannot drift apart. `counts` is the LAST ACTUAL
|
|
509
|
+
execution's `{ statements, candidates, results }` and is `null` before
|
|
510
|
+
the document has run once — an estimate mislabelled as a count is
|
|
511
|
+
exactly what an honest explain may not print.
|
|
512
|
+
|
|
421
513
|
### The two residual modes
|
|
422
514
|
|
|
423
515
|
- **Row residual** — only the projection is untranslated: predicates,
|
|
@@ -465,7 +557,9 @@ and this is where it says so (MODEL-FORMAT §4). Under `'rtree'` the
|
|
|
465
557
|
names the virtual table. `rank` is `null` or the k-nearest stage —
|
|
466
558
|
`{ column, dims, probe, limit, offset, margin, decides: 'engine' }` —
|
|
467
559
|
what the fetch reads, the window the cut serves, the margin it keeps,
|
|
468
|
-
and who decides the order (always the engine). `
|
|
560
|
+
and who decides the order (always the engine). `series` is `null` or
|
|
561
|
+
the temporal record ("The temporal plan" above), whose `counts` are the
|
|
562
|
+
last ACTUAL run's rather than an estimate. `estimatedRows` is ABSENT on SQLite drivers — the capability slot
|
|
469
563
|
is empty and no number is fabricated. `residual` is `null` or
|
|
470
564
|
`{ mode: 'row' | 'set' | 'knn', reasons: [{ construct, reason }] }` with
|
|
471
565
|
reasons drawn from the deliberate-residual table. With
|
|
@@ -484,8 +578,9 @@ collection binding, so the planner passes that binding's **name** in:
|
|
|
484
578
|
wrapped under any other name every reference would read as an external,
|
|
485
579
|
the determinism rule would reject the fragment, and the hatch would
|
|
486
580
|
silently not engage — no error and no reason in `explain()`.
|
|
487
|
-
Registration is keyed by `
|
|
488
|
-
|
|
581
|
+
Registration is keyed by `semanticKey(fragment)` (the
|
|
582
|
+
order-insensitive identity from `@jarenjs/core/object`) so identical
|
|
583
|
+
fragments share one registration, and the planner MUST
|
|
489
584
|
produce a correct plan with the capability disabled (tested that way).
|
|
490
585
|
Preference order: native SQL → deterministic function → residual, and
|
|
491
586
|
`explain()` names the choice. **Index-form UDFs are deliberately not
|
|
@@ -498,11 +593,15 @@ UDF-expression indexes.
|
|
|
498
593
|
### The statement cache
|
|
499
594
|
|
|
500
595
|
A caller of the core primitives, not an eighth implementation:
|
|
501
|
-
`
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
596
|
+
`createSemanticCache` keyed by the whole discriminating tuple — the
|
|
597
|
+
document plus collection, dialect, strictness, the pushdown switch and
|
|
598
|
+
the profile — where the identity is the tuple's COMPLETE
|
|
599
|
+
serialization, never a fingerprint of it: a 32-bit content hash
|
|
600
|
+
collides after tens of thousands of documents, and a collision here
|
|
601
|
+
answers one query with another query's plan and rows (the
|
|
602
|
+
cache-identity test exists to keep that key abolished).
|
|
603
|
+
`store.stats()` exposes hits, misses and evictions so the cache is
|
|
604
|
+
proven rather than assumed.
|
|
506
605
|
`store.stats()` exposes hits, misses and evictions, so the cache is
|
|
507
606
|
proven rather than assumed.
|
|
508
607
|
|
|
@@ -578,7 +677,7 @@ rollback truncates the journal buffer to its checkpoint. Overhead is
|
|
|
578
677
|
measured and published: capture off ~6µs, journal ~17µs, session ~69µs
|
|
579
678
|
per single-op commit, amortizing across a transaction.
|
|
580
679
|
|
|
581
|
-
## Live queries (`src/live.js`, `src/window.js`)
|
|
680
|
+
## Live queries (`src/live.js`, `src/live-time.js`, `src/window.js`)
|
|
582
681
|
|
|
583
682
|
A live query classifies its document against the normative maintenance
|
|
584
683
|
table by reading the COMPILED PLAN — translated filters, order terms
|
|
@@ -605,6 +704,23 @@ proven by a seeded oracle that holds the maintained result equal to a
|
|
|
605
704
|
fresh re-query after every mutation. Incremental beats re-run 13× at
|
|
606
705
|
1k rows, 51× at 10k.
|
|
607
706
|
|
|
707
|
+
**Event time** (`src/live-time.js`) adds two more strategies for the
|
|
708
|
+
one document shape whose answer is a function of instants rather than
|
|
709
|
+
of rows: a `$resample` bucket state and a `$rolling` window state, both
|
|
710
|
+
over a FIXED width. There is no clock under either — the watermark is a
|
|
711
|
+
finite epoch the host supplies and `advance()` is the only way it moves
|
|
712
|
+
— and both fold through `@jarenjs/core/series` itself rather than
|
|
713
|
+
carrying a second aggregate, so a maintained answer cannot drift from
|
|
714
|
+
what a fresh query gives. A write touches one bucket (two when it
|
|
715
|
+
crosses a boundary) or the windows ending in `[t, t + width)`, and
|
|
716
|
+
exactly those are recomputed. Everything a per-key state cannot place
|
|
717
|
+
exactly re-runs with its member named: a calendar ladder, a named zone,
|
|
718
|
+
a `locf`/`linear` fill, a `first`/`last` aggregate, a retention under
|
|
719
|
+
`width + allowedLateness`. A reading behind the lateness boundary is
|
|
720
|
+
never folded in silently — the view re-reads and the emission carries a
|
|
721
|
+
`lateData` record — which is the one place this layer spends a full
|
|
722
|
+
re-query to keep a promise rather than a number.
|
|
723
|
+
|
|
608
724
|
## Durable runs and the job queue (`src/jobs.js`, `src/dag-job.js`)
|
|
609
725
|
|
|
610
726
|
The queue's whole correctness story is one guarded statement: the
|