@jarenjs/db 0.34.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 (83) hide show
  1. package/ARCHITECTURE.md +397 -0
  2. package/README.md +218 -0
  3. package/dist/types/algebra.d.ts +133 -0
  4. package/dist/types/app.d.ts +49 -0
  5. package/dist/types/capture.d.ts +85 -0
  6. package/dist/types/cli.d.ts +2 -0
  7. package/dist/types/dag-job.d.ts +40 -0
  8. package/dist/types/ddl.d.ts +170 -0
  9. package/dist/types/dialect.d.ts +130 -0
  10. package/dist/types/dialects/sqlite.d.ts +9 -0
  11. package/dist/types/driver.d.ts +128 -0
  12. package/dist/types/drivers/bun.d.ts +47 -0
  13. package/dist/types/drivers/node.d.ts +37 -0
  14. package/dist/types/drivers/wasm.d.ts +65 -0
  15. package/dist/types/emit-model.d.ts +44 -0
  16. package/dist/types/emit.d.ts +72 -0
  17. package/dist/types/entity.d.ts +23 -0
  18. package/dist/types/errors.d.ts +165 -0
  19. package/dist/types/graph.d.ts +28 -0
  20. package/dist/types/index.d.ts +35 -0
  21. package/dist/types/jobs.d.ts +134 -0
  22. package/dist/types/live.d.ts +62 -0
  23. package/dist/types/migrate.d.ts +163 -0
  24. package/dist/types/model.d.ts +36 -0
  25. package/dist/types/patch-sql.d.ts +37 -0
  26. package/dist/types/plan.d.ts +119 -0
  27. package/dist/types/profile.d.ts +80 -0
  28. package/dist/types/query.d.ts +100 -0
  29. package/dist/types/residual.d.ts +50 -0
  30. package/dist/types/store.d.ts +53 -0
  31. package/dist/types/tracker.d.ts +43 -0
  32. package/dist/types/typed.d.ts +15 -0
  33. package/dist/types/types.d.ts +26 -0
  34. package/dist/types/udf.d.ts +70 -0
  35. package/dist/types/window.d.ts +52 -0
  36. package/docs/JOBS-FORMAT.md +218 -0
  37. package/docs/LIVE-FORMAT.md +348 -0
  38. package/docs/MIGRATION-FORMAT.md +302 -0
  39. package/docs/MODEL-FORMAT.md +928 -0
  40. package/package.json +81 -0
  41. package/schemas/jaren-migration.draft-07.schema.json +144 -0
  42. package/schemas/jaren-migration.schema.json +144 -0
  43. package/schemas/jaren-model.draft-07.schema.json +149 -0
  44. package/schemas/jaren-model.schema.json +149 -0
  45. package/src/algebra.js +105 -0
  46. package/src/app.js +108 -0
  47. package/src/capture.js +584 -0
  48. package/src/cli.js +264 -0
  49. package/src/dag-job.js +86 -0
  50. package/src/ddl.js +588 -0
  51. package/src/dialect.js +297 -0
  52. package/src/dialects/sqlite.js +175 -0
  53. package/src/driver.js +419 -0
  54. package/src/drivers/bun.js +101 -0
  55. package/src/drivers/node.js +93 -0
  56. package/src/drivers/wasm.js +178 -0
  57. package/src/emit-model.js +208 -0
  58. package/src/emit.js +393 -0
  59. package/src/entity.js +367 -0
  60. package/src/errors.js +173 -0
  61. package/src/graph.js +101 -0
  62. package/src/index.js +64 -0
  63. package/src/jobs.js +507 -0
  64. package/src/live.js +899 -0
  65. package/src/migrate.js +1411 -0
  66. package/src/model.js +476 -0
  67. package/src/patch-sql.js +150 -0
  68. package/src/plan.js +1038 -0
  69. package/src/profile.js +131 -0
  70. package/src/query.js +1010 -0
  71. package/src/residual.js +91 -0
  72. package/src/store.js +1422 -0
  73. package/src/tracker.js +776 -0
  74. package/src/typed.js +19 -0
  75. package/src/types.js +36 -0
  76. package/src/udf.js +132 -0
  77. package/src/window.js +125 -0
  78. package/types/app.d.ts +36 -0
  79. package/types/bun.d.ts +9 -0
  80. package/types/index.d.ts +592 -0
  81. package/types/node.d.ts +15 -0
  82. package/types/typed.d.ts +108 -0
  83. package/types/wasm.d.ts +5 -0
@@ -0,0 +1,397 @@
1
+ # @jarenjs/db — architecture
2
+
3
+ The public documentation promises SQLite and nothing else. This file
4
+ documents the two seams that make that promise cheap to keep and cheap
5
+ to outgrow — an undocumented seam decays into an accident.
6
+
7
+ ## The driver seam (`src/driver.js`, `src/drivers/*`)
8
+
9
+ A driver is `{ name, dialect, open(path, options) }` returning a
10
+ `Connection` (see `docs/MODEL-FORMAT.md` §4). Three bindings exist,
11
+ one per subpath — `./node` (`node:sqlite`), `./bun` (`bun:sqlite`),
12
+ `./wasm` (an injected handle) — and each imports its runtime builtin
13
+ **lazily inside `open()`**, never at module scope. The release gate
14
+ imports every export subpath under Node *and* Bun; Bun ships no
15
+ `node:sqlite` and Node cannot resolve `bun:` specifiers, so a
16
+ top-level builtin import would turn the gate red in both directions.
17
+ `open()` is where "this binding does not exist here" becomes the coded
18
+ `JD0003`.
19
+
20
+ Every connection and statement method may return a value or a promise.
21
+ The store composes through the sync-capable `chain` helper, which
22
+ applies a continuation without allocating a promise when the driver
23
+ answered with a value — the public asynchronous surface then lifts
24
+ exactly once per call. Where the connection is synchronous the same
25
+ operation cores are exposed promise-free under `store.sync`; where it
26
+ is not, `store.sync` is absent rather than stubbed.
27
+
28
+ Capabilities are probed once at open (version, compile options,
29
+ binding declaration) and frozen. Two slots — `statementTimeout` and
30
+ `rowEstimates` — are `false` on every SQLite driver because the
31
+ underlying facts do not exist there (no interrupt, no progress
32
+ handler, no estimate API). They are part of the contract so a driver
33
+ that has the facts can fill them without a contract change.
34
+
35
+ ## The dialect seam (`src/dialect.js`, `src/dialects/sqlite.js`)
36
+
37
+ A dialect is data plus a small emitter, and it is the only place SQL
38
+ text is produced. `createDialect(spec)` composes a spelling spec —
39
+ identifier quoting, parameter references, string literals, JSON
40
+ extract/set/remove/append, transaction phrases, PRAGMA phrases,
41
+ introspection queries, the table suffix and document column type —
42
+ into the DDL and DML builders the store consumes. Nothing outside a
43
+ dialect concatenates SQL; a reviewer can point at every emitted
44
+ statement and find it behind the seam. The suite pins this with a
45
+ test-double dialect whose quoting, parameter style and type names all
46
+ differ: the same model produces correspondingly different SQL.
47
+
48
+ Deliberately *not* dialect concerns, because they are behavioural
49
+ rather than syntactic, and live in the capability table instead:
50
+ whether functions register per connection, whether change capture
51
+ exists and in what form, and whether tables can be restructured in
52
+ place.
53
+
54
+ The SQLite dialect stores documents as JSONB in a `BLOB` column of a
55
+ `STRICT` table, projects each indexed path into a virtual generated
56
+ column over `jsonb_extract`, and renders reads back to text through
57
+ `json()`. Parameters are positional because every shipped binding
58
+ binds arrays.
59
+
60
+ ## What sits on top
61
+
62
+ - `src/ddl.js` — a normalized collection to its physical plan. Index
63
+ paths are analyzed through the query engine's **published AST**
64
+ (`analyzeQuery`): singularity decides indexability (`JD0004`
65
+ otherwise), and the collection's schema is the type source for
66
+ generated columns. The plan carries both the CREATE statements and
67
+ the structural facts an existing table must match (`JD0002` when it
68
+ does not — nothing is ever altered).
69
+ - `src/patch-sql.js` — RFC 6902 to JSON-set primitives, discriminated
70
+ against the live document (a pointer cannot say array-or-object on
71
+ its own). Untranslatable operations fall back to a whole-document
72
+ write; the store counts and exposes the fallback.
73
+ - `src/store.js` — model normalization (`JD0005`), shape
74
+ create-or-verify, the validated write path (injected
75
+ `compileSchema` hook; `capabilities.validated` says whether it is
76
+ in force), identity strategies, savepoint-nested transactions, and
77
+ the sync surface.
78
+
79
+ Dependency arrows: `core → json → db`, plus `validate → db` for
80
+ exactly ONE reason: the entity walk resolves `$ref` and merges `allOf`
81
+ through the schema resolvers `@jarenjs/validate/normalize` exports for
82
+ other packages to walk schemas with (emit already does). Validation
83
+ itself still arrives only through the injected `compileSchema` hook —
84
+ this package never calls the validator. No linq, no app, no view, no
85
+ flow. The linq package couples by contract (`execute(document,
86
+ options)`), never by import.
87
+
88
+ ## The decisions that cost something
89
+
90
+ - **D3 — the normalized AST is a compatibility surface.** The planner
91
+ walks the engine's PUBLISHED AST, which promoted an internal shape
92
+ to a versioned contract the engine must now keep. Paid for: a
93
+ load-time exhaustiveness pact means a new language construct breaks
94
+ the build instead of silently becoming a residual.
95
+ - **D6 — the store API is asynchronous** even though every shipped
96
+ driver is synchronous. Measured cost: ~0.17 µs per point read
97
+ (~6 %) over the `store.sync` twin. Paid for: the browser's OPFS
98
+ story and any future non-embedded driver need no API change, and
99
+ the sync-capable `chain` keeps the internal composition
100
+ allocation-free so the surface pays exactly one promise per call.
101
+ - **D21 — the dialect indirection is paid for a second backend that
102
+ does not exist.** Every byte of SQL routes through a spelling spec;
103
+ the test double proves the seam by rendering the same plans and the
104
+ same DDL differently. The cost is one indirection and a larger
105
+ contract; the alternative was a rewrite on the day a second dialect
106
+ matters — and the capability slots (`statementTimeout`,
107
+ `rowEstimates`) already model what a server would fill in.
108
+
109
+ ## The pushdown contract (`src/plan.js`, `src/emit.js`, `src/query.js`)
110
+
111
+ This is an implementation contract, not a format: it binds the planner,
112
+ the emitter and their tests, and `explain()` is its runtime witness.
113
+
114
+ **Three stages, and the middle one is dialect-neutral.** A query
115
+ document is analyzed to the engine's published AST, the AST becomes a
116
+ `Plan` — a relational algebra value carrying **no SQL text** (a test
117
+ scans plan output for dialect tokens) — and the plan renders to SQL
118
+ through a dialect. What cannot be proven equivalent runs as a
119
+ **residual**: a real compiled Jaren query, never a reimplementation.
120
+ `explain()` always says which is which.
121
+
122
+ **Dispatch is exhaustive.** The planner switches on the AST's node
123
+ kinds with no default branch that degrades silently: an unrecognised
124
+ kind is an internal error naming the kind and the `AST_VERSION`. A
125
+ construct that is *deliberately* not translated is a row in the table
126
+ below, and that table's reasons are what `explain()` reports.
127
+
128
+ ### The translated set
129
+
130
+ A single-binding FLWOR over the collection (`$for: { it: '$[*]' }`)
131
+ with: comparison predicates (`$eq $ne $lt $le $gt $ge`) between a
132
+ singular member path and a literal or external; `$and`/`$or`/`$not`
133
+ composition; `$exists`/`$empty`; `$starts-with`/`$ends-with`/
134
+ `$contains` on schema-typed string paths with literal patterns;
135
+ `$orderby` over singular schema-typed paths (`$dir`, `$empty`, no
136
+ collation); a top-level `$subsequence` window with literal bounds; the
137
+ top-level aggregates `$count` (bare-binding return only) and
138
+ `$sum`/`$avg`/`$min`/`$max` over a singular schema-typed path; and the
139
+ whole-document projection `$return: '$it'`.
140
+
141
+ ### The deliberate-residual table
142
+
143
+ | construct | reason |
144
+ |---|---|
145
+ | `$let` bindings, `$fold`, `$groupby`, positional/window bindings | no equivalence proof exists yet; residual by default |
146
+ | a second `$for` binding, joins, non-singular path expansion | one relation per plan in this version |
147
+ | `$match` and other unlisted operators, `$call` | no native spelling proven equivalent |
148
+ | `$orderby` with a `$collation` | a collation the dialect cannot reproduce is refused, not approximated |
149
+ | projections other than `'$it'` | run per row (the row residual) — pushed, ordered and windowed rows, projected by the engine |
150
+ | string operators with an external pattern | the pattern's type is unknowable at plan time and the engine ERRORS on non-string patterns |
151
+ | comparisons where both sides are paths | join territory |
152
+ | array/object literals in comparisons | deep-equality has no guarded native form |
153
+ | spatial predicates | no spatial index vocabulary in the model format |
154
+
155
+ ### The type truth table
156
+
157
+ Jaren compares by JSON type; SQL engines by storage class and
158
+ affinity. `jsonb_extract` returns SQL `NULL` for a missing member AND
159
+ for a stored JSON `null` — only `json_type` tells them apart, so every
160
+ translated predicate is guarded by it and is therefore **total
161
+ (two-valued)**: `NOT`, `AND` and `OR` compose classically and SQL's
162
+ three-valued `NULL` logic never reaches a result row. Probed engine
163
+ facts the table encodes: a comparison against a missing member is
164
+ `false` (even `$ne`); a cross-type `$eq` is `false` and a cross-type
165
+ `$ne` against a PRESENT value is `true`; booleans do not order;
166
+ string order is codepoint order (which is exactly SQLite's BINARY
167
+ order over UTF-8);
168
+ `$exists` is true for a stored `null`, `$empty` is true for a missing
169
+ member.
170
+
171
+ With `jt` = `json_type(doc, path)`, `v` = the compared value (the
172
+ generated column where one exists), `?` = the bound operand:
173
+
174
+ | Jaren predicate | emitted form |
175
+ |---|---|
176
+ | `$eq` path, number | `jt IN ('integer','real') AND v = ?` |
177
+ | `$ne` path, number | `jt IS NOT NULL AND (jt NOT IN ('integer','real') OR v <> ?)` |
178
+ | `$lt/$le/$gt/$ge` path, number | `jt IN ('integer','real') AND v op ?` |
179
+ | `$eq` path, string | `jt = 'text' AND v = ?` |
180
+ | `$ne` path, string | `jt IS NOT NULL AND (jt <> 'text' OR v <> ?)` |
181
+ | `$lt/$le/$gt/$ge` path, string | `jt = 'text' AND v op ?` |
182
+ | `$eq` path, `true`/`false` | `jt = 'true'` / `jt = 'false'` (no value bind) |
183
+ | `$ne` path, `true`/`false` | `jt IS NOT NULL AND jt <> 'true'/'false'` |
184
+ | `$eq` path, `null` | `jt = 'null'` |
185
+ | `$ne` path, `null` | `jt IS NOT NULL AND jt <> 'null'` |
186
+ | ordering vs `true/false/null` literal | constant `FALSE` (booleans and nulls do not order) |
187
+ | `$exists` path | `jt IS NOT NULL` |
188
+ | `$empty` path | `jt IS NULL` |
189
+ | `$eq` path, external | `(jt = 'text' AND typeof(?) = 'text' AND v = ?) OR (jt IN ('integer','real') AND typeof(?) IN ('integer','real') AND v = ?)` |
190
+ | `$ne` path, external | `jt IS NOT NULL AND NOT (…the $eq form…)` |
191
+ | ordering vs external | the same two-branch form with `op` |
192
+ | `$starts-with` path, string | `jt = 'text' AND substr(v, 1, length(?)) = ?` |
193
+ | `$ends-with` path, string | `jt = 'text' AND (length(?) = 0 OR substr(v, -length(?)) = ?)` |
194
+ | `$contains` path, string | `jt = 'text' AND instr(v, ?) > 0` |
195
+
196
+ The guards make the forms sound for typed AND untyped paths alike —
197
+ the schema type's job is choosing the generated COLUMN (the index),
198
+ never weakening the guard. Two documented preconditions: string
199
+ operators, aggregates AND ordering are only promoted on schema-typed
200
+ paths because the engine ERRORS on non-conforming operands where SQL
201
+ would coerce or sort — a stored `null` under an ordered key, a
202
+ non-string under a string operator — so on an unvalidated store, rows
203
+ violating the collection schema can make the engine throw where the
204
+ database answers; keep `compileSchema` injected if that distinction
205
+ matters to you.
206
+
207
+ **Bind-time diversion.** SQLite cannot bind a boolean, and a
208
+ `null`-valued external needs Jaren's null semantics, not SQL's. At
209
+ execute time, if any referenced external is missing or not a string or
210
+ finite number, the call runs the always-compiled set residual instead
211
+ of the native statement — same answer, one branch, no wrong-typed SQL.
212
+
213
+ ### The two residual modes
214
+
215
+ - **Row residual** — only the projection is untranslated: predicates,
216
+ ordering and the window are fully pushed; each fetched row runs
217
+ `{ $for: { it: '$[*]' }, $return: [ <the document's $return> ] }`
218
+ (the array wrapper keeps array-valued items unambiguous) and the
219
+ items concatenate in row order. Streams.
220
+ - **Set residual** — anything else: the pushed predicate conjuncts
221
+ narrow candidates (`$and` splits; a partially translatable `$or`
222
+ does not), and the WHOLE original compiled document runs over the
223
+ materialized candidate array. Re-applying pushed conjuncts is
224
+ idempotent, so pushdown is pure narrowing. Reported as a barrier.
225
+
226
+ ### `explain()`
227
+
228
+ Extends `compileJsonQuery(...).explain()`'s shape — `{ externals,
229
+ operators, functions, collations, limits }` — with `{ sql, params,
230
+ indexes, residual, barriers, scanNarrative }`. `params` lists the
231
+ bound slots in order (external names and literal markers — values are
232
+ ALWAYS bound, never interpolated). `indexes` names the declared
233
+ indexes whose generated columns the pushed predicates and ordering
234
+ touch, and the `scanNarrative` is the database's own `EXPLAIN QUERY
235
+ PLAN` prose so the claim is checkable against the engine that will run
236
+ it. `estimatedRows` is ABSENT on SQLite drivers — the capability slot
237
+ is empty and no number is fabricated. `residual` is `null` or
238
+ `{ mode: 'row' | 'set', reasons: [{ construct, reason }] }` with
239
+ reasons drawn from the deliberate-residual table. With
240
+ `strict: true`, any residual is instead the compile error `JD0010`
241
+ naming the forcing construct.
242
+
243
+ ### The UDF escape hatch (capability-gated)
244
+
245
+ Between native SQL and pulling rows sits registering a compiled
246
+ predicate conjunct as a deterministic function used in the `WHERE`
247
+ clause. Gated on `capabilities.userFunctions` (absent on Bun by
248
+ construction) and applied only to fragments with no externals, no
249
+ functions and no collations — deterministic and side-effect-free by
250
+ analysis, not by hope. Registration is keyed by `contentKey(fragment)`
251
+ so identical fragments share one registration, and the planner MUST
252
+ produce a correct plan with the capability disabled (tested that way).
253
+ Preference order: native SQL → deterministic function → residual, and
254
+ `explain()` names the choice. **Index-form UDFs are deliberately not
255
+ part of this**: an index over a registered function makes the database
256
+ unwritable from any connection that has not registered the identical
257
+ function — a WHERE-clause UDF carries no such schema dependency, and
258
+ that operational hazard is why the model format declares no
259
+ UDF-expression indexes.
260
+
261
+ ### The statement cache
262
+
263
+ A caller of the core primitives, not an eighth implementation:
264
+ `createBoundedCache` keyed by `contentKey(document)` plus collection,
265
+ dialect and strictness. `contentKey` is the memo-grade key
266
+ (`hashContent(stableStringify(x) ?? '')` — drops `undefined` members,
267
+ no cycle guard; both properties acceptable for a cache key), never
268
+ `canonicalizeJson` (signature-grade, throws on `undefined`).
269
+ `store.stats()` exposes hits, misses and evictions, so the cache is
270
+ proven rather than assumed.
271
+
272
+ ## The relational half (`src/model.js`, `src/plan.js` §entities, `src/query.js`)
273
+
274
+ The same planner, a second document kind: entity query documents
275
+ address the multi-entity root (`$.User[*]`), the ONLY shape a
276
+ differential oracle can prove (the engine has no embedded relation
277
+ members to walk — which is why relation-name sugar is deliberately
278
+ absent from the query surface and lives on `load`). Three reference
279
+ flavors decide emission: entity COLUMNS get total forms with no
280
+ `json_type` guard (a mapped property has no present-null), entity
281
+ EPOCH columns get a ±1 s index range plus the exact document-string
282
+ recheck (mixed stored precisions can never diverge from the engine's
283
+ codepoint order), and everything else rides the phase-A guarded truth
284
+ table aliased per binding. Two-binding equijoins emit INNER JOIN with
285
+ binding-order row-identity tiebreakers — exactly the engine's
286
+ nested-loop order. The graph loader compiles include trees to
287
+ correlated `json_group_array`/`json_object` subqueries: one statement
288
+ per load, proven by a counting driver, never promised.
289
+
290
+ ## The unit of work (`src/tracker.js`)
291
+
292
+ Materialised entities are deep-frozen plain JSON and the frozen
293
+ document IS the snapshot — one retained reference, structural sharing
294
+ made safe by the freeze. `saveChanges()` diffs with the suite's own
295
+ diff engine and maps operations to minimal statements (column writes,
296
+ `jsonb_set` chains, join-table key-set sync, a counted whole-row
297
+ fallback); inserts batch parent-first, deletes run child-first,
298
+ foreign-key cycles among the changed set are `JD0040`, and a declared
299
+ `version` property turns every update into an optimistic
300
+ `WHERE version = ?` with `JD2040` on conflict. The tracker mutates
301
+ ONLY after commit: a failed save retries.
302
+
303
+ ## The migration engine, relationally (`src/migrate.js`, `src/cli.js`)
304
+
305
+ The strategy-table diff renders SELF-CONTAINED steps — additive
306
+ columns, data steps, and one rebuild implementation following
307
+ SQLite's documented twelve-step procedure with `foreign_key_check`
308
+ inside the transaction. Shape EQUALITY (schemaShapeOf versus a fresh
309
+ createModelShape build) is the acceptance criterion, asserted on the
310
+ shadow before the real database is touched and again after. Probed
311
+ and designed around: node:sqlite enables foreign keys BY DEFAULT (the
312
+ pragma bracket is load-bearing), `jsonb()` PARSES its argument
313
+ (column folds pass plain SQL values), and a table rename does not
314
+ rename columns (join tables rename their endpoint keys explicitly).
315
+
316
+ ## One cross-runtime seam worth remembering
317
+
318
+ `bun:sqlite` answers **null** for a missing row where `node:sqlite`
319
+ answers undefined; the bun adapter normalizes at the seam (and the
320
+ bun-shaped test double mimics the null so the packed run pins it).
321
+ Found by the ORM benchmark's first real-Bun file-store open — the
322
+ in-memory tests never reopen a database, so create-or-verify had
323
+ never seen bun's null.
324
+
325
+ ## Change capture (`src/capture.js`)
326
+
327
+ Every committed write becomes an ordered stream of RFC 6902 patches.
328
+ Two sources behind one contract: SQLite's **session changeset** where
329
+ the binding exposes `createSession` (node:sqlite), parsed from its
330
+ binary format by a hand-written decoder; a **write-path journal**
331
+ where it does not (bun:sqlite, the wasm build), buffering before/after
332
+ documents as the store writes them. Both net ONE op per row — insert+
333
+ update coalesces, insert+delete vanishes, an update back to the
334
+ original emits nothing — so the two modes agree as SETS (a differential
335
+ test pins it). The journal's netting was added when the wasm parity
336
+ suite ran a same-row multi-op transaction the original differential
337
+ script never wrote. Op order within a record is UNSPECIFIED; every op targets
338
+ a distinct pointer. The persisted `_jaren_changes` log rides the same
339
+ transaction as the writes it describes; a caught inner-savepoint
340
+ rollback truncates the journal buffer to its checkpoint. Overhead is
341
+ measured and published: capture off ~6µs, journal ~17µs, session ~69µs
342
+ per single-op commit, amortizing across a transaction.
343
+
344
+ ## Live queries (`src/live.js`, `src/window.js`)
345
+
346
+ A live query classifies its document against the normative maintenance
347
+ table by reading the COMPILED PLAN — translated filters, order terms
348
+ and aggregates are exactly the planner's, never re-derived. Five
349
+ strategies: incremental **rows** (per-key items, arrival order), the
350
+ maintained **window** (all matching rows sorted, ties by key token, a
351
+ delete inside the visible slice answered without re-query), running
352
+ **accumulators** (per-row contributions retained so a capture `remove`
353
+ — which carries no old value — is still answerable; min/max recompute
354
+ over contributions when the extremum's holder leaves), per-group
355
+ **deltas** (the accumulator machinery once per group), and **re-run**
356
+ for everything else — declared, reported through `live.mode`, never
357
+ silent. Invalidation matches a record by table plus pointer prefix,
358
+ over-approximating toward re-evaluation (a missed update would be a
359
+ correctness bug; an extra one is only slower). Emissions preserve
360
+ reference identity for untouched rows — the O(k) renderer's contract —
361
+ proven by a seeded oracle that holds the maintained result equal to a
362
+ fresh re-query after every mutation. Incremental beats re-run 13× at
363
+ 1k rows, 51× at 10k.
364
+
365
+ ## Durable runs and the job queue (`src/jobs.js`, `src/dag-job.js`)
366
+
367
+ The queue's whole correctness story is one guarded statement: the
368
+ claim UPDATE selects the earliest eligible row (pending, failed, or an
369
+ expired lease — so recovery IS the next claim, not a sweeper) whose
370
+ kind the worker registered, sets it leased with a deadline, and
371
+ RETURNs it. One statement is one transaction, so no two workers claim
372
+ the same job without any distributed lock; every later transition
373
+ wears `state='leased' AND lease_owner=?`, making execution
374
+ at-least-once and completion exactly-once (proven by four workers on
375
+ four connections over one WAL file). The `@jarenjs/flow` composition
376
+ injects `compileDag` (db never imports flow — an import-graph test
377
+ enforces it) and binds a per-job checkpoint store; a DAG run's
378
+ completion records the result, marks the job done and prunes the
379
+ checkpoint rows in ONE transaction, so a crash resumes from its
380
+ checkpointed nodes rather than restarting. Non-goals stated plainly: a
381
+ shared SQLite file over a network filesystem is not a safe
382
+ coordination substrate.
383
+
384
+ ## The wasm driver (`src/drivers/wasm.js`)
385
+
386
+ An injected handle, never an import: the host loads the official
387
+ SQLite wasm build and this driver adapts its `oo1` object API — which
388
+ is SYNCHRONOUS in a dedicated worker over the SAH-pool OPFS VFS, which
389
+ is exactly what keeps journal capture, live queries and the job queue
390
+ working unchanged in a browser. The engine parity is proven in Node
391
+ against the real wasm bytes (the full pushdown oracle, entities, live
392
+ queries, jobs, a shadow-verified migration); the browser suite proves
393
+ the ENVIRONMENT — OPFS persistence across reloads, the owner topology
394
+ (one context holds the sole connection, tabs are clients over a
395
+ BroadcastChannel), and the second-writer refusal — across Chromium,
396
+ Firefox and WebKit, with the memory fallback stated where OPFS is
397
+ absent.
package/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # @jarenjs/db
2
+
3
+ Documents AND entities in SQLite. A **model document** declares
4
+ collections (a JSON Schema, a key, indexes) and — since phase B —
5
+ **entities**: keys, typed columns, relations, defaults and an
6
+ optimistic-concurrency token, all inside the schema through the
7
+ `x-entity` vocabulary. `openStore` applies the physical mapping
8
+ through a dialect and gives transactional, schema-validated reads and
9
+ writes; queries arrive as plain Jaren query documents (usually
10
+ written through `@jarenjs/linq`) and are **pushed down to SQL** where
11
+ equivalence is proven, with everything else running honestly in the
12
+ engine. The same code runs on Node, on Bun, and in a browser against
13
+ an injected wasm handle, with zero dependencies outside `@jarenjs/*`.
14
+
15
+ **The honest framing, first**: Prisma, Drizzle and Kysely are mature,
16
+ support several databases, and are faster on some benchmark rows —
17
+ those losses are published on the suite page with their reasons. What
18
+ none of them has is a query that is one serializable JSON document,
19
+ executable by two independent engines proven to agree by a
20
+ differential oracle, running unchanged in Node, Bun and the browser,
21
+ with schema-validated writes from the fastest validator in the
22
+ ecosystem. The composition is the product; the individual numbers are
23
+ what they are.
24
+
25
+ ```js
26
+ import { openStore } from '@jarenjs/db';
27
+ import { nodeDriver } from '@jarenjs/db/node';
28
+
29
+ const store = await openStore({
30
+ $model: '0.1',
31
+ collections: {
32
+ users: {
33
+ schema: {
34
+ type: 'object',
35
+ required: ['id', 'email'],
36
+ properties: {
37
+ id: { type: 'string' },
38
+ email: { type: 'string', format: 'email' },
39
+ age: { type: 'integer' },
40
+ },
41
+ },
42
+ key: '/id',
43
+ indexes: [{ name: 'by_age', path: '$.age' }],
44
+ },
45
+ },
46
+ }, { driver: nodeDriver(), path: 'app.db' });
47
+
48
+ const users = store.collection('users');
49
+ await users.insert({ id: 'u1', email: 'ada@example.test', age: 36 });
50
+
51
+ // a query document — here written by hand; linq writes the same thing
52
+ const adults = await users.execute({
53
+ $for: { it: '$[*]' },
54
+ $where: { $ge: ['$it.age', 21] },
55
+ $return: '$it',
56
+ });
57
+ ```
58
+
59
+ - **The pushdown planner with `explain()`.** A query compiles through
60
+ the engine's published AST into a dialect-neutral plan and renders
61
+ to guarded, parameter-bound SQL; whatever cannot be proven
62
+ equivalent runs as a real compiled Jaren query (the residual), and
63
+ `explain()` always says which is which — the SQL, the bound
64
+ parameters, the indexes used (verified against the database's own
65
+ plan output), and the residual's named reasons. A 418-run
66
+ differential oracle keeps both paths agreeing. `strict: true` turns
67
+ any residual into a compile error.
68
+ - **Registered operators, correct in the residual, pushed where it
69
+ pays.** Open with a registry (`operators:
70
+ createJsltRegistry().use(mathPack).use(financePack)`) and a query may
71
+ use `$npv`, `$mean`, `$sqrt`, … over stored documents. Each runs
72
+ correctly in the residual — identical to the in-memory engine,
73
+ `explain()` names it. The `pushable:'scalar'` subset (the math ops) is
74
+ additionally accelerated into SQLite as **deterministic UDFs** where
75
+ the driver allows (node:sqlite yes; bun:sqlite has no UDF API and stays
76
+ the residual — reported by `capabilities.pushableOperators`). Pushdown
77
+ is a large win beside a selective native predicate or a `LIMIT` (3.9×,
78
+ 615× measured) and a wash on a solo full-table computed predicate —
79
+ published honestly in MODEL-FORMAT §8.2, not gated behind a cost model
80
+ SQLite gives no row estimates to build. A first-class operator is
81
+ allowed under a profile by default (which also blocks host-side UDF
82
+ registration for untrusted documents); the profile's `functions`
83
+ allow-list still governs a `$call`-reached `fn`; the row bound still
84
+ fires. Without a registry the store is unchanged — `$npv` is `JQ0002`.
85
+ - **Storage is declarative.** Indexed paths become generated columns
86
+ plus real indexes, typed from the collection's schema. Opening an
87
+ existing database verifies the declared shape and refuses to alter
88
+ it — reshaping is the migration story.
89
+ - **Migrations are documents.** `planMigration` diffs two models into
90
+ rendered-DDL + JSLT-transform + assertion steps; a shadow database
91
+ replays the whole chain before the real store is touched; a
92
+ checksummed history refuses edited or reordered migrations; a
93
+ narrowing without an adequate transform is refused against the REAL
94
+ data, inside the transaction.
95
+ - **The safe profile.** Untrusted query documents run under composed
96
+ bounds: engine limits on the residual, a mandatory row bound that
97
+ refuses rather than truncates, reference allow-lists, optional
98
+ full-scan refusal, and per-collection mandatory predicates no
99
+ document shape can shed. Read-only stores refuse writes at the
100
+ driver.
101
+ - **Writes validate** through an injected hook; without one,
102
+ `store.capabilities.validated` is `false` and the docs say what that
103
+ costs. The public API is asynchronous (the browser's OPFS story
104
+ forces it) with a promise-free `store.sync` twin where the driver is
105
+ synchronous.
106
+
107
+ ## What SQLite-only means, frankly
108
+
109
+ SQLite is the supported backend — 3.45 or newer, on `node:sqlite`,
110
+ `bun:sqlite`, or your injected wasm build — and nothing else is
111
+ promised. The dialect seam exists and is tested against a double, but
112
+ no second dialect ships. Concretely: there is **no statement timeout**
113
+ (the drivers expose no interrupt; the capability slot is honestly
114
+ `false`), no server, no replication, and cross-process concurrency is
115
+ SQLite's own story (WAL plus a busy timeout, both set and visible on
116
+ `store.capabilities`).
117
+
118
+ ## The relational half (phase B)
119
+
120
+ - **Entities and relations** (`x-entity`, MODEL-FORMAT §9): hybrid
121
+ rows — key and mapped scalar columns beside one JSONB document —
122
+ with real foreign keys (`PRAGMA foreign_keys` set AND verified),
123
+ all three relation kinds, and derived epoch columns for indexed
124
+ instant ranges.
125
+ - **One-statement graph loads** (§10): `entity('User').load({ include:
126
+ { posts: { include: { comments: true } } } })` runs in exactly ONE
127
+ statement regardless of depth or parent count — asserted by a
128
+ counting driver in the tests, and published with statement counts
129
+ beside the timings on the benchmark page. Keyset pagination when
130
+ the ordering allows it, reported, never silent.
131
+ - **The unit of work** (§11): reads are plain deep-frozen JSON (no
132
+ proxies, asserted); mutation is replacement; `saveChanges()` diffs
133
+ snapshots into minimal parameterised statements in one transaction,
134
+ with insert batching, `JD0040` cycle refusal, `JD2040` optimistic
135
+ conflicts, and a report of every statement, fallback and count.
136
+ - **Generated types**: `entityEmitModel` + `@jarenjs/emit` render the
137
+ model into entity interfaces, input variants and an `EntityMetaMap`;
138
+ `typedStore` (from `@jarenjs/db/typed`) types every read, checks
139
+ every write, and widens `load` results by their include
140
+ specification.
141
+ - **Relational migrations and the `jaren-db` CLI** (MIGRATION-FORMAT
142
+ §§9–12): the strategy-table diff, the documented twelve-step table
143
+ rebuild with `foreign_key_check` inside the transaction, shape
144
+ EQUALITY against a fresh build as the acceptance criterion, drift
145
+ detection, and `jaren-db check` for CI.
146
+
147
+ ## The reactive and durable half (phase C)
148
+
149
+ - **Change capture** (LIVE-FORMAT §§1–6): every committed write
150
+ becomes an observable stream of RFC 6902 patches — from SQLite's
151
+ own session changesets where the binding has them, from a write-path
152
+ journal where it does not (`bun:sqlite`, the wasm build). One diff
153
+ format runs store → patch → live query → O(k) render. Capture is
154
+ opt-in; the overhead is published, not waved away.
155
+ - **Live queries** (LIVE-FORMAT §§7–12): `collection.live(document)`
156
+ maintains a result as writes arrive and emits patches — incremental
157
+ for `where`/`select`/`orderBy`+`limit`/aggregates/single-level
158
+ `groupBy` (the normative maintenance table), re-run for everything
159
+ else, **declared, never silent** (`live.mode` names the reason).
160
+ Unaffected rows stay reference-identical; a seeded oracle holds the
161
+ maintained result equal to a fresh re-query after every mutation.
162
+ - **Durable runs and the job queue** (JOBS-FORMAT, FLOW-FORMAT §7.6):
163
+ a `@jarenjs/flow` DAG run checkpoints declared nodes and RESUMES
164
+ after a crash; `store.jobs` leases work in one guarded statement
165
+ (exactly-once completion, no distributed lock), retries with
166
+ backoff, dead-letters, and reclaims expired leases as recovery.
167
+ - **The browser** (`@jarenjs/db/wasm`): the same store, the same
168
+ queries, the same live updates run on the official SQLite wasm build
169
+ over the header-free OPFS SAH-pool VFS — one tab owns the
170
+ connection, others are clients. Proven in the `#/data` studio across
171
+ Chromium, Firefox and WebKit.
172
+
173
+ ## Sync-readiness — what exists and what does not
174
+
175
+ The change stream is an ordered log of RFC 6902 patches with a
176
+ monotonic sequence, and SQLite's own changeset/conflict primitives are
177
+ available — which is what a replication protocol would be *built
178
+ from*. **No replication is shipped.** There is no conflict resolution,
179
+ no site identity, no causal ordering across writers, and no capture of
180
+ writes made by another connection (the coarse `dataVersion()` signal
181
+ is the honest mitigation, not a pretend fine-grained one). Building
182
+ replication on these primitives is a roadmap item, not a hint.
183
+
184
+ ## What this is not — every non-claim in one place
185
+
186
+ - **SQLite only.** One backend (3.45+); the dialect seam is tested
187
+ against a double but no second dialect ships. No server.
188
+ - **No replication or sync engine** (see above). No cross-connection
189
+ change capture — another connection's writes are invisible locally.
190
+ - **No statement timeout** on SQLite (the drivers expose no interrupt;
191
+ the capability slot is honestly `false`), no row estimates.
192
+ - **Not safe for mutually hostile tenants** without the profile's
193
+ mandatory predicate — SECURITY states the claims and non-claims.
194
+ - **The job queue is one database, one machine.** A shared SQLite file
195
+ over a NETWORK FILESYSTEM (NFS, SMB, many container volume mounts) is
196
+ NOT a safe coordination substrate — SQLite's locking is unreliable
197
+ there. Same-host processes over WAL are the supported topology. No
198
+ priority classes, no cron, no workflow compensation.
199
+ - **Live-query maintenance is limited to the declared table** (§7);
200
+ joins, entity queries and non-canonical shapes re-run, reported.
201
+ - **The wasm build journals** (its session extension is not yet
202
+ adapted); OPFS needs a secure context, and where it is absent the
203
+ store runs in memory with the durability difference stated.
204
+ - **Named future work, not silent gaps**: `$groupby` pushdown,
205
+ relation-name query sugar, a many-to-many membership API, incremental
206
+ joins, other SQL dialects, replication, database introspection
207
+ (MODEL-FORMAT §10.6, the roadmap).
208
+
209
+ The normative formats are
210
+ [docs/MODEL-FORMAT.md](docs/MODEL-FORMAT.md) (storage §§1–7, safe
211
+ profile §8, entities §9, relational translation §10, the unit of work
212
+ §11), [docs/MIGRATION-FORMAT.md](docs/MIGRATION-FORMAT.md) (documents
213
+ §§1–8, relational changes §§9–12),
214
+ [docs/LIVE-FORMAT.md](docs/LIVE-FORMAT.md) (capture §§1–6, live queries
215
+ §§7–12) and [docs/JOBS-FORMAT.md](docs/JOBS-FORMAT.md) (the durable
216
+ queue §§1–9); the seams, the pushdown contract and every engine are in
217
+ [ARCHITECTURE.md](ARCHITECTURE.md); the benchmark methodology is in
218
+ [benchmark/README.md](../../benchmark/README.md).