@jarenjs/db 0.49.2 → 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 (70) hide show
  1. package/ARCHITECTURE.md +27 -15
  2. package/README.md +141 -41
  3. package/docs/JOBS-FORMAT.md +24 -8
  4. package/docs/LIVE-FORMAT.md +38 -9
  5. package/docs/MIGRATION-FORMAT.md +118 -36
  6. package/docs/MODEL-FORMAT.md +232 -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/capture.js +66 -28
  11. package/src/cli.js +225 -44
  12. package/src/ddl.js +23 -3
  13. package/src/dialects/sqlite.js +2 -1
  14. package/src/driver.js +63 -16
  15. package/src/drivers/wasm.js +1 -0
  16. package/src/emit-model.js +14 -0
  17. package/src/emit.js +10 -3
  18. package/src/entity.js +92 -47
  19. package/src/errors.js +25 -0
  20. package/src/index.js +2 -2
  21. package/src/jobs.js +40 -5
  22. package/src/live-time.js +12 -3
  23. package/src/live.js +11 -1
  24. package/src/migrate.js +397 -191
  25. package/src/model.js +173 -8
  26. package/src/plan.js +135 -38
  27. package/src/query.js +138 -13
  28. package/src/store.js +221 -66
  29. package/src/tracker.js +173 -48
  30. package/types/index.d.ts +152 -10
  31. package/types/node.d.ts +3 -1
  32. package/types/typed.d.ts +58 -2
  33. package/types/wasm.d.ts +7 -0
  34. package/dist/types/algebra.d.ts +0 -230
  35. package/dist/types/app.d.ts +0 -49
  36. package/dist/types/capture.d.ts +0 -85
  37. package/dist/types/cli.d.ts +0 -2
  38. package/dist/types/dag-job.d.ts +0 -40
  39. package/dist/types/ddl.d.ts +0 -229
  40. package/dist/types/derive.d.ts +0 -250
  41. package/dist/types/dialect.d.ts +0 -154
  42. package/dist/types/dialects/sqlite.d.ts +0 -9
  43. package/dist/types/driver.d.ts +0 -110
  44. package/dist/types/drivers/bun.d.ts +0 -47
  45. package/dist/types/drivers/node.d.ts +0 -37
  46. package/dist/types/drivers/wasm.d.ts +0 -65
  47. package/dist/types/emit-model.d.ts +0 -44
  48. package/dist/types/emit.d.ts +0 -75
  49. package/dist/types/entity.d.ts +0 -23
  50. package/dist/types/errors.d.ts +0 -170
  51. package/dist/types/graph.d.ts +0 -28
  52. package/dist/types/index.d.ts +0 -37
  53. package/dist/types/jobs.d.ts +0 -140
  54. package/dist/types/knn.d.ts +0 -69
  55. package/dist/types/live-time.d.ts +0 -141
  56. package/dist/types/live.d.ts +0 -64
  57. package/dist/types/migrate.d.ts +0 -170
  58. package/dist/types/model.d.ts +0 -36
  59. package/dist/types/patch-sql.d.ts +0 -37
  60. package/dist/types/plan.d.ts +0 -142
  61. package/dist/types/profile.d.ts +0 -80
  62. package/dist/types/query.d.ts +0 -112
  63. package/dist/types/residual.d.ts +0 -64
  64. package/dist/types/series.d.ts +0 -227
  65. package/dist/types/store.d.ts +0 -60
  66. package/dist/types/tracker.d.ts +0 -43
  67. package/dist/types/typed.d.ts +0 -15
  68. package/dist/types/types.d.ts +0 -26
  69. package/dist/types/udf.d.ts +0 -75
  70. package/dist/types/window.d.ts +0 -52
@@ -45,18 +45,29 @@ shape change is a **transformation of values**, not a table rebuild.
45
45
  - `kind: "jslt"` rewrites every document of a collection through a
46
46
  compiled JSLT stylesheet, in batches, inside the migration's
47
47
  transaction. The empty stylesheet (`[]`) is the identity transform.
48
- A step carrying `"draft": true` is a planner placeholder and MUST
49
- refuse to run (`JD0021`) until the author fills it in.
48
+ Over an ENTITY table the stylesheet sees the whole row — the mapped
49
+ columns merged into the document under the TARGET model's mapping
50
+ and what it returns is split back into columns and document by that
51
+ mapping; the key member is kept from the row (a stylesheet that
52
+ omits it loses nothing) and a stylesheet that changes it is
53
+ `JD0023`. A step carrying `"draft": true` is a planner placeholder
54
+ and MUST refuse to run (`JD0021`) until the author fills it in.
50
55
  - `kind: "query"` is an assertion: the query runs over the
51
56
  collection's documents and MUST answer an empty sequence (`expect:
52
57
  "empty"`, the default) or an EBV-true value (`expect: "ebv"`) for
53
58
  the migration to proceed. This is how a migration states its own
54
59
  precondition — "no user has a null email before the NOT NULL
55
- index" — and it is checked on the shadow first.
60
+ index" — and it is checked on the shadow first. Over an entity table
61
+ the assertion reads the same merged rows a `jslt` step sees.
56
62
  - `kind: "derive"` recomputes named STORED derived index columns from
57
63
  the documents already in a collection — the backfill described in
58
64
  §2.1. It is idempotent: a derived value is a pure function of the
59
65
  document, so a replay writes what the first run wrote.
66
+ - `kind: "sql"` executes one rendered DATA statement — a fold of a
67
+ column into the document, a backfill, an `INSERT … SELECT` — §9.4; a
68
+ dry run always prints it with its note.
69
+ - `kind: "rebuild"` is the entity restructure of §10, self-contained:
70
+ the `CREATE` of the new shape, the copy and the index DDL.
60
71
  - Steps are ordered, and the order is the contract.
61
72
 
62
73
  ### 2.1 Derived spatial columns and the backfill
@@ -97,7 +108,11 @@ and the column entry carries the width the value is packed to:
97
108
  ## 3. Planning and the widening/narrowing rule
98
109
 
99
110
  `planMigration(fromModel, toModel, { dialect, id, derived })` produces
100
- `{ migration, report }` by diffing the two models' PHYSICAL plans:
111
+ `{ migration, report }` by diffing the two models' PHYSICAL plans. The
112
+ from-model is the previous model — the previous model FILE, or, under
113
+ the CLI's snapshot discipline (§11), the committed `model.snapshot.json`
114
+ the last `plan` advanced: a database stores shape hashes, never models,
115
+ so the previous shape lives beside the code, where a diff can read it.
101
116
 
102
117
  - An added collection becomes its full CREATE DDL; a removed
103
118
  collection becomes a `DROP TABLE` step whose note says
@@ -107,7 +122,16 @@ and the column entry carries the width the value is packed to:
107
122
  `"x-rename": "oldName"` on the target collection; the planner emits
108
123
  the rename first and rebuilds the indexes (a renamed SQLite table
109
124
  keeps its old index names — probed). Without the hint, a rename is
110
- a drop plus a create and the report says so.
125
+ a drop plus a create and the report says so. The hint is not part of
126
+ the shape: `x-rename` is stripped before the shape hash is computed,
127
+ so a model that keeps carrying a satisfied hint hashes the same as
128
+ one without it and plans nothing — a rename is idempotent across
129
+ `plan` runs. An entity rename carries its join tables with it, by
130
+ the endpoints the mapping records rather than by splitting the
131
+ table's name (an entity name may itself contain `_`); where the
132
+ rename flips the sorted endpoint order the join table is rebuilt
133
+ (create, `INSERT … SELECT`, drop) under its new name and no
134
+ membership is lost.
111
135
  - Added, removed and changed indexes become index DDL — reusing the
112
136
  store's own DDL generator, never a second implementation. A changed
113
137
  generated column (type or path) is a drop plus an add, with its
@@ -123,7 +147,10 @@ and the column entry carries the width the value is packed to:
123
147
  schema through the injected `compileSchema` hook. A document that no
124
148
  longer validates is `JD0021` and the whole migration rolls back — a
125
149
  narrowing without an adequate transform cannot land. A widening
126
- needs no transform, and passes this check by fact.
150
+ needs no transform, and passes this check by fact. For an entity
151
+ the validated document is the whole row — columns merged back under
152
+ the target mapping — so a pure widening of a column-mapped member
153
+ passes and a narrowing of one is caught.
127
154
  - Changing a collection's key declaration is not planned (a rebuild);
128
155
  the planner refuses with a `TypeError` naming the non-goal.
129
156
 
@@ -139,10 +166,11 @@ store untouched.
139
166
 
140
167
  The shadow runs over an empty data set; the real-data facts (the
141
168
  widening check, key consistency, the assertions over real rows) run on
142
- the real store inside its transaction. The model format declares no
143
- UDF-expression indexes, so there is no function set to re-register on
144
- the shadow stated here because a dialect that allowed such indexes
145
- would make the shadow fail on a schema the real store accepts.
169
+ the real store inside its transaction. The shadow registers the same
170
+ functions as the real run: `migrate(…, { registerFunctions })` runs on
171
+ the shadow, the real and the reference connections before any DDL
172
+ (§10), so a hand-created index over a registered deterministic
173
+ function neither fails the shadow nor is silently dropped by it.
146
174
 
147
175
  ## 5. History and checksums
148
176
 
@@ -171,8 +199,16 @@ hash of the `baseline` model when no migration has run.
171
199
  otherwise), the physical end shape is verified, and the real-data
172
200
  validation of §3 runs.
173
201
  - `dryRun: true` prints every statement and the affected document
174
- counts, validates the chain on the shadow, and writes NOTHING. The
175
- API default is to run; a CLI SHOULD default to the dry run.
202
+ counts, validates the chain on the shadow, and writes NOTHING — not
203
+ even the history table: it PROBES for one and reads an absent one as
204
+ an empty history, so a dry run may be pointed at a production
205
+ database and leave its file byte-identical. The API default is to
206
+ 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.
176
212
  - Each pending migration runs in ONE exclusive transaction
177
213
  (`BEGIN IMMEDIATE` on SQLite — concurrent writers wait or time out
178
214
  under the busy timeout) with a savepoint per step; any failure rolls
@@ -225,13 +261,13 @@ every row has a shadow-verified test that migrates seeded data:
225
261
  | Change | Strategy |
226
262
  |---|---|
227
263
  | add mapped column (property added, or moved out of the document) | `ALTER TABLE ADD COLUMN` — always nullable (absent reads back absent, MODEL-FORMAT §9.3) — plus a `sql` data step when the property's values already live in the document |
228
- | drop mapped column (property removed, or moved into the document) | fold the column back into the document first (`sql` step) when the property survives; drop its index, then `DROP COLUMN` where SQLite's conditions hold, else rebuild |
264
+ | drop mapped column (property removed, or moved into the document) | fold the column back into the document first (`sql` step) when the property survives — a `NULL` column folds to ABSENT, never to JSON `null`, so §9.3's rule survives the fold, in the rebuild copy too; drop its index, then `DROP COLUMN` where SQLite's conditions hold, else rebuild |
229
265
  | change type / enum CHECK / key / epoch flavor | **rebuild** (§10) |
230
266
  | add or drop an index (`unique`/`index`/version) | plain DDL |
231
- | add or drop a relation (foreign-key column, join table) | foreign keys **rebuild** the holder; join tables create/drop directly |
232
- | entity added / dropped | create / `DROP TABLE` (destructive, named) |
267
+ | add or drop a relation (foreign-key column, join table) | foreign keys **rebuild** the holder — an inferred foreign-key column the target model no longer declares is folded into the document when the target declares the property, else named in `report.lost` and the plan is destructive; join tables create/drop directly |
268
+ | entity added / dropped | create / `DROP TABLE` (destructive, named), children before parents so no foreign key dangles mid-migration |
233
269
  | entity renamed | declared with `x-rename` on the target entity — never inferred; join tables renamed mechanically with their endpoints |
234
- | scalar ⇄ JSONB move (`column: "json"` toggled, shape change) | rebuild + a data step |
270
+ | scalar ⇄ JSONB move (`column: "json"` toggled) | the first two rows: `ADD COLUMN` plus a `sql` lift out of the document, or a `sql` fold plus `DROP COLUMN` — a rebuild only where SQLite cannot drop the column in place |
235
271
 
236
272
  Two rules keep the diff honest:
237
273
 
@@ -279,12 +315,13 @@ the transaction** — a broken reference fails the migration rather
279
315
  than shipping.
280
316
 
281
317
  Two deviations from the cited twelve steps, recorded: (1) the
282
- procedure brackets itself with `PRAGMA foreign_keys=OFF/ON`, which is
283
- a no-op inside a transaction the migration connection never enables
284
- the pragma (SQLite's default is off; `openStore` enables AND verifies
285
- it per connection), so enforcement during the rebuild is off exactly
286
- as the procedure wants, and `foreign_key_check` provides the
287
- guarantee; (2) triggers and views are not re-created because this
318
+ procedure's `PRAGMA foreign_keys=OFF/ON` bracket is honoured
319
+ literally, OUTSIDE the transaction (inside one the pragma is a no-op):
320
+ `node:sqlite` enables enforcement by default, and with it on a parent
321
+ table could not even be dropped, so a migration holding a rebuild
322
+ step turns enforcement off before `BEGIN IMMEDIATE` and back on after
323
+ it settles, and `foreign_key_check` inside the transaction provides
324
+ the guarantee the bracket suspended; (2) triggers and views are not re-created because this
288
325
  store creates none — a hand-added trigger is outside the model and
289
326
  outside the diff, which drift (§12) will name.
290
327
 
@@ -307,23 +344,68 @@ drop the index) on a schema the store accepts.
307
344
  `jaren-db` drives the workflow (mirroring `jaren-emit`):
308
345
 
309
346
  ```
310
- jaren-db plan --from <model> --to <model> [--store <db>] [--id x] [--out file]
311
- jaren-db status --model <model> --store <db> --baseline <model> [--migrations <dir>]
312
- jaren-db apply --store <db> --baseline <model> --migrations <dir> [--model <m>] [--dry-run] [--yes]
313
- jaren-db check --model <model> --store <db> --baseline <model> [--migrations <dir>]
314
- jaren-db shape --model <model>
347
+ jaren-db plan --from <model> --to <model> [--store <db>] [--id x] [--out file]
348
+ jaren-db plan --model <model> [--snapshot <file>] [--store <db>] [--id x] --out file
349
+ jaren-db snapshot --model <model> [--snapshot <file>] [--types <file>]
350
+ jaren-db status --model <model> --store <db> --baseline <model> [--migrations <dir>] [--snapshot <file>]
351
+ jaren-db apply --store <db> --baseline <model> --migrations <dir> [--model <m>] [--dry-run] [--yes]
352
+ jaren-db check --model <model> --store <db> --baseline <model> [--migrations <dir>] [--snapshot <file>]
353
+ jaren-db shape --model <model>
315
354
  ```
316
355
 
356
+ - **A model or a migration is a `.json` file or a MODULE.** `--model`,
357
+ `--from`, `--to` and `--baseline` accept a `.json` file or a module
358
+ (`.js`, `.mjs`, `.cjs` — and `.ts` where the host strips types: Node
359
+ ≥ 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`
361
+ export — the model pen's document, or any object whose `toJSON()`
362
+ emits one; `--migrations <dir>` reads `.json` files and modules
363
+ (`default` or `migration` — the migration pen's builder), sorted by
364
+ file name. A module whose export is not a document, or whose emission
365
+ is not JSON, fails with the module named. **Modules are pure:** the
366
+ CLI loads every module TWICE (two `import()`s under distinct
367
+ cache-busting queries) and refuses one whose two emissions differ —
368
+ no clock, no env, no randomness — because a migration that hashes
369
+ differently per load can never match its own history.
317
370
  - `plan` diffs two model FILES (a database stores shape hashes, not
318
- models — the from-model is the previous model file); with `--store`
319
- it first verifies the from-model's hash matches the database's
320
- recorded shape.
321
- - `check` is the CI command: exit 1 when migrations are pending OR the
322
- database drifted; 0 in sync.
323
- - `apply` prints every statement before running; destructive steps
324
- (drop table/column, rebuild) require `--yes` or an interactive
325
- confirmation that NAMES what is lost. Default is dry-run + ask.
326
- - `status` lists applied/pending and reports drift (§12).
371
+ models — the from-model is the previous model file), or, with
372
+ `--model`, the committed SNAPSHOT against the model: `--snapshot`
373
+ names it and defaults to `model.snapshot.json` beside the model; a
374
+ model whose shape equals the snapshot's plans nothing and exits 0;
375
+ otherwise the migration is written (`--out`) and the snapshot is
376
+ advanced to the model — without `--out` the plan is printed and the
377
+ snapshot stays, and the CLI says so. With `--store` it first compares
378
+ the from-model's physical shape with the database itself never
379
+ with the history, which would refuse every database that has applied
380
+ a migration.
381
+ - `snapshot` writes the model's snapshot (`--snapshot`, the same
382
+ default) — from the model the store was created with, before the
383
+ first `plan --model`; with `--types <file>` it also writes emit's
384
+ TypeScript declaration for the model (`entityEmitModel` rendered by
385
+ `@jarenjs/emit`, loaded lazily — `@jarenjs/db` does not depend on emit,
386
+ and a host without it is told exactly what `--types` needs), so a
387
+ transform over a JSON snapshot can be typed by annotation. Two runs on
388
+ one input write nothing the second time.
389
+ - `check` is the CI command: exit 1 on an UNPLANNED MODEL CHANGE (a
390
+ snapshot in use whose shape is not the model's — the model moved and
391
+ nobody planned; named as such, never as the database's drift), when
392
+ migrations are pending, OR when the database drifted; 0 in sync.
393
+ `--model` is required — without it drift cannot be measured, and
394
+ `check` refuses rather than print `in sync`. `status` reports the same
395
+ verdict on its `model:` line.
396
+ - `apply` prints every statement, then asks; destructive steps (drop
397
+ table/column, rebuild) print what is lost and ask for that
398
+ separately. `--yes` answers both, `--dry-run` stops after the
399
+ printout. Without an interactive terminal there is nobody to ask, so
400
+ `apply` without `--yes` exits 1 after the printout with nothing
401
+ applied — a CI job passes `--yes` deliberately, never by default.
402
+ `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`.
407
+ - `status` lists applied/pending and reports drift (§12); on a
408
+ database without a history table it creates the empty one (§6).
327
409
  - `shape` prints the physical mapping a model produces.
328
410
 
329
411
  ## 12. Drift