@jarenjs/linq 0.86.0 → 0.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/docs/DB-CLIENT.md +2 -2
- package/docs/JSLT-PEN.md +1 -1
- package/docs/LINQ-FORMAT.md +12 -11
- package/docs/MIGRATION-PEN.md +55 -28
- package/docs/QUERY-PEN.md +1 -1
- package/package.json +6 -6
- package/src/migration/define.js +29 -19
- package/src/migration/steps.js +26 -7
- package/types/migration.d.ts +72 -2
package/README.md
CHANGED
|
@@ -387,6 +387,14 @@ model that moved without a plan (`jaren-db check`). The mapping table
|
|
|
387
387
|
and the worked examples are
|
|
388
388
|
[docs/MIGRATION-PEN.md](docs/MIGRATION-PEN.md).
|
|
389
389
|
|
|
390
|
+
For existing physical tables, `.step({ kind: 'table', plan })` preserves
|
|
391
|
+
the complete guarded `planTableMigration` artifact. `fromPlanned` keeps the
|
|
392
|
+
physical source/dispositions/assertions/target header. An optional `{ model }`
|
|
393
|
+
on `transform` or `assert` selects the layout at that point in the chain,
|
|
394
|
+
including an old name absent from the final model. Execution, complete target
|
|
395
|
+
acceptance, fixture replay and receipts remain in db; the pen imports no engine.
|
|
396
|
+
See the [runnable physical lifecycle](../db/docs/MIGRATION-FORMAT.md#runnable-physical-lifecycle).
|
|
397
|
+
|
|
390
398
|
## By code: the contract pen
|
|
391
399
|
|
|
392
400
|
`@jarenjs/linq/contract` writes `$contract` 0.1 documents — the
|
package/docs/DB-CLIENT.md
CHANGED
|
@@ -843,10 +843,10 @@ never builds one; the migration between two of them is
|
|
|
843
843
|
|
|
844
844
|
## 7. Cost
|
|
845
845
|
|
|
846
|
-
`@jarenjs/linq/db` builds to **<!--fact:bundle.db-->
|
|
846
|
+
`@jarenjs/linq/db` builds to **<!--fact:bundle.db-->728,423<!--/fact--> bytes** as a minified,
|
|
847
847
|
tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
|
|
848
848
|
measures and `npm run test:tree-shaking` reports, published rounded
|
|
849
|
-
(<!--fact:bundle.db.kb-->
|
|
849
|
+
(<!--fact:bundle.db.kb-->728<!--/fact--> kB) beside the other nine subpath prices in
|
|
850
850
|
[docs/CONSUMING.md](../../../docs/CONSUMING.md).
|
|
851
851
|
|
|
852
852
|
It is by far the largest of the ten, and the reason is §1.1's edge rather
|
package/docs/JSLT-PEN.md
CHANGED
|
@@ -948,7 +948,7 @@ source. That makes it the SMALLEST of the nine pen bundles, and the
|
|
|
948
948
|
reason is that a stylesheet is mostly bodies, and a body is the shared
|
|
949
949
|
machine every pen already pays for.
|
|
950
950
|
|
|
951
|
-
Two figures worth reading beside it: `./migration` (<!--fact:bundle.migration-->
|
|
951
|
+
Two figures worth reading beside it: `./migration` (<!--fact:bundle.migration-->23,806<!--/fact--> bytes)
|
|
952
952
|
carries this pen's `body()` and pays for it, which is why the two prices
|
|
953
953
|
sit so close; and `./flow` (<!--fact:bundle.flow-->18,478<!--/fact--> bytes) is within 60 bytes of this one
|
|
954
954
|
despite writing two formats, because it shares the same capture and adds
|
package/docs/LINQ-FORMAT.md
CHANGED
|
@@ -36,12 +36,12 @@ is the index of those guides, and it is how a reader reaches any of them.
|
|
|
36
36
|
<!--fact:pens.index-->
|
|
37
37
|
| Document | Lines | What it writes, and when to open it |
|
|
38
38
|
|---|---:|---|
|
|
39
|
-
| [LINQ-FORMAT.md](LINQ-FORMAT.md) |
|
|
39
|
+
| [LINQ-FORMAT.md](LINQ-FORMAT.md) | 945 | this file, the binder and the family's **normative reference**: what a pen is, the rules all of them keep, the shared `JL01xx` table, and the cross-pen views derived from the guides it indexes. **Read it when** you want a rule that is true of every pen, an index of the documents, or one place to look up a method without knowing which pen owns it |
|
|
40
40
|
| [QUERY-PEN.md](QUERY-PEN.md) | 1,761 | the chain, `.` — query documents (`jaren-query`) and the provider seam. **Read it when** you are querying data, or implementing a provider that answers a query document |
|
|
41
41
|
| [SCHEMA-PEN.md](SCHEMA-PEN.md) | 1,205 | `./schema` — JSON Schema 2020-12: the structural keywords, the constraints and the annotations, each with a method of its own, plus `$query`, `$defs`/`$ref` recursion and the normalizer's per-field predicates. **Read it when** you are describing the shape of data — for validation, for a form, or as the base of an entity |
|
|
42
42
|
| [MODEL-PEN.md](MODEL-PEN.md) | 1,116 | `./model` — the `x-entity` vocabulary on JSON Schema, and the `$model` 0.1 document `openStore` accepts unchanged. **Read it when** you are declaring a store's entities, their keys and their relations |
|
|
43
43
|
| [JSLT-PEN.md](JSLT-PEN.md) | 955 | `./jslt` — `$jslt` 0.1 stylesheets: the envelope and its rules, whose bodies are captured over the matched value. **Read it when** you are transforming one document into another |
|
|
44
|
-
| [MIGRATION-PEN.md](MIGRATION-PEN.md) |
|
|
44
|
+
| [MIGRATION-PEN.md](MIGRATION-PEN.md) | 808 | `./migration` — `$migration` 0.1 documents: the two shape hashes and the ordered steps the runner takes. **Read it when** you are moving a store from one model to the next |
|
|
45
45
|
| [CONTRACT-PEN.md](CONTRACT-PEN.md) | 1,221 | `./contract` — `$contract` 0.1 documents: the operations, their schemas, their declared behavior and their REST binding. **Read it when** you are declaring an API and want its client, its server and its tools typed from one document |
|
|
46
46
|
| [FLOW-PEN.md](FLOW-PEN.md) | 1,033 | `./flow` — `jaren-fsm` 0.1 machines and `jaren-dag` 0.1 dataflows, every query-valued member captured. **Read it when** you are declaring a state machine or a dependency graph of tasks |
|
|
47
47
|
| [APP-PEN.md](APP-PEN.md) | 1,143 | `./app` — the `jaren-app` 0.1 document `createApp` runs, and the JSON Schema of its state beside it. **Read it when** you are declaring a whole application: state, view, actions, effects |
|
|
@@ -203,12 +203,12 @@ and the bundle is the byte count the tree-shaking probe builds.
|
|
|
203
203
|
<!--fact:pens.census-->
|
|
204
204
|
| Document | Subpath | Lines | Mapping rows | Worked examples | Refusals | Bundle |
|
|
205
205
|
|---|---|---:|---:|---:|---:|---:|
|
|
206
|
-
| [LINQ-FORMAT.md](LINQ-FORMAT.md) | — |
|
|
206
|
+
| [LINQ-FORMAT.md](LINQ-FORMAT.md) | — | 945 | — | — | — | — |
|
|
207
207
|
| [QUERY-PEN.md](QUERY-PEN.md) | `.` | 1,761 | 34 | 8 | 15 | 175,560 B |
|
|
208
208
|
| [SCHEMA-PEN.md](SCHEMA-PEN.md) | `./schema` | 1,205 | 82 | 10 | 4 | 35,285 B |
|
|
209
209
|
| [MODEL-PEN.md](MODEL-PEN.md) | `./model` | 1,116 | 30 | 6 | 3 | 44,143 B |
|
|
210
210
|
| [JSLT-PEN.md](JSLT-PEN.md) | `./jslt` | 955 | 17 | 8 | 3 | 18,424 B |
|
|
211
|
-
| [MIGRATION-PEN.md](MIGRATION-PEN.md) | `./migration` |
|
|
211
|
+
| [MIGRATION-PEN.md](MIGRATION-PEN.md) | `./migration` | 808 | 12 | 5 | 4 | 23,806 B |
|
|
212
212
|
| [CONTRACT-PEN.md](CONTRACT-PEN.md) | `./contract` | 1,221 | 38 | 6 | 3 | 47,427 B |
|
|
213
213
|
| [FLOW-PEN.md](FLOW-PEN.md) | `./flow` | 1,033 | 16 | 7 | 3 | 18,478 B |
|
|
214
214
|
| [APP-PEN.md](APP-PEN.md) | `./app` | 1,143 | 22 | 7 | 3 | 49,574 B |
|
|
@@ -217,9 +217,9 @@ and the bundle is the byte count the tree-shaking probe builds.
|
|
|
217
217
|
| [JTLT-PEN.md](JTLT-PEN.md) | `./jtlt` | 83 | 13 | 1 | 2 | 15,293 B |
|
|
218
218
|
| [PROJECT-PEN.md](PROJECT-PEN.md) | `./project` | 81 | 9 | 1 | 1 | 13,793 B |
|
|
219
219
|
| [CHARTS-PEN.md](CHARTS-PEN.md) | `./charts` | 94 | 21 | 1 | 1 | 15,582 B |
|
|
220
|
-
| [DB-CLIENT.md](DB-CLIENT.md) | `./db` | 1,085 | 46 | 4 | 2 |
|
|
220
|
+
| [DB-CLIENT.md](DB-CLIENT.md) | `./db` | 1,085 | 46 | 4 | 2 | 728,423 B |
|
|
221
221
|
| [FORMULA-PEN.md](FORMULA-PEN.md) | `./formula` | 41 | 2 | — | — | 14,850 B |
|
|
222
|
-
| **16 documents** | | **12,
|
|
222
|
+
| **16 documents** | | **12,616** | **369** | **72** | | |
|
|
223
223
|
<!--/fact-->
|
|
224
224
|
|
|
225
225
|
A pen whose mapping rows are far below its worked examples is a pen
|
|
@@ -265,7 +265,7 @@ it and each document publishes it. The rounded column is what
|
|
|
265
265
|
| `@jarenjs/linq/schema` | [SCHEMA-PEN.md](SCHEMA-PEN.md) | 35,285 B | 35 kB |
|
|
266
266
|
| `@jarenjs/linq/model` | [MODEL-PEN.md](MODEL-PEN.md) | 44,143 B | 44 kB |
|
|
267
267
|
| `@jarenjs/linq/jslt` | [JSLT-PEN.md](JSLT-PEN.md) | 18,424 B | 18 kB |
|
|
268
|
-
| `@jarenjs/linq/migration` | [MIGRATION-PEN.md](MIGRATION-PEN.md) |
|
|
268
|
+
| `@jarenjs/linq/migration` | [MIGRATION-PEN.md](MIGRATION-PEN.md) | 23,806 B | 24 kB |
|
|
269
269
|
| `@jarenjs/linq/contract` | [CONTRACT-PEN.md](CONTRACT-PEN.md) | 47,427 B | 47 kB |
|
|
270
270
|
| `@jarenjs/linq/flow` | [FLOW-PEN.md](FLOW-PEN.md) | 18,478 B | 18 kB |
|
|
271
271
|
| `@jarenjs/linq/app` | [APP-PEN.md](APP-PEN.md) | 49,574 B | 50 kB |
|
|
@@ -274,7 +274,7 @@ it and each document publishes it. The rounded column is what
|
|
|
274
274
|
| `@jarenjs/linq/jtlt` | [JTLT-PEN.md](JTLT-PEN.md) | 15,293 B | 15 kB |
|
|
275
275
|
| `@jarenjs/linq/project` | [PROJECT-PEN.md](PROJECT-PEN.md) | 13,793 B | 14 kB |
|
|
276
276
|
| `@jarenjs/linq/charts` | [CHARTS-PEN.md](CHARTS-PEN.md) | 15,582 B | 16 kB |
|
|
277
|
-
| `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) |
|
|
277
|
+
| `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) | 728,423 B | 728 kB |
|
|
278
278
|
| `@jarenjs/linq/formula` | [FORMULA-PEN.md](FORMULA-PEN.md) | 14,850 B | 15 kB |
|
|
279
279
|
<!--/fact-->
|
|
280
280
|
|
|
@@ -597,10 +597,11 @@ it says.
|
|
|
597
597
|
| `.transform(name, (row, x) => …)` | `{ kind: 'jslt', collection: name, stylesheet: [{ match: '$', body }] }` — one root rule, the body captured through the JSLT pen's `body()` over the WHOLE row, `x.root`/`x.path` the externals the engine binds (JSLT-FORMAT §8.2) | `row` is `Expr<Old>` (`DocOf<From, name>`); the result must spell `New` — a dropped, mistyped or foreign member does not compile; the honest top (`get()`) is admitted where a precise value is | native; a table the target model does not declare `JL0106`; an undeclared external `JL0104` |
|
|
598
598
|
| `.transform(name, stylesheet(…))`, `.transform(name, rules)` | the rules ARRAY — a `jslt` step carries the array, so the envelope's `unmatched`/`modes` have no place in it | a typed stylesheet's or first rule's `Out` must be `New`; a hand-written rule is the honest top | native; a disposition or a mode table `JL0102`; not JSON `JL0101` |
|
|
599
599
|
| `.assert(name, (row) => …, { expect? })` | `{ kind: 'query', collection: name, assert: { $for: { it: '$[*]' }, $where: <predicate>, $return: '$it' }, expect? }` — the format's own `$for` over the rows; the predicate names the VIOLATION (`expect: 'empty'`, the default, absent from the document) or the witness (`expect: 'ebv'`) | `row` is the members the two shapes share — a precondition sees old rows, a postcondition new ones, and what both agree on is what neither lies about; annotate (`(row: Expr<User>) => …`) when one shape is meant | native; another `expect` `JL0101`; an external `JL0104`; an undeclared table `JL0106` |
|
|
600
|
-
| `.assert(name, query, { expect? })` | the query document verbatim | — | native |
|
|
600
|
+
| `.assert(name, query, { expect?, model? })` | the query document verbatim, plus an optional current model | — | native |
|
|
601
|
+
| `.transform(name, spelling, { model })`, `.assert(name, spelling, { model, expect? })` | the ordinary step plus an immutable current `$model` document | transform input/output and assertion rows use this model's layout; names may be absent from the final model | native; an invalid model `JL0101`; a name absent from the selected model `JL0106` |
|
|
601
602
|
| `.derive(name, columns)` | `{ kind: 'derive', collection: name, columns }` — a backfill of stored derived columns (§2.1), the columns verbatim | `readonly DeriveColumn[]` | native; no columns `JL0101`; an undeclared table `JL0106` |
|
|
602
|
-
| `.step(raw)` | any planner-emitted step,
|
|
603
|
-
| `fromPlanned(document, { from?, to? })` | the planner's document,
|
|
603
|
+
| `.step(raw)` | any planner-emitted step, including a complete guarded `{ kind: 'table', plan }` or a hybrid `rebuild`; a `draft` flag rides untouched | `MigrationStep`, including `TableStep`/`ReviewedTablePlan` | native; an unrecognised kind or incomplete plan/step `JL0101` |
|
|
604
|
+
| `fromPlanned(document, { from?, to? })` | the planner's document, including its `physical` header and complete saved plans; `.transform(name, …)` replaces its draft in place, and other methods append | the models type transforms and are checked against the hashes | native; a model that is not the planned one `JL0102`; two drafts for one name, or no draft and neither target nor step model `JL0106` |
|
|
604
605
|
| `.document`, `.toJSON()` | the deep-frozen `$migration` document — assembled once and memoized, so `a.document === a.document` | `MigrationDocument` | native |
|
|
605
606
|
|
|
606
607
|
### The Jaren contract pen — [CONTRACT-PEN.md §2](CONTRACT-PEN.md)
|
package/docs/MIGRATION-PEN.md
CHANGED
|
@@ -114,19 +114,16 @@ naming the reason).
|
|
|
114
114
|
| `.transform(name, (row, x) => …)` | `{ kind: 'jslt', collection: name, stylesheet: [{ match: '$', body }] }` — one root rule, the body captured through the JSLT pen's `body()` over the WHOLE row, `x.root`/`x.path` the externals the engine binds (JSLT-FORMAT §8.2) | `row` is `Expr<Old>` (`DocOf<From, name>`); the result must spell `New` — a dropped, mistyped or foreign member does not compile; the honest top (`get()`) is admitted where a precise value is | native; a table the target model does not declare `JL0106`; an undeclared external `JL0104` |
|
|
115
115
|
| `.transform(name, stylesheet(…))`, `.transform(name, rules)` | the rules ARRAY — a `jslt` step carries the array, so the envelope's `unmatched`/`modes` have no place in it | a typed stylesheet's or first rule's `Out` must be `New`; a hand-written rule is the honest top | native; a disposition or a mode table `JL0102`; not JSON `JL0101` |
|
|
116
116
|
| `.assert(name, (row) => …, { expect? })` | `{ kind: 'query', collection: name, assert: { $for: { it: '$[*]' }, $where: <predicate>, $return: '$it' }, expect? }` — the format's own `$for` over the rows; the predicate names the VIOLATION (`expect: 'empty'`, the default, absent from the document) or the witness (`expect: 'ebv'`) | `row` is the members the two shapes share — a precondition sees old rows, a postcondition new ones, and what both agree on is what neither lies about; annotate (`(row: Expr<User>) => …`) when one shape is meant | native; another `expect` `JL0101`; an external `JL0104`; an undeclared table `JL0106` |
|
|
117
|
-
| `.assert(name, query, { expect? })` | the query document verbatim | — | native |
|
|
117
|
+
| `.assert(name, query, { expect?, model? })` | the query document verbatim, plus an optional current model | — | native |
|
|
118
|
+
| `.transform(name, spelling, { model })`, `.assert(name, spelling, { model, expect? })` | the ordinary step plus an immutable current `$model` document | transform input/output and assertion rows use this model's layout; names may be absent from the final model | native; an invalid model `JL0101`; a name absent from the selected model `JL0106` |
|
|
118
119
|
| `.derive(name, columns)` | `{ kind: 'derive', collection: name, columns }` — a backfill of stored derived columns (§2.1), the columns verbatim | `readonly DeriveColumn[]` | native; no columns `JL0101`; an undeclared table `JL0106` |
|
|
119
|
-
| `.step(raw)` | any planner-emitted step,
|
|
120
|
-
| `fromPlanned(document, { from?, to? })` | the planner's document,
|
|
120
|
+
| `.step(raw)` | any planner-emitted step, including a complete guarded `{ kind: 'table', plan }` or a hybrid `rebuild`; a `draft` flag rides untouched | `MigrationStep`, including `TableStep`/`ReviewedTablePlan` | native; an unrecognised kind or incomplete plan/step `JL0101` |
|
|
121
|
+
| `fromPlanned(document, { from?, to? })` | the planner's document, including its `physical` header and complete saved plans; `.transform(name, …)` replaces its draft in place, and other methods append | the models type transforms and are checked against the hashes | native; a model that is not the planned one `JL0102`; two drafts for one name, or no draft and neither target nor step model `JL0106` |
|
|
121
122
|
| `.document`, `.toJSON()` | the deep-frozen `$migration` document — assembled once and memoized, so `a.document === a.document` | `MigrationDocument` | native |
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
(
|
|
126
|
-
method here and the sixth is `step()`. What makes this pen worth reading
|
|
127
|
-
is not the size of its surface but what each step is checked against —
|
|
128
|
-
§4 is the richest refusal section in the family relative to the pen's
|
|
129
|
-
size, and it is the reason.
|
|
124
|
+
The `$migration` 0.1 vocabulary has seven kinds: `ddl`, `jslt`, `query`,
|
|
125
|
+
`derive`, `sql`, `rebuild` and `table`. The two structural artifact kinds use
|
|
126
|
+
`step()`; the pen preserves their plans without implementing SQL execution.
|
|
130
127
|
|
|
131
128
|
Three rules the table implies, spelled out:
|
|
132
129
|
|
|
@@ -137,13 +134,11 @@ Three rules the table implies, spelled out:
|
|
|
137
134
|
instruction, not shape, so a model that keeps carrying a satisfied hint
|
|
138
135
|
hashes the same as one without it and plans nothing (MIGRATION-FORMAT
|
|
139
136
|
§3 — a rename is idempotent across `plan` runs).
|
|
140
|
-
- **A step's
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
every other name is the runner's to judge and a transform for another
|
|
146
|
-
table is spelled with `step()`.
|
|
137
|
+
- **A step's name belongs to its selected model.** `transform` and
|
|
138
|
+
`assert` use an explicit `{ model }` when supplied, or the known target
|
|
139
|
+
otherwise; `derive` uses the target. Entities and collections both count.
|
|
140
|
+
A planned transform with no draft and no known model refuses `JL0106`;
|
|
141
|
+
supply its current model or spell the raw step explicitly.
|
|
147
142
|
- **Steps are appended in the order they are called**, and `transform`
|
|
148
143
|
over a planned document is the one exception: it replaces the draft for
|
|
149
144
|
that name IN PLACE, so the planner's ordering — DDL before the data
|
|
@@ -480,7 +475,7 @@ else.
|
|
|
480
475
|
| `m().assert('User', p, { other: 1 })` | `assert() does not take 'other'` | `{ expect }` |
|
|
481
476
|
| `m().assert('User', undefined)` | `assert() takes a predicate (row) => … or a query document over the rows` | a predicate |
|
|
482
477
|
| `m().derive('User', [])` | `derive() takes a non-empty array of derived-column records ({ name, derive, segments }), got a Array instance` | the columns the planner emitted |
|
|
483
|
-
| `m().step({ kind: 'nope' })`, `m().step(42)` | `step() takes a migration step with a recognised kind (ddl, jslt, query, derive, sql, rebuild), got kind a string` | one of the
|
|
478
|
+
| `m().step({ kind: 'nope' })`, `m().step(42)` | `step() takes a migration step with a recognised kind (ddl, jslt, query, derive, sql, rebuild, table), got kind a string` | one of the seven kinds |
|
|
484
479
|
| `m().step({ kind: 'rebuild', table: 'User' })` | `step() 'rebuild' needs 'create' (MIGRATION-FORMAT §2)` — `docPath` `/create` | the rendered parts |
|
|
485
480
|
| `m().step({ kind: 'sql' })` | `step() 'sql' needs 'sql' (MIGRATION-FORMAT §2)` — `docPath` `/sql` | the statement |
|
|
486
481
|
| `m().step({ kind: 'jslt', collection: 'User' })` | `step() 'jslt' needs 'stylesheet' (MIGRATION-FORMAT §2)` — `docPath` `/stylesheet` | a rules array |
|
|
@@ -532,8 +527,9 @@ exactly what it must not have.
|
|
|
532
527
|
|
|
533
528
|
This pen's own code. Two conditions, and no other pen raises either.
|
|
534
529
|
|
|
535
|
-
**A step naming a table
|
|
536
|
-
`transform
|
|
530
|
+
**A step naming a table its selected model does not declare.** Checked for
|
|
531
|
+
`transform` and `assert` against an explicit current model when supplied,
|
|
532
|
+
or the known target otherwise. `derive` checks the known target.
|
|
537
533
|
|
|
538
534
|
| The spelling that trips it | The message | The spelling that works |
|
|
539
535
|
|---|---|---|
|
|
@@ -553,20 +549,20 @@ no steps, so there is never a draft.
|
|
|
553
549
|
|
|
554
550
|
| The spelling that trips it | The message | The spelling that works |
|
|
555
551
|
|---|---|---|
|
|
556
|
-
| `fromPlanned(planned).transform('Nope', fn)` — no models given | `transform() over 'Nope': the planned migration drafts no transform for it and no target model was given — pass { to } to fromPlanned(), or spell the step with step()` | `fromPlanned
|
|
552
|
+
| `fromPlanned(planned).transform('Nope', fn)` — no models given | `transform() over 'Nope': the planned migration drafts no transform for it and no target model or explicit step model was given — pass { model } to transform(), { to } to fromPlanned(), or spell the step with step()` | supply the current `{ model }`, give `fromPlanned` its `{ to }`, or use `.step({ kind: 'jslt', … })` |
|
|
557
553
|
| `fromPlanned(twoDrafts).transform('User', fn)` | `transform() cannot tell which draft to replace: the planned migration carries 2 draft transforms for 'User'` | edit the planned document down to one draft per table, or replace them with `step()` calls |
|
|
558
554
|
|
|
559
555
|
The first is not a mistake so much as a missing fact: without a target
|
|
560
556
|
model the pen knows only which tables the planner drafted, so a name it
|
|
561
557
|
has never seen could be a typo or could be perfectly good. It refuses and
|
|
562
|
-
names
|
|
558
|
+
names the available ways to supply that fact. The second cannot arise from
|
|
563
559
|
`jaren-db plan`, which drafts at most one transform per table; it arises
|
|
564
560
|
when a planned document is edited or two are concatenated, and a pen that
|
|
565
561
|
picked one would be picking which of the author's two transforms to
|
|
566
562
|
throw away.
|
|
567
563
|
|
|
568
|
-
|
|
569
|
-
`derive` and `step`
|
|
564
|
+
With neither a target nor an explicit step model, `ddl`, `sql`, `assert`,
|
|
565
|
+
`derive` and `step` leave undeclared-name checks to the runner. That is
|
|
570
566
|
the honest position — the pen refuses what it can see, and it cannot see
|
|
571
567
|
a model it was not given.
|
|
572
568
|
|
|
@@ -609,6 +605,14 @@ over old rows and a postcondition over new ones, and the pen cannot know
|
|
|
609
605
|
which this one is, so what both agree on is what neither lies about;
|
|
610
606
|
annotate (`(u: Expr<NewUser>) => …`) when one shape is meant.
|
|
611
607
|
|
|
608
|
+
An explicit `{ model: current }` overrides these default type readings.
|
|
609
|
+
The transform row and result both use `DocOf<typeof current, name>`;
|
|
610
|
+
an assertion reads that current shape. `fromPlanned` retains the entire
|
|
611
|
+
physical header as immutable JSON. Db's generic `PhysicalMigrationDocument`
|
|
612
|
+
preserves supplied step types, so `fromPlanned(await planPhysicalMigration(...))`
|
|
613
|
+
composes directly when the steps are typed. Unknown saved input remains
|
|
614
|
+
unknown until validated; incomplete table artifacts do not gain missing guards.
|
|
615
|
+
|
|
612
616
|
### 5.2 The honest limits, and the two routes past them
|
|
613
617
|
|
|
614
618
|
**From a JSON snapshot the old row is `unknown`.** A JSON literal is
|
|
@@ -674,6 +678,27 @@ be re-implementing the planner. The alternative for all three is the same
|
|
|
674
678
|
one the format intends: let `jaren-db plan` render them, and take the
|
|
675
679
|
document up with `fromPlanned`.
|
|
676
680
|
|
|
681
|
+
`table` also uses `step()`, carrying the entire reviewed
|
|
682
|
+
`planTableMigration` artifact. Its `source`, `after`, row/storage and identity
|
|
683
|
+
guards must not be replaced by a loop over `statements` and `finish`.
|
|
684
|
+
The pen checks the complete artifact's structure and preserves it; db checks
|
|
685
|
+
the source/checksum and executes its guarded transaction.
|
|
686
|
+
|
|
687
|
+
Historical models can precede and follow that structural step:
|
|
688
|
+
|
|
689
|
+
```js
|
|
690
|
+
const steps = defineMigration({ id: 'upgrade', from: before, to: after })
|
|
691
|
+
.transform('Item', (row) => ({ id: row.id, value: row.value.upper() }), { model: before })
|
|
692
|
+
.step({ kind: 'table', plan: reviewedTablePlan })
|
|
693
|
+
.assert('Item', (row) => row.revision.lt(1), { model: after }).document.steps;
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
The [runnable db example](../../db/docs/MIGRATION-FORMAT.md#runnable-physical-lifecycle)
|
|
697
|
+
supplies the complete model/table declarations, reviewed target fixture,
|
|
698
|
+
physical dispositions, populated replay and checked second startup. Synthetic
|
|
699
|
+
fixture rows stay outside the shared migration document. Physical model diffing
|
|
700
|
+
still refuses changed declarations; the author reviews structural plans.
|
|
701
|
+
|
|
677
702
|
### 6.2 `defineMigration` does not validate a migration against a database
|
|
678
703
|
|
|
679
704
|
This is the sentence a reader most needs, because believing otherwise
|
|
@@ -687,8 +712,10 @@ below is `@jarenjs/db`'s `migrate()`, and none of it has happened when
|
|
|
687
712
|
- **The shadow replay.** Before the real store is touched, the WHOLE
|
|
688
713
|
chain — baseline, applied and pending — replays on a shadow database:
|
|
689
714
|
every DDL statement runs, every stylesheet and assertion compiles and
|
|
690
|
-
executes, and the
|
|
691
|
-
|
|
715
|
+
executes, and the selected model or complete physical target is checked.
|
|
716
|
+
`shadowFixture` may initialize an application-owned historical schema;
|
|
717
|
+
replay still uses the same migration/history executor (MIGRATION-FORMAT §4).
|
|
718
|
+
A step the pen accepted and SQLite rejects fails
|
|
692
719
|
there, with the real store untouched.
|
|
693
720
|
- **The widening/narrowing check against real data.** At the end of the
|
|
694
721
|
run, inside its transaction, every stored document is validated against
|
|
@@ -745,10 +772,10 @@ from a drop plus a create, and guessing risks silent data loss.
|
|
|
745
772
|
|
|
746
773
|
## 7. Cost
|
|
747
774
|
|
|
748
|
-
`@jarenjs/linq/migration` builds to **<!--fact:bundle.migration-->
|
|
775
|
+
`@jarenjs/linq/migration` builds to **<!--fact:bundle.migration-->23,806<!--/fact--> bytes** as a minified,
|
|
749
776
|
tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
|
|
750
777
|
measures and `npm run test:tree-shaking` reports, published rounded
|
|
751
|
-
(<!--fact:bundle.migration.kb-->
|
|
778
|
+
(<!--fact:bundle.migration.kb-->24<!--/fact--> kB) beside the other nine subpath prices in
|
|
752
779
|
[docs/CONSUMING.md](../../../docs/CONSUMING.md).
|
|
753
780
|
|
|
754
781
|
The probe is a gate, not a report: building a two-step migration as a
|
package/docs/QUERY-PEN.md
CHANGED
|
@@ -1717,7 +1717,7 @@ making:
|
|
|
1717
1717
|
`docs/CONSUMING.md` states the rounded price of all ten subpaths in one
|
|
1718
1718
|
table, each figure held equal to the same measurements. Two of its rows
|
|
1719
1719
|
are the ones to read together: the chain at <!--fact:bundle.chain.kb-->176<!--/fact--> kB and
|
|
1720
|
-
`./db` at <!--fact:bundle.db.kb-->
|
|
1720
|
+
`./db` at <!--fact:bundle.db.kb-->728<!--/fact--> kB.
|
|
1721
1721
|
The client costs what the store costs, by construction, and the chain
|
|
1722
1722
|
costs what running a query costs.
|
|
1723
1723
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/linq",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.89.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
@@ -108,13 +108,13 @@
|
|
|
108
108
|
"prepack": "npm run build:types"
|
|
109
109
|
},
|
|
110
110
|
"dependencies": {
|
|
111
|
-
"@jarenjs/core": "^0.
|
|
112
|
-
"@jarenjs/json": "^0.
|
|
111
|
+
"@jarenjs/core": "^0.89.0",
|
|
112
|
+
"@jarenjs/json": "^0.89.0"
|
|
113
113
|
},
|
|
114
114
|
"peerDependencies": {
|
|
115
|
-
"@jarenjs/db": "^0.
|
|
116
|
-
"@jarenjs/formats": "^0.
|
|
117
|
-
"@jarenjs/validate": "^0.
|
|
115
|
+
"@jarenjs/db": "^0.89.0",
|
|
116
|
+
"@jarenjs/formats": "^0.89.0",
|
|
117
|
+
"@jarenjs/validate": "^0.89.0"
|
|
118
118
|
},
|
|
119
119
|
"peerDependenciesMeta": {
|
|
120
120
|
"@jarenjs/db": {
|
package/src/migration/define.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* and nothing here ever clears a `draft` flag: a draft left in place
|
|
11
11
|
* still refuses to run (`JD0021`, the runner's rule). The pen refuses
|
|
12
12
|
* what it cannot spell and what the runner would refuse later and the
|
|
13
|
-
* pen can see now — a step over a table the
|
|
13
|
+
* pen can see now — a step over a table the selected model does not
|
|
14
14
|
* declare (`JL0106`).
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from './steps.js';
|
|
26
26
|
|
|
27
27
|
const MIGRATION_VERSION = '0.1';
|
|
28
|
-
const HEAD_MEMBERS = ['$migration', 'id', 'from', 'to', 'note', 'steps'];
|
|
28
|
+
const HEAD_MEMBERS = ['$migration', 'id', 'from', 'to', 'note', 'steps', 'physical'];
|
|
29
29
|
|
|
30
30
|
/** A JSON value, copied: the document is a value of its own. @param {any} v */
|
|
31
31
|
const copy = (v) => JSON.parse(JSON.stringify(v));
|
|
@@ -132,14 +132,14 @@ export class Migration {
|
|
|
132
132
|
return this.#with([...this.#steps, step]);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/** A step's table must be one
|
|
136
|
-
* target
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
135
|
+
/** A step's table must be one its explicit current model or the known
|
|
136
|
+
* target model declares; the pen catches a missing name before execution. */
|
|
137
|
+
#requireDeclared(name, what, model = undefined) {
|
|
138
|
+
const names = model === undefined ? this.#names : declaredNames(model);
|
|
139
|
+
if (names !== null && !names.includes(name)) {
|
|
140
140
|
throw new LinqBuildError('JL0106',
|
|
141
|
-
`${what} names '${name}', which the target model does not declare — it declares `
|
|
142
|
-
+ (
|
|
141
|
+
`${what} names '${name}', which the ${model === undefined ? 'target' : 'step'} model does not declare — it declares `
|
|
142
|
+
+ (names.length === 0 ? 'nothing' : names.map((n) => `'${n}'`).join(', ')));
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -156,15 +156,17 @@ export class Migration {
|
|
|
156
156
|
/**
|
|
157
157
|
* The transform of one table's rows (a `jslt` step). Over a planned
|
|
158
158
|
* document it REPLACES the draft the planner left for that table, in
|
|
159
|
-
* place; otherwise it is appended, for a table the
|
|
160
|
-
* declares. Two drafts for one name, a draft-less planned
|
|
161
|
-
*
|
|
159
|
+
* place; otherwise it is appended, for a table the explicit current or
|
|
160
|
+
* target model declares. Two drafts for one name, a draft-less planned
|
|
161
|
+
* document with neither model, or an undeclared name are `JL0106`.
|
|
162
162
|
* @param {string} name
|
|
163
163
|
* @param {any} spelling - a callback `(row, x) => …`, a `stylesheet(…)`
|
|
164
164
|
* document, or a rules array
|
|
165
|
+
* @param {{ model?: any }} [options] - The layout at this step, before later DDL
|
|
165
166
|
*/
|
|
166
|
-
transform(name, spelling) {
|
|
167
|
-
const step = transformStep(name, spelling);
|
|
167
|
+
transform(name, spelling, options = undefined) {
|
|
168
|
+
const step = transformStep(name, spelling, options);
|
|
169
|
+
if (step.model !== undefined) this.#requireDeclared(name, 'transform()', step.model);
|
|
168
170
|
const drafts = [];
|
|
169
171
|
this.#steps.forEach((s, i) => {
|
|
170
172
|
if (s.kind === 'jslt' && s.draft === true && s.collection === name) drafts.push(i);
|
|
@@ -179,12 +181,12 @@ export class Migration {
|
|
|
179
181
|
next[drafts[0]] = step;
|
|
180
182
|
return this.#with(next);
|
|
181
183
|
}
|
|
182
|
-
if (this.#names === null) {
|
|
184
|
+
if (this.#names === null && step.model === undefined) {
|
|
183
185
|
throw new LinqBuildError('JL0106',
|
|
184
186
|
`transform() over '${name}': the planned migration drafts no transform for it and no `
|
|
185
|
-
+ 'target model was given — pass { to } to fromPlanned(), or spell the step with step()');
|
|
187
|
+
+ 'target model or explicit step model was given — pass { model } to transform(), { to } to fromPlanned(), or spell the step with step()');
|
|
186
188
|
}
|
|
187
|
-
this.#requireDeclared(name, 'transform()');
|
|
189
|
+
this.#requireDeclared(name, 'transform()', step.model);
|
|
188
190
|
return this.#append(step);
|
|
189
191
|
}
|
|
190
192
|
|
|
@@ -192,11 +194,11 @@ export class Migration {
|
|
|
192
194
|
* An assertion over one table's rows (a `query` step).
|
|
193
195
|
* @param {string} name
|
|
194
196
|
* @param {any} spelling - a predicate `(row) => …`, or a query document
|
|
195
|
-
* @param {{ expect?: 'empty' | 'ebv' }} [options]
|
|
197
|
+
* @param {{ expect?: 'empty' | 'ebv', model?: any }} [options]
|
|
196
198
|
*/
|
|
197
199
|
assert(name, spelling, options = undefined) {
|
|
198
200
|
const step = assertStep(name, spelling, options);
|
|
199
|
-
this.#requireDeclared(name, 'assert()');
|
|
201
|
+
this.#requireDeclared(name, 'assert()', step.model);
|
|
200
202
|
return this.#append(step);
|
|
201
203
|
}
|
|
202
204
|
|
|
@@ -314,5 +316,13 @@ export function fromPlanned(document, options = undefined) {
|
|
|
314
316
|
}
|
|
315
317
|
head.note = doc.note;
|
|
316
318
|
}
|
|
319
|
+
if (doc.physical !== undefined) {
|
|
320
|
+
if (!isJsonObject(doc.physical) || !Array.isArray(doc.physical.source)
|
|
321
|
+
|| !isJsonObject(doc.physical.dispositions) || !Array.isArray(doc.physical.assertions)) {
|
|
322
|
+
throw new LinqBuildError('JL0101',
|
|
323
|
+
'fromPlanned() physical header needs source, dispositions and assertions', '/physical');
|
|
324
|
+
}
|
|
325
|
+
head.physical = doc.physical;
|
|
326
|
+
}
|
|
317
327
|
return new Migration(head, doc.steps.map((step) => rawStep(step)), names);
|
|
318
328
|
}
|
package/src/migration/steps.js
CHANGED
|
@@ -20,7 +20,7 @@ import { describeValue, requireJson } from '../json-boundary.js';
|
|
|
20
20
|
import { LinqBuildError } from '../errors.js';
|
|
21
21
|
|
|
22
22
|
/** The kinds the runner accepts, in the artifact's order. */
|
|
23
|
-
const STEP_KINDS = ['ddl', 'jslt', 'query', 'derive', 'sql', 'rebuild'];
|
|
23
|
+
const STEP_KINDS = ['ddl', 'jslt', 'query', 'derive', 'sql', 'rebuild', 'table'];
|
|
24
24
|
const EXPECTS = ['empty', 'ebv'];
|
|
25
25
|
const NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
26
26
|
/** An assertion evaluates with no externals: `p.x` cannot appear. */
|
|
@@ -61,6 +61,15 @@ function readNote(note, what) {
|
|
|
61
61
|
return note;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** A current layout travels as immutable JSON, without importing the model pen. */
|
|
65
|
+
function withModel(step, model) {
|
|
66
|
+
if (model === undefined) return step;
|
|
67
|
+
const doc = requireJson(model, 'step model');
|
|
68
|
+
if (!isJsonObject(doc) || doc.$model !== '0.1')
|
|
69
|
+
throw new LinqBuildError('JL0101', 'a step model must be a $model 0.1 document');
|
|
70
|
+
return { ...step, model: copy(doc) };
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
/** The externals proxy an assertion's predicate sees: nothing. */
|
|
65
74
|
const NO_EXTERNALS = new Proxy(Object.freeze({}), {
|
|
66
75
|
get(_target, prop) {
|
|
@@ -104,8 +113,10 @@ export function sqlStep(sql, note = undefined) {
|
|
|
104
113
|
* @param {string} name
|
|
105
114
|
* @param {any} spelling
|
|
106
115
|
*/
|
|
107
|
-
export function transformStep(name, spelling) {
|
|
116
|
+
export function transformStep(name, spelling, options = undefined) {
|
|
108
117
|
requireName(name, 'transform()');
|
|
118
|
+
if (options !== undefined && (!isJsonObject(options) || Object.keys(options).some((key) => key !== 'model')))
|
|
119
|
+
throw new LinqBuildError('JL0101', 'transform() options are { model? }');
|
|
109
120
|
let stylesheet;
|
|
110
121
|
if (typeof spelling === 'function') {
|
|
111
122
|
stylesheet = [{ match: '$', body: body(spelling) }];
|
|
@@ -133,7 +144,7 @@ export function transformStep(name, spelling) {
|
|
|
133
144
|
'transform() takes a callback (row, x) => …, a stylesheet(…) document or a rules array, '
|
|
134
145
|
+ `got ${describeValue(spelling)}`);
|
|
135
146
|
}
|
|
136
|
-
return { kind: 'jslt', collection: name, stylesheet };
|
|
147
|
+
return withModel({ kind: 'jslt', collection: name, stylesheet }, options?.model);
|
|
137
148
|
}
|
|
138
149
|
|
|
139
150
|
/**
|
|
@@ -145,7 +156,7 @@ export function transformStep(name, spelling) {
|
|
|
145
156
|
* are the witness. A document is taken verbatim.
|
|
146
157
|
* @param {string} name
|
|
147
158
|
* @param {any} spelling
|
|
148
|
-
* @param {{ expect?: 'empty' | 'ebv' }} [options]
|
|
159
|
+
* @param {{ expect?: 'empty' | 'ebv', model?: any }} [options]
|
|
149
160
|
*/
|
|
150
161
|
export function assertStep(name, spelling, options = undefined) {
|
|
151
162
|
requireName(name, 'assert()');
|
|
@@ -155,7 +166,7 @@ export function assertStep(name, spelling, options = undefined) {
|
|
|
155
166
|
throw new LinqBuildError('JL0101', `assert() options are { expect? }, got ${describeValue(options)}`);
|
|
156
167
|
}
|
|
157
168
|
for (const key of Object.keys(options)) {
|
|
158
|
-
if (key !== 'expect') throw new LinqBuildError('JL0101', `assert() does not take '${key}'`);
|
|
169
|
+
if (key !== 'expect' && key !== 'model') throw new LinqBuildError('JL0101', `assert() does not take '${key}'`);
|
|
159
170
|
}
|
|
160
171
|
if (options.expect !== undefined) {
|
|
161
172
|
if (!EXPECTS.includes(options.expect)) {
|
|
@@ -180,7 +191,7 @@ export function assertStep(name, spelling, options = undefined) {
|
|
|
180
191
|
}
|
|
181
192
|
const out = { kind: 'query', collection: name, assert: query };
|
|
182
193
|
if (expect === 'ebv') out.expect = 'ebv';
|
|
183
|
-
return out;
|
|
194
|
+
return withModel(out, options?.model);
|
|
184
195
|
}
|
|
185
196
|
|
|
186
197
|
/**
|
|
@@ -219,6 +230,14 @@ export function rawStep(step) {
|
|
|
219
230
|
}
|
|
220
231
|
};
|
|
221
232
|
switch (raw.kind) {
|
|
233
|
+
case 'table':
|
|
234
|
+
need('plan', isJsonObject(raw.plan) && raw.plan.version === 1
|
|
235
|
+
&& ['id', 'table', 'checksum', 'temporary'].every((key) => typeof raw.plan[key] === 'string' && raw.plan[key].length > 0)
|
|
236
|
+
&& typeof raw.plan.rebuild === 'boolean'
|
|
237
|
+
&& ['source', 'after'].every((key) => Array.isArray(raw.plan[key]) && raw.plan[key].every(isJsonObject))
|
|
238
|
+
&& ['unchanged', 'statements', 'finish'].every((key) => Array.isArray(raw.plan[key])
|
|
239
|
+
&& raw.plan[key].every((value) => typeof value === 'string' && value.length > 0)));
|
|
240
|
+
break;
|
|
222
241
|
case 'ddl': case 'sql':
|
|
223
242
|
need('sql', typeof raw.sql === 'string' && raw.sql !== '');
|
|
224
243
|
break;
|
|
@@ -240,5 +259,5 @@ export function rawStep(step) {
|
|
|
240
259
|
need('copy', typeof raw.copy === 'string');
|
|
241
260
|
need('indexes', Array.isArray(raw.indexes));
|
|
242
261
|
}
|
|
243
|
-
return raw;
|
|
262
|
+
return ['jslt', 'query'].includes(raw.kind) ? withModel(raw, raw.model) : raw;
|
|
244
263
|
}
|
package/types/migration.d.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* assertion's row is typed by the members the two shapes SHARE (a
|
|
19
19
|
* precondition sees old rows, a postcondition new ones; what both agree
|
|
20
20
|
* on is what neither lies about), annotated when one shape is meant.
|
|
21
|
+
* With an explicit step `model`, transform input/output and assertion
|
|
22
|
+
* rows use that model's layout, including names absent from the final model.
|
|
21
23
|
* Every claim here has a runtime twin in
|
|
22
24
|
* `test/linq/migration-pen.test.js` and a compile-level pin in
|
|
23
25
|
* `test/consumer/linq-migration.ts`.
|
|
@@ -39,6 +41,8 @@ export interface JsltStep {
|
|
|
39
41
|
/** A planner placeholder; the runner refuses it (`JD0021`). The pen never sets or clears it. */
|
|
40
42
|
readonly draft?: boolean;
|
|
41
43
|
readonly note?: string;
|
|
44
|
+
/** The immutable model for this step's current layout. */
|
|
45
|
+
readonly model?: object;
|
|
42
46
|
}
|
|
43
47
|
export interface QueryStep {
|
|
44
48
|
readonly kind: 'query';
|
|
@@ -46,6 +50,7 @@ export interface QueryStep {
|
|
|
46
50
|
readonly assert: Json;
|
|
47
51
|
readonly expect?: 'empty' | 'ebv';
|
|
48
52
|
readonly note?: string;
|
|
53
|
+
readonly model?: object;
|
|
49
54
|
}
|
|
50
55
|
export interface DeriveColumn {
|
|
51
56
|
readonly name: string;
|
|
@@ -69,7 +74,39 @@ export interface RebuildStep {
|
|
|
69
74
|
readonly indexes: readonly string[];
|
|
70
75
|
readonly note?: string;
|
|
71
76
|
}
|
|
72
|
-
|
|
77
|
+
/** The complete serializable artifact returned by planTableMigration. */
|
|
78
|
+
export interface ReviewedTablePlan {
|
|
79
|
+
readonly version: 1;
|
|
80
|
+
readonly id: string;
|
|
81
|
+
readonly table: string;
|
|
82
|
+
readonly checksum: string;
|
|
83
|
+
readonly source: readonly unknown[];
|
|
84
|
+
readonly after: readonly unknown[];
|
|
85
|
+
readonly rebuild: boolean;
|
|
86
|
+
readonly temporary: string;
|
|
87
|
+
readonly unchanged: readonly string[];
|
|
88
|
+
readonly statements: readonly string[];
|
|
89
|
+
readonly finish: readonly string[];
|
|
90
|
+
}
|
|
91
|
+
export interface TableStep {
|
|
92
|
+
readonly kind: 'table';
|
|
93
|
+
readonly plan: ReviewedTablePlan;
|
|
94
|
+
readonly note?: string;
|
|
95
|
+
}
|
|
96
|
+
export type MigrationStep = DdlStep | SqlStep | JsltStep | QueryStep | DeriveStep | RebuildStep | TableStep;
|
|
97
|
+
|
|
98
|
+
export interface PhysicalObject {
|
|
99
|
+
readonly type: 'table' | 'view' | 'index' | 'trigger';
|
|
100
|
+
readonly name: string;
|
|
101
|
+
readonly owner: string;
|
|
102
|
+
readonly sql: string | null;
|
|
103
|
+
}
|
|
104
|
+
export interface PhysicalHeader {
|
|
105
|
+
readonly source: readonly PhysicalObject[];
|
|
106
|
+
readonly dispositions: Readonly<Record<string, 'preserve' | 'replace' | 'drop'>>;
|
|
107
|
+
readonly assertions: readonly { readonly sql: string; readonly params?: readonly unknown[]; readonly expected: readonly unknown[] }[];
|
|
108
|
+
readonly target?: { readonly objects: readonly PhysicalObject[]; readonly tables?: readonly string[] };
|
|
109
|
+
}
|
|
73
110
|
|
|
74
111
|
/** The `$migration` 0.1 document (MIGRATION-FORMAT §2). */
|
|
75
112
|
export interface MigrationDocument {
|
|
@@ -79,6 +116,7 @@ export interface MigrationDocument {
|
|
|
79
116
|
readonly to: string;
|
|
80
117
|
readonly note?: string;
|
|
81
118
|
readonly steps: readonly MigrationStep[];
|
|
119
|
+
readonly physical?: PhysicalHeader;
|
|
82
120
|
}
|
|
83
121
|
|
|
84
122
|
// ————— what a model document types —————
|
|
@@ -124,7 +162,11 @@ export type SheetFor<S, New> = S extends { readonly __out: infer O }
|
|
|
124
162
|
? (unknown extends O ? unknown : O extends New ? unknown : never)
|
|
125
163
|
: unknown;
|
|
126
164
|
|
|
127
|
-
export interface
|
|
165
|
+
export interface TransformOptions<Current = never> {
|
|
166
|
+
/** Override the final model with the layout at this step. */
|
|
167
|
+
readonly model?: Current;
|
|
168
|
+
}
|
|
169
|
+
export interface AssertOptions<Current = never> extends TransformOptions<Current> {
|
|
128
170
|
/** `'empty'` (the default, absent from the document): no row may
|
|
129
171
|
* satisfy the predicate — it names the violation; `'ebv'`: the matching
|
|
130
172
|
* rows are the witness. */
|
|
@@ -141,6 +183,22 @@ export class Migration<From = unknown, To = unknown> {
|
|
|
141
183
|
ddl(sql: string, note?: string): Migration<From, To>;
|
|
142
184
|
/** One data statement spelled directly (`sql`, MIGRATION-FORMAT §9.4). */
|
|
143
185
|
sql(sql: string, note?: string): Migration<From, To>;
|
|
186
|
+
/** With an explicit current model, both the row and result use that layout. */
|
|
187
|
+
transform<Current extends object, N extends DeclaredNames<Current>, V extends ExprBase<unknown> = MemberExpr<DocOf<Current, N>>>(
|
|
188
|
+
name: N,
|
|
189
|
+
rule: (row: V, x: Externals<{}, ValueOf<V>>) => Spell<DocOf<Current, N>>,
|
|
190
|
+
options: TransformOptions<Current> & { readonly model: Current },
|
|
191
|
+
): Migration<From, To>;
|
|
192
|
+
transform<Current extends object, N extends DeclaredNames<Current>, S extends StylesheetLike>(
|
|
193
|
+
name: N,
|
|
194
|
+
stylesheet: S & SheetFor<S, DocOf<Current, N>>,
|
|
195
|
+
options: TransformOptions<Current> & { readonly model: Current },
|
|
196
|
+
): Migration<From, To>;
|
|
197
|
+
transform<Current extends object, N extends DeclaredNames<Current>, const R extends readonly RuleDocument[]>(
|
|
198
|
+
name: N,
|
|
199
|
+
rules: R & RulesFor<R, DocOf<Current, N>>,
|
|
200
|
+
options: TransformOptions<Current> & { readonly model: Current },
|
|
201
|
+
): Migration<From, To>;
|
|
144
202
|
/** A `jslt` step: one root rule captured over the OLD row (`root`/`path`
|
|
145
203
|
* as externals), whose result spells the NEW row. Over a planned
|
|
146
204
|
* document it replaces the draft for `name`; otherwise it is appended
|
|
@@ -148,6 +206,7 @@ export class Migration<From = unknown, To = unknown> {
|
|
|
148
206
|
transform<N extends DeclaredNames<To>, V extends ExprBase<unknown> = MemberExpr<DocOf<From, N>>>(
|
|
149
207
|
name: N,
|
|
150
208
|
rule: (row: V, x: Externals<{}, ValueOf<V>>) => Spell<DocOf<To, N>>,
|
|
209
|
+
options?: TransformOptions,
|
|
151
210
|
): Migration<From, To>;
|
|
152
211
|
/** A `stylesheet(…)` document — its output must be the new row when it
|
|
153
212
|
* is typed; a hand-written envelope is the honest top. A disposition or
|
|
@@ -155,12 +214,23 @@ export class Migration<From = unknown, To = unknown> {
|
|
|
155
214
|
transform<N extends DeclaredNames<To>, S extends StylesheetLike>(
|
|
156
215
|
name: N,
|
|
157
216
|
stylesheet: S & SheetFor<S, DocOf<To, N>>,
|
|
217
|
+
options?: TransformOptions,
|
|
158
218
|
): Migration<From, To>;
|
|
159
219
|
/** A rules array: a typed first rule must produce the new row; a
|
|
160
220
|
* hand-written rule is the honest top. */
|
|
161
221
|
transform<N extends DeclaredNames<To>, const R extends readonly RuleDocument[]>(
|
|
162
222
|
name: N,
|
|
163
223
|
rules: R & RulesFor<R, DocOf<To, N>>,
|
|
224
|
+
options?: TransformOptions,
|
|
225
|
+
): Migration<From, To>;
|
|
226
|
+
/** An explicit current model types a historical assertion's row. */
|
|
227
|
+
assert<Current extends object, N extends DeclaredNames<Current>, V extends ExprBase<unknown> = MemberExpr<DocOf<Current, N>>>(
|
|
228
|
+
name: N,
|
|
229
|
+
predicate: (row: V) => BoolExpr | boolean,
|
|
230
|
+
options: AssertOptions<Current> & { readonly model: Current },
|
|
231
|
+
): Migration<From, To>;
|
|
232
|
+
assert<Current extends object, N extends DeclaredNames<Current>>(
|
|
233
|
+
name: N, query: Json, options: AssertOptions<Current> & { readonly model: Current },
|
|
164
234
|
): Migration<From, To>;
|
|
165
235
|
/** A `query` step over `name`'s rows: the predicate names the VIOLATION
|
|
166
236
|
* (`expect: 'empty'`, the default) or the witness (`'ebv'`); the row is
|