@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.
Files changed (72) hide show
  1. package/ARCHITECTURE.md +133 -17
  2. package/README.md +270 -36
  3. package/docs/JOBS-FORMAT.md +24 -8
  4. package/docs/LIVE-FORMAT.md +139 -7
  5. package/docs/MIGRATION-FORMAT.md +118 -36
  6. package/docs/MODEL-FORMAT.md +251 -30
  7. package/package.json +4 -5
  8. package/schemas/jaren-migration.draft-07.schema.json +73 -0
  9. package/schemas/jaren-migration.schema.json +73 -0
  10. package/src/algebra.js +22 -3
  11. package/src/capture.js +66 -28
  12. package/src/cli.js +225 -44
  13. package/src/ddl.js +23 -3
  14. package/src/dialect.js +13 -0
  15. package/src/dialects/sqlite.js +21 -1
  16. package/src/driver.js +63 -16
  17. package/src/drivers/wasm.js +1 -0
  18. package/src/emit-model.js +14 -0
  19. package/src/emit.js +42 -9
  20. package/src/entity.js +92 -47
  21. package/src/errors.js +28 -0
  22. package/src/index.js +2 -2
  23. package/src/jobs.js +40 -5
  24. package/src/live-time.js +605 -0
  25. package/src/live.js +52 -9
  26. package/src/migrate.js +397 -191
  27. package/src/model.js +173 -8
  28. package/src/plan.js +834 -47
  29. package/src/query.js +296 -22
  30. package/src/residual.js +15 -6
  31. package/src/series.js +349 -0
  32. package/src/store.js +243 -69
  33. package/src/tracker.js +173 -48
  34. package/types/index.d.ts +206 -12
  35. package/types/node.d.ts +3 -1
  36. package/types/typed.d.ts +58 -2
  37. package/types/wasm.d.ts +7 -0
  38. package/dist/types/algebra.d.ts +0 -199
  39. package/dist/types/app.d.ts +0 -49
  40. package/dist/types/capture.d.ts +0 -85
  41. package/dist/types/cli.d.ts +0 -2
  42. package/dist/types/dag-job.d.ts +0 -40
  43. package/dist/types/ddl.d.ts +0 -229
  44. package/dist/types/derive.d.ts +0 -250
  45. package/dist/types/dialect.d.ts +0 -149
  46. package/dist/types/dialects/sqlite.d.ts +0 -9
  47. package/dist/types/driver.d.ts +0 -110
  48. package/dist/types/drivers/bun.d.ts +0 -47
  49. package/dist/types/drivers/node.d.ts +0 -37
  50. package/dist/types/drivers/wasm.d.ts +0 -65
  51. package/dist/types/emit-model.d.ts +0 -44
  52. package/dist/types/emit.d.ts +0 -75
  53. package/dist/types/entity.d.ts +0 -23
  54. package/dist/types/errors.d.ts +0 -167
  55. package/dist/types/graph.d.ts +0 -28
  56. package/dist/types/index.d.ts +0 -37
  57. package/dist/types/jobs.d.ts +0 -140
  58. package/dist/types/knn.d.ts +0 -69
  59. package/dist/types/live.d.ts +0 -62
  60. package/dist/types/migrate.d.ts +0 -170
  61. package/dist/types/model.d.ts +0 -36
  62. package/dist/types/patch-sql.d.ts +0 -37
  63. package/dist/types/plan.d.ts +0 -140
  64. package/dist/types/profile.d.ts +0 -80
  65. package/dist/types/query.d.ts +0 -111
  66. package/dist/types/residual.d.ts +0 -61
  67. package/dist/types/store.d.ts +0 -53
  68. package/dist/types/tracker.d.ts +0 -43
  69. package/dist/types/typed.d.ts +0 -15
  70. package/dist/types/types.d.ts +0 -26
  71. package/dist/types/udf.d.ts +0 -75
  72. 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: string
239
- operators, aggregates AND ordering are only promoted on schema-typed
240
- paths because the engine ERRORS on non-conforming operands where SQL
241
- would coerce or sort a stored `null` under an ordered key, a
242
- non-string under a string operator — so on an unvalidated store, rows
243
- violating the collection schema can make the engine throw where the
244
- database answers; keep `compileSchema` injected if that distinction
245
- matters to you.
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). `estimatedRows` is ABSENT on SQLite drivers — the capability slot
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 `contentKey(fragment)`
488
- so identical fragments share one registration, and the planner MUST
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
- `createBoundedCache` keyed by `contentKey(document)` plus collection,
502
- dialect and strictness. `contentKey` is the memo-grade key
503
- (`hashContent(stableStringify(x) ?? '')` drops `undefined` members,
504
- no cycle guard; both properties acceptable for a cache key), never
505
- `canonicalizeJson` (signature-grade, throws on `undefined`).
596
+ `createSemanticCache` keyed by the whole discriminating tuple — the
597
+ document plus collection, dialect, strictness, the pushdown switch and
598
+ the profilewhere 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