@jarenjs/db 0.56.0 → 0.66.1

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 (68) hide show
  1. package/ARCHITECTURE.md +393 -56
  2. package/README.md +585 -53
  3. package/docs/HOSTS.md +269 -0
  4. package/docs/JOBS-FORMAT.md +293 -45
  5. package/docs/LIVE-FORMAT.md +122 -14
  6. package/docs/MIGRATION-FORMAT.md +142 -17
  7. package/docs/MODEL-FORMAT.md +744 -64
  8. package/package.json +21 -7
  9. package/schemas/jaren-model.draft-07.schema.json +224 -162
  10. package/schemas/jaren-model.schema.json +224 -162
  11. package/src/algebra.js +227 -9
  12. package/src/backup.js +161 -0
  13. package/src/cancellation.js +48 -0
  14. package/src/capture.js +218 -45
  15. package/src/cli.js +165 -59
  16. package/src/cursor.js +411 -0
  17. package/src/dag-job.js +154 -21
  18. package/src/ddl.js +102 -8
  19. package/src/dialect.js +267 -112
  20. package/src/dialects/expression-read.js +158 -0
  21. package/src/dialects/postgres.js +618 -0
  22. package/src/dialects/rtree-ddl.js +129 -0
  23. package/src/dialects/sqlite.js +243 -11
  24. package/src/document-files.js +311 -0
  25. package/src/document-steps.js +422 -0
  26. package/src/documents.js +335 -0
  27. package/src/driver.js +448 -61
  28. package/src/drivers/bun.js +37 -1
  29. package/src/drivers/indexeddb-snapshot.js +149 -0
  30. package/src/drivers/node-pool.js +11 -0
  31. package/src/drivers/node-worker-endpoint.js +105 -0
  32. package/src/drivers/node-worker.js +204 -0
  33. package/src/drivers/node.js +41 -7
  34. package/src/drivers/postgres.js +331 -0
  35. package/src/drivers/wasm-oo1.js +97 -0
  36. package/src/drivers/wasm-session.js +67 -0
  37. package/src/drivers/wasm.js +17 -83
  38. package/src/drivers/worker-pool.js +183 -0
  39. package/src/drivers/worker-protocol.js +79 -0
  40. package/src/drivers/worker-queue.js +60 -0
  41. package/src/emit.js +339 -48
  42. package/src/entity.js +20 -22
  43. package/src/errors.js +422 -19
  44. package/src/expression.js +284 -0
  45. package/src/graph.js +64 -8
  46. package/src/index.js +46 -17
  47. package/src/introspect.js +583 -0
  48. package/src/jobs.js +843 -107
  49. package/src/json-bytes.js +58 -0
  50. package/src/maintenance.js +175 -0
  51. package/src/migrate.js +248 -181
  52. package/src/model.js +68 -0
  53. package/src/plan.js +1119 -138
  54. package/src/pragmas.js +314 -0
  55. package/src/profile.js +151 -3
  56. package/src/query.js +1634 -323
  57. package/src/residual.js +17 -0
  58. package/src/series.js +12 -4
  59. package/src/store.js +1505 -264
  60. package/src/tracker.js +203 -29
  61. package/src/udf.js +88 -7
  62. package/types/index.d.ts +1097 -25
  63. package/types/node-pool.d.ts +28 -0
  64. package/types/node-worker.d.ts +54 -0
  65. package/types/node.d.ts +69 -2
  66. package/types/postgres.d.ts +46 -0
  67. package/types/typed.d.ts +25 -3
  68. package/types/wasm.d.ts +14 -0
@@ -166,8 +166,12 @@ store untouched.
166
166
 
167
167
  The shadow runs over an empty data set; the real-data facts (the
168
168
  widening check, key consistency, the assertions over real rows) run on
169
- the real store inside its transaction. The shadow registers the same
170
- functions as the real run: `migrate(…, { registerFunctions })` runs on
169
+ the real store inside its transaction. Batched transforms and validation
170
+ include every row identity, including negative integer primary keys;
171
+ target-schema validation visits every declared entity, even after an
172
+ empty entity table.
173
+ The shadow registers the same functions as the real run:
174
+ `migrate(…, { registerFunctions })` runs on
171
175
  the shadow, the real and the reference connections before any DDL
172
176
  (§10), so a hand-created index over a registered deterministic
173
177
  function neither fails the shadow nor is silently dropped by it.
@@ -204,26 +208,115 @@ hash of the `baseline` model when no migration has run.
204
208
  an empty history, so a dry run may be pointed at a production
205
209
  database and leave its file byte-identical. The API default is to
206
210
  run; a CLI SHOULD default to the dry run.
207
- - `migrationStatus` (and the CLI's `status`/`check`) create the empty
208
- history table on a database that has none the one write a reading
209
- command makes, so a fresh file answers `applied: (none)` rather than
210
- a missing-table error. This is the one place the two differ: a dry
211
- run reports the same state and writes nothing at all.
211
+ - `migrationStatus` (and the CLI's `status`/`check`) write nothing
212
+ either: the history table is probed, never created, and an absent one
213
+ reads as an empty history, so a fresh file answers `applied: (none)`
214
+ and stays byte for byte what it was. The history table is created by
215
+ the real run alone, before its first migration is recorded.
212
216
  - Each pending migration runs in ONE exclusive transaction
213
217
  (`BEGIN IMMEDIATE` on SQLite — concurrent writers wait or time out
214
218
  under the busy timeout) with a savepoint per step; any failure rolls
215
219
  back the whole migration including its earlier steps. Where a driver
216
220
  cannot open exclusively, the transaction still isolates; the busy
217
221
  policy of MODEL-FORMAT §4 governs contention.
222
+ - A run is cancellable: `migrate(target, migrations, { signal,
223
+ deadline })` checks both BETWEEN migrations, between steps and
224
+ between the batches of a data step — never inside a statement, which
225
+ runs to its end — with the deadline read against `options.runtime`'s
226
+ clock. An abort is `JD2080` and a passed deadline `JD2075`; the
227
+ migration in flight rolls back whole (its savepoints, its history
228
+ row), the migrations already committed stand, and a rerun resumes
229
+ from the recorded position. The shadow replay is cancellable at the
230
+ same boundaries. `migrationStatus` refuses a call already cancelled
231
+ or past its deadline before it opens anything.
232
+ - **An assertion is classified before it runs, and the classification
233
+ decides what it costs.** One classifier answers for every host — a
234
+ Store, an array, a file — so they cannot disagree about the price:
235
+
236
+ | strategy | which assertions | what it costs |
237
+ |---|---|---|
238
+ | per-document | a FLWOR over `$[*]` whose `$where`/`$return` read only the binding | one keyset batch at a time; fails fast at the first batch that violates |
239
+ | fold | exactly one of `$count`, `$sum`, `$min`, `$max` over the root | one batch at a time; each batch is answered by the ENGINE and the partial answers combine |
240
+ | materialize | everything else (`$let`, `$distinct`, a nested `$for`, two aggregates) | every document at once, under `assertionBounds` |
241
+
242
+ A fold is sound because the operator is associative: the answer over a
243
+ collection is the combination of the answers over any partition of it.
244
+ Nothing reimplements an operator — each batch is evaluated by the same
245
+ compiled query the whole-collection path would use, and only the
246
+ COMBINE step is written here, so null handling, empty-sequence answers
247
+ and type coercions are the engine's. A suite runs every fold shape both
248
+ ways, over ten corpora and six partitions, and requires the value and
249
+ the verdict to be indistinguishable; a shape that cannot pass it is not
250
+ in the set.
251
+
252
+ **A materializing assertion is bounded.** `options.assertionBounds`
253
+ defaults to `{ maxRows: 100000, maxBytes: 67108864 }` and is crossed
254
+ BEFORE the excess is held — the walk stops at the row that would break
255
+ it, refusing `JD2007` (rows) or `JD2076` (bytes) and naming the two
256
+ assertion shapes that are answered in batches instead. `null` on either
257
+ member removes that bound, which a caller must ask for: an unbounded
258
+ read nobody declared is exactly what this classification removes. This
259
+ is a deliberate behavior change — a migration that used to read a very
260
+ large collection whole now refuses until its bound is raised or its
261
+ assertion is rewritten.
218
262
  - JSLT steps walk the collection in bounded batches
219
263
  (`options.batchSize`, default 500) ordered by row identity, report
220
- progress through `options.onProgress`, and never hold the whole
221
- collection in memory. Assertion steps read the whole collection into
222
- one array a documented cost; keep assertions early, before the
223
- data grows.
264
+ progress through `options.onProgress` (`{ migration, collection,
265
+ transformed | derived | asserted }`, one event per batch), and never
266
+ hold the whole collection in memory. A PER-DOCUMENT assertion a
267
+ FLWOR over `$[*]` whose `$where` and `$return` read only the binding
268
+ — walks the same batches and fails fast at the first batch that
269
+ violates, because its answer over each batch is its answer over the
270
+ whole. A cross-document assertion (one that reads the root: `$count:
271
+ '$[*]'`, a `$let`, a `$distinct`, a nested `$for`) reads the whole
272
+ collection into one array — a stated cost; keep such assertions
273
+ early, before the data grows. A cross-document assertion that is one
274
+ associative aggregate no longer costs that read at all — see the
275
+ classification table above.
224
276
  - A transform MUST NOT change a caller-keyed document's key member —
225
277
  the key column would go stale; the run refuses (`JD0023`).
226
278
 
279
+ ### 6.1 Running without a database
280
+
281
+ A migration's `jslt` and `query` steps act on DOCUMENTS, so they do not
282
+ need tables. Two surfaces run them against documents a caller already
283
+ holds, sharing one implementation of what a step means with the Store —
284
+ the same transform rule, the same key rule, the same classification of
285
+ an assertion, the same refusals in the same words.
286
+
287
+ - `migrateDocuments({ collection: [...] }, migrations, options)` answers
288
+ `{ documents, report }`. The source is REWINDABLE, so every step runs
289
+ over the whole collection before the next begins, exactly as a Store
290
+ runs it. This is what makes its answer — and its refusal, on the same
291
+ step — identical to the Store's for the same documents.
292
+ - `streamDocuments({ collection: iterable }, migrations, { write })`
293
+ walks a source that can be read only once, writing each document out
294
+ as it finishes. The input is consumed exactly once and nothing beyond
295
+ one batch is held, so a collection larger than memory still migrates.
296
+
297
+ Both refuse, BEFORE asking for the first document, any step this host
298
+ cannot honour (`JD0023`):
299
+
300
+ | Step kind | Without a database |
301
+ |---|---|
302
+ | `jslt`, `query` | runs |
303
+ | `ddl`, `sql`, `rebuild`, `derive` | refused by name — no tables to change |
304
+
305
+ A step naming a collection the caller did not supply is refused the same
306
+ way. Nothing is half-applied: a runner without a transaction cannot take
307
+ a partial write back, so the whole refusal happens before the first read.
308
+
309
+ Two limits are the single pass's, and are stated rather than hidden:
310
+
311
+ - A CROSS-DOCUMENT assertion needs every document at once, which one
312
+ pass does not hold. `streamDocuments` refuses it by name; run that
313
+ collection through `migrateDocuments`, whose source it can re-read.
314
+ - When two different steps would each refuse, `migrateDocuments` and the
315
+ Store name the EARLIER step, because each step finishes before the
316
+ next begins. `streamDocuments` carries a batch through every step, so
317
+ it can name the later one. Both refuse, with the same code and the
318
+ same words; only which step is named can differ.
319
+
227
320
  ## 7. Non-goals
228
321
 
229
322
  - **Down migrations are not shipped in 0.1.** A JSLT transform is not
@@ -247,6 +340,7 @@ hash of the `baseline` model when no migration has run.
247
340
  | `JD0021` | the migration is missing a required data transform |
248
341
  | `JD0022` | an applied migration disagrees with the history record |
249
342
  | `JD0023` | a migration step failed |
343
+ | `JD0024` | a document source or target could not be read or written |
250
344
 
251
345
  These live in the same runtime `DB_CODES` table as the storage codes
252
346
  (MODEL-FORMAT §7); the union of both documents is proven in sync with
@@ -351,13 +445,17 @@ jaren-db status --model <model> --store <db> --baseline <model> [--migrations
351
445
  jaren-db apply --store <db> --baseline <model> --migrations <dir> [--model <m>] [--dry-run] [--yes]
352
446
  jaren-db check --model <model> --store <db> --baseline <model> [--migrations <dir>] [--snapshot <file>]
353
447
  jaren-db shape --model <model>
448
+ jaren-db documents --migrations <dir> --in <file|-> (--out <file|-> | --in-place --yes | --check)
449
+ [--format json|jsonl] [--out-format json|jsonl] [--collection <name>]
450
+ [--batch-size <n>]
354
451
  ```
355
452
 
356
453
  - **A model or a migration is a `.json` file or a MODULE.** `--model`,
357
454
  `--from`, `--to` and `--baseline` accept a `.json` file or a module
358
455
  (`.js`, `.mjs`, `.cjs` — and `.ts` where the host strips types: Node
359
456
  ≥ 24 does by default, and `--no-strip-types` is refused by name)
360
- loaded with `import()` and read as its `default` export or its `model`
457
+ loaded through `@jarenjs/json/node` the suite's one document loader,
458
+ shared with `jaren-contract` — and read as its `default` export or its `model`
361
459
  export — the model pen's document, or any object whose `toJSON()`
362
460
  emits one; `--migrations <dir>` reads `.json` files and modules
363
461
  (`default` or `migration` — the migration pen's builder), sorted by
@@ -400,13 +498,40 @@ jaren-db shape --model <model>
400
498
  `apply` without `--yes` exits 1 after the printout with nothing
401
499
  applied — a CI job passes `--yes` deliberately, never by default.
402
500
  `apply --dry-run` is the CLI's printout, not §6's `dryRun: true`: it
403
- reads the history the way `status` does — creating the empty table on
404
- a database that has none and does NOT replay the chain on the
405
- shadow, so a draft step still prints instead of refusing. The
406
- shadow's verdict comes with the real `apply`.
501
+ reads the history the way `status` does — probed, never created and
502
+ does NOT replay the chain on the shadow, so a draft step still prints
503
+ instead of refusing. The shadow's verdict comes with the real `apply`.
407
504
  - `status` lists applied/pending and reports drift (§12); on a
408
- database without a history table it creates the empty one (§6).
505
+ database without a history table it creates nothing (§6).
409
506
  - `shape` prints the physical mapping a model produces.
507
+ - `documents` runs a migration's DOCUMENT steps over a file instead of a
508
+ database — §6.1's runners, given a path or stdio. `--in`/`--out` take
509
+ a file or `-`; the encoding follows the extension (`.jsonl`/`.ndjson`
510
+ line-delimited, everything else one JSON array) unless `--format` /
511
+ `--out-format` says otherwise, and stdio defaults to JSONL. Input and
512
+ output encodings are independent, so this is also the converter.
513
+ - **A file holds ONE collection.** The migrations name it; a chain
514
+ whose document steps touch more than one cannot be applied to a
515
+ file, and is refused rather than partly run. `--collection` asserts
516
+ which collection the file holds and refuses a mismatch.
517
+ - **`--out` writes elsewhere; `--in-place` replaces the input and
518
+ needs `--yes`.** Either way the documents land in a sibling
519
+ temporary that is renamed over the target only once every document
520
+ has survived every step. A failure — a step, a malformed source, a
521
+ cancelled run — removes the temporary and leaves the target byte for
522
+ byte as it was.
523
+ - **`--check` transforms and validates everything and writes nothing**,
524
+ which is the CI shape: it answers whether this chain still applies
525
+ to this data.
526
+ - **Three exit codes, three meanings:** `0` the chain applies and every
527
+ assertion holds; `1` the run failed (a step refused, the source was
528
+ malformed, the file was missing, a step needs a database); `2` the
529
+ command line itself was wrong (a missing or contradictory flag, an
530
+ unknown format). A script can tell "you asked for the wrong thing"
531
+ from "what you asked for does not hold".
532
+ - The report names the strategy §6.1 chose — `streamed`, or
533
+ `materialized` when a cross-document assertion needs the collection
534
+ at once.
410
535
 
411
536
  ## 12. Drift
412
537