@jarenjs/db 0.49.2 → 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.
- package/ARCHITECTURE.md +420 -71
- package/README.md +711 -79
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +309 -45
- package/docs/LIVE-FORMAT.md +156 -19
- package/docs/MIGRATION-FORMAT.md +247 -40
- package/docs/MODEL-FORMAT.md +968 -86
- package/package.json +21 -8
- package/schemas/jaren-migration.draft-07.schema.json +73 -0
- package/schemas/jaren-migration.schema.json +73 -0
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +255 -44
- package/src/cli.js +337 -50
- package/src/cursor.js +411 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +125 -11
- package/src/dialect.js +267 -112
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +245 -12
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +503 -69
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +18 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit-model.js +14 -0
- package/src/emit.js +349 -51
- package/src/entity.js +102 -59
- package/src/errors.js +430 -2
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -19
- package/src/introspect.js +583 -0
- package/src/jobs.js +870 -99
- package/src/json-bytes.js +58 -0
- package/src/live-time.js +12 -3
- package/src/live.js +11 -1
- package/src/maintenance.js +175 -0
- package/src/migrate.js +606 -333
- package/src/model.js +241 -8
- package/src/plan.js +1238 -160
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1748 -312
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1672 -276
- package/src/tracker.js +367 -68
- package/src/udf.js +88 -7
- package/types/index.d.ts +1246 -32
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +72 -3
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +81 -3
- package/types/wasm.d.ts +21 -0
- package/dist/types/algebra.d.ts +0 -230
- package/dist/types/app.d.ts +0 -49
- package/dist/types/capture.d.ts +0 -85
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/dag-job.d.ts +0 -40
- package/dist/types/ddl.d.ts +0 -229
- package/dist/types/derive.d.ts +0 -250
- package/dist/types/dialect.d.ts +0 -154
- package/dist/types/dialects/sqlite.d.ts +0 -9
- package/dist/types/driver.d.ts +0 -110
- package/dist/types/drivers/bun.d.ts +0 -47
- package/dist/types/drivers/node.d.ts +0 -37
- package/dist/types/drivers/wasm.d.ts +0 -65
- package/dist/types/emit-model.d.ts +0 -44
- package/dist/types/emit.d.ts +0 -75
- package/dist/types/entity.d.ts +0 -23
- package/dist/types/errors.d.ts +0 -170
- package/dist/types/graph.d.ts +0 -28
- package/dist/types/index.d.ts +0 -37
- package/dist/types/jobs.d.ts +0 -140
- package/dist/types/knn.d.ts +0 -69
- package/dist/types/live-time.d.ts +0 -141
- package/dist/types/live.d.ts +0 -64
- package/dist/types/migrate.d.ts +0 -170
- package/dist/types/model.d.ts +0 -36
- package/dist/types/patch-sql.d.ts +0 -37
- package/dist/types/plan.d.ts +0 -142
- package/dist/types/profile.d.ts +0 -80
- package/dist/types/query.d.ts +0 -112
- package/dist/types/residual.d.ts +0 -64
- package/dist/types/series.d.ts +0 -227
- package/dist/types/store.d.ts +0 -60
- package/dist/types/tracker.d.ts +0 -43
- package/dist/types/typed.d.ts +0 -15
- package/dist/types/types.d.ts +0 -26
- package/dist/types/udf.d.ts +0 -75
- package/dist/types/window.d.ts +0 -52
package/docs/LIVE-FORMAT.md
CHANGED
|
@@ -92,7 +92,10 @@ them).
|
|
|
92
92
|
already knows what it wrote. **Less complete, stated plainly**: it
|
|
93
93
|
cannot see writes made through raw SQL, triggers, or another
|
|
94
94
|
connection; a journal-mode delete of a row the store never read
|
|
95
|
-
emits its `remove` without having seen the old document;
|
|
95
|
+
emits its `remove` without having seen the old document; a keyed
|
|
96
|
+
`put` reads the stored document first, so it emits the `replace`
|
|
97
|
+
session mode emits and a `put` that changes nothing emits nothing;
|
|
98
|
+
and of the
|
|
96
99
|
database's own `ON DELETE` side effects it reconstructs exactly ONE
|
|
97
100
|
— join-table membership dying with its entity (read before the
|
|
98
101
|
delete) — while cascades into CHILD rows (`onDelete: 'cascade'` /
|
|
@@ -114,14 +117,86 @@ reads forward:
|
|
|
114
117
|
const records = await store.changesSince(lastSeq); // JD2051 when no log
|
|
115
118
|
```
|
|
116
119
|
|
|
117
|
-
`seq` is monotonic; with the log enabled
|
|
118
|
-
|
|
120
|
+
`seq` is monotonic; with the log enabled the DATABASE allocates it —
|
|
121
|
+
one statement, inside the write's own transaction, advances a durable
|
|
122
|
+
singleton row (`_jaren_changes_state`, the highest sequence this file
|
|
123
|
+
ever allocated) and reads the new value back through `RETURNING`, and
|
|
124
|
+
the record is then inserted under it — so two stores over one file
|
|
125
|
+
never collide on the log's key, each sees the other's sequence continue,
|
|
126
|
+
and a write that rolls back takes its allocation back with its row. The
|
|
127
|
+
state row is engine metadata: it is created beside the log, seeded once
|
|
128
|
+
from an existing file's surviving `MAX(seq)` (0 for a file that never
|
|
129
|
+
held a row; a later open changes nothing), never lowered, never pruned,
|
|
130
|
+
and never a capture, live, model or migration subject. Without the log,
|
|
131
|
+
`seq` is per-process. `changesSince`
|
|
132
|
+
answers records in the shape observers receive, `collections`
|
|
133
|
+
included; a cursor that is not a number is a `TypeError`, as is a
|
|
134
|
+
`retention` that is not a positive integer. Retention is
|
|
119
135
|
a bounded count (`retention`, default 1000): older rows are pruned in
|
|
120
|
-
the same transaction
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
136
|
+
the same transaction — every write deletes the records more than
|
|
137
|
+
`retention` behind the one it just appended, and nothing else prunes
|
|
138
|
+
the log. The log is an ordered, replayable stream — which is what
|
|
139
|
+
makes a late-joining consumer possible.
|
|
140
|
+
|
|
141
|
+
**`changesSince` is unbounded, and unsafe for a reconnecting
|
|
142
|
+
consumer.** It answers every surviving record in one array, with no
|
|
143
|
+
limit, no byte bound and no watermark: a consumer whose last `seq` fell
|
|
144
|
+
below the retention floor receives the suffix that happens to survive
|
|
145
|
+
and cannot distinguish "everything you missed" from "some of what you
|
|
146
|
+
missed, and the rest is gone" — it believes itself caught up with a
|
|
147
|
+
hole in its state. It stays as a published member, and it is not the
|
|
148
|
+
supported path for a consumer that reconnects.
|
|
149
|
+
|
|
150
|
+
**The bounded reader: `store.changes`.** Present exactly when the log
|
|
151
|
+
is enabled (`JD2051` otherwise, as `changesSince`).
|
|
152
|
+
|
|
153
|
+
- `changes.bounds()` answers the two watermarks: `earliestAvailable`,
|
|
154
|
+
the earliest surviving sequence (`MIN(seq)` over the log; `null` when
|
|
155
|
+
nothing survives), and `highWatermark`, the highest sequence the FILE
|
|
156
|
+
ever allocated, read from the durable state row — so a log that
|
|
157
|
+
retention emptied, reopened in a new process, still answers
|
|
158
|
+
`{ earliestAvailable: null, highWatermark: N }` and a cursor below `N`
|
|
159
|
+
meets `resetRequired` rather than a plausible empty history. Both are
|
|
160
|
+
the file's facts, never a process counter or a clock; cheap, and what a
|
|
161
|
+
consumer needs before it decides whether its cursor is usable.
|
|
162
|
+
- `changes.page({ after, limit, maxBytes, signal })` answers
|
|
163
|
+
`{ items, next, earliestAvailable, highWatermark, hasMore,
|
|
164
|
+
resetRequired }`. `after` is the last sequence seen and is required:
|
|
165
|
+
there is no legitimate "give me everything" for a change log. The
|
|
166
|
+
page never holds more than `limit` records (default 100, applied as
|
|
167
|
+
SQL `LIMIT`) nor more than `maxBytes` serialised patch bytes,
|
|
168
|
+
accumulated at record boundaries; a single record larger than
|
|
169
|
+
`maxBytes` is the refusal `JD2074` without advancing `next` — the
|
|
170
|
+
same rule and the same implementation an entity page uses
|
|
171
|
+
(MODEL-FORMAT §10.5). `hasMore` is decided by one peek past the
|
|
172
|
+
page; `next` is the sequence to continue from (`after` itself when
|
|
173
|
+
nothing was delivered); `signal` cancels at a record boundary
|
|
174
|
+
(`JD2072`).
|
|
175
|
+
- **`resetRequired: true`** when the record after `after` no longer
|
|
176
|
+
survives — `after + 1 < earliestAvailable` (or the log is empty
|
|
177
|
+
above `after`'s successor). Then `items` is **empty** and `next` is
|
|
178
|
+
**absent**: the refusal is total, because a partial suffix beside a
|
|
179
|
+
reset flag would invite a consumer to use both. The watermarks are
|
|
180
|
+
read after the rows, so a floor that rose during the read can only
|
|
181
|
+
make the verdict stricter, never let a pruned gap pass as a
|
|
182
|
+
continuation.
|
|
183
|
+
|
|
184
|
+
**The consumer's recovery procedure**, in words: keep the last `seq`
|
|
185
|
+
you applied; on reconnect, call `changes.page({ after: lastSeq })` and
|
|
186
|
+
apply pages while `hasMore`, storing `next` as you go; when a page
|
|
187
|
+
answers `resetRequired: true`, stop applying — your state has a hole —
|
|
188
|
+
re-seed it from a full snapshot of the collections you follow, and
|
|
189
|
+
resume paging from that page's `highWatermark`, because every record
|
|
190
|
+
at or below it is already reflected in the snapshot you just took.
|
|
191
|
+
Choosing how much history to keep is the host's decision
|
|
192
|
+
(`retention`); what the reader owes is that when rows go, it reports
|
|
193
|
+
the gap instead of hiding it.
|
|
194
|
+
|
|
195
|
+
**Replication is not built here**, and this log alone does not make
|
|
196
|
+
it safe: there is no conflict resolution, no site identity, no causal
|
|
197
|
+
ordering across writers. The bounded reader with its watermarks and
|
|
198
|
+
its explicit gap is the precondition a replication protocol would be
|
|
199
|
+
built on — not the protocol. That sentence is the whole claim.
|
|
125
200
|
|
|
126
201
|
## 6. Cross-connection behaviour and non-claims
|
|
127
202
|
|
|
@@ -159,7 +234,25 @@ live.close();
|
|
|
159
234
|
`store.live(document, options)` registers an entity-root document (the
|
|
160
235
|
multi-entity shape of MODEL-FORMAT §10) the same way. Live queries
|
|
161
236
|
REQUIRE change capture — the patch stream is the invalidation source —
|
|
162
|
-
and registering on a store opened without `capture` is `JD0050`.
|
|
237
|
+
and registering on a store opened without `capture` is `JD0050`. Both
|
|
238
|
+
registrations run under the store gate through their **initial query**
|
|
239
|
+
(MODEL-FORMAT §5.1): a registration made while another transaction is
|
|
240
|
+
open waits for it to settle and initializes from committed rows only, a
|
|
241
|
+
rolled-back row never reaches `result`, and a refused registration
|
|
242
|
+
leaves `stats().liveQueries` unchanged. A registration made from INSIDE
|
|
243
|
+
a transaction view initializes from that transaction's rows and shares
|
|
244
|
+
its fate — kept and maintained on commit, closed on rollback. Once
|
|
245
|
+
registered, the handle is maintained by committed writes alone and takes
|
|
246
|
+
no gate of its own. `changes.page()` takes the same `deadline` every
|
|
247
|
+
other page does (`JD2075` at a record boundary).
|
|
248
|
+
|
|
249
|
+
A producer may hand a registration a CHAIN instead of a document: the
|
|
250
|
+
`@jarenjs/linq/db` client's `live(chain, options)` passes the chain's
|
|
251
|
+
`toDocument()` and its `explain().bindings` as the externals to exactly
|
|
252
|
+
these two registrations (`store.live` for an entity-root chain,
|
|
253
|
+
`collection.live` for a collection's), so the strategy, the reason and
|
|
254
|
+
the maintenance are this table's — an entity chain re-runs, declared —
|
|
255
|
+
and this document stays the only place they are decided.
|
|
163
256
|
|
|
164
257
|
**This table is normative.** Every row is implemented and tested;
|
|
165
258
|
nothing outside it is attempted. Classification reads the compiled
|
|
@@ -260,7 +353,11 @@ state. `externals` are fixed at registration — a query whose inputs
|
|
|
260
353
|
change is a new registration.
|
|
261
354
|
|
|
262
355
|
Maintenance runs synchronously inside patch delivery, in commit
|
|
263
|
-
order, on the store's own connection.
|
|
356
|
+
order, on the store's own connection. Delivery is never re-entered: a
|
|
357
|
+
write made from inside an observer or a subscriber commits at once,
|
|
358
|
+
but its record is queued and delivered after the current record has
|
|
359
|
+
reached every consumer, so sibling live views see commits in commit
|
|
360
|
+
order rather than in call-stack order. Writes from ANOTHER connection
|
|
264
361
|
are invisible to capture (§6) and therefore to live queries; the
|
|
265
362
|
coarse `dataVersion()` signal and the §11 topology are the honest
|
|
266
363
|
answers, and re-registering re-reads.
|
|
@@ -332,8 +429,11 @@ Decided by a platform fact: OPFS synchronous access handles are
|
|
|
332
429
|
all, so "one connection per tab" is not available and never will be.
|
|
333
430
|
Therefore:
|
|
334
431
|
|
|
335
|
-
- ONE owning context holds the sole connection —
|
|
336
|
-
|
|
432
|
+
- ONE owning context holds the sole connection — the first tab's
|
|
433
|
+
dedicated worker to install the OPFS access-handle pool owns it (the
|
|
434
|
+
pool is exclusive by construction) and holds a `navigator.locks` lock
|
|
435
|
+
for its lifetime so a later tab can tell a busy owner from no owner
|
|
436
|
+
(a `BroadcastChannel` ping is the fallback where locks are absent) —
|
|
337
437
|
and every other tab is a client;
|
|
338
438
|
- queries, writes and the patch stream travel between clients and the
|
|
339
439
|
owner over `BroadcastChannel` / `MessagePort`; a client's live query
|
|
@@ -346,8 +446,37 @@ Therefore:
|
|
|
346
446
|
Node and Bun present the same API with no channel at all — the store
|
|
347
447
|
is its own owner, and application code is identical everywhere. The
|
|
348
448
|
in-browser proof of this topology (delivery across real tabs, the
|
|
349
|
-
refusal, reload survival)
|
|
350
|
-
|
|
449
|
+
refusal, reload survival) is the website's Playwright suite over the
|
|
450
|
+
`#/data` studio; this section is the decided contract it implements.
|
|
451
|
+
|
|
452
|
+
The wasm session adapter performs an actual disposable create/attach/changeset/delete
|
|
453
|
+
probe. It declares sessions only after success; `sessionReason` explains journal
|
|
454
|
+
fallback. Changeset bytes are detached from wasm-owned memory before transfer, and
|
|
455
|
+
capture cleanup deletes every session even on rollback or connection close.
|
|
456
|
+
|
|
457
|
+
The studio probes isolated SharedArrayBuffer OPFS, header-free SAH-pool OPFS,
|
|
458
|
+
atomic IndexedDB snapshots, then visibly non-durable memory. IndexedDB snapshots
|
|
459
|
+
require exclusive ownership and acknowledge writes after atomic version replacement.
|
|
460
|
+
They expose no synchronous/live surface; the Store pane explicitly refreshes after
|
|
461
|
+
writes. Failed snapshot persistence invalidates the connection without publishing
|
|
462
|
+
partial state. [Execution hosts](HOSTS.md) specifies bounds and the observed matrix.
|
|
463
|
+
|
|
464
|
+
**The browser boot is a closed protocol.** Reaching an owner, a client
|
|
465
|
+
or a standalone memory store passes through five named stages —
|
|
466
|
+
`worker-start`, `sqlite-init`, `vfs-acquire`, `topology`,
|
|
467
|
+
`store-open` — and every attempt ends in exactly one of two states:
|
|
468
|
+
ready, or a stable failure record `{ code: 'DATA_BOOT', stage, message }`
|
|
469
|
+
naming the stage that failed. Each stage carries its own budget, so a
|
|
470
|
+
stage that never settles fails under its own name rather than under an
|
|
471
|
+
outer deadline that cannot say which resource to release; an OPFS pool
|
|
472
|
+
that is absent advances to the next persistence probe; an existing owner
|
|
473
|
+
produces the `client` answer above, while a pool install that hangs is a `vfs-acquire` failure
|
|
474
|
+
and never masquerades as absence. A failed attempt releases everything it
|
|
475
|
+
created — worker, port client, channel, listeners, timers — before the
|
|
476
|
+
page hears of it, so a retry (or a reload) starts clean, and the page
|
|
477
|
+
shows the stage and offers the retry. The stage runner is
|
|
478
|
+
`packages/website/src/lib/boot-stages.js`; the site's transport and its
|
|
479
|
+
owner worker are the two halves that run it.
|
|
351
480
|
|
|
352
481
|
## 12. Lifecycle, bounds, and non-goals
|
|
353
482
|
|
|
@@ -375,9 +504,11 @@ ERRORING rather than degrading (the D14 rule — the bound is printed):
|
|
|
375
504
|
|
|
376
505
|
Non-claims, in one place: no incremental joins (re-run is the declared
|
|
377
506
|
strategy), no cross-connection invalidation (§6's `data_version` is
|
|
378
|
-
the signal), no maintenance over asynchronous connections
|
|
379
|
-
|
|
380
|
-
|
|
507
|
+
the signal), no maintenance over asynchronous connections —
|
|
508
|
+
`capabilities.live` is `false` there and a registration is `JD0051`
|
|
509
|
+
naming the reason, because maintenance point-reads rows synchronously
|
|
510
|
+
inside delivery (the wasm driver's oo1 API is synchronous, which is
|
|
511
|
+
why the browser has live queries at all) — no replication, and no ordering guarantee for
|
|
381
512
|
unordered queries beyond §9's determinism.
|
|
382
513
|
|
|
383
514
|
## 13. Event time
|
|
@@ -404,7 +535,8 @@ live.stats().watermark; // what it is now
|
|
|
404
535
|
`eventTime` is a **closed** member set: `path`, `watermark`,
|
|
405
536
|
`allowedLateness` (default 0) and `retention`. Anything else — a
|
|
406
537
|
misspelling, a non-finite epoch, a negative lateness, a `path` that is
|
|
407
|
-
not a singular row selector — is `JD0053` at registration
|
|
538
|
+
not a singular row selector — is `JD0053` at registration (its
|
|
539
|
+
`docPath` names the collection, `/collections/<name>`), not a member
|
|
408
540
|
quietly ignored. `advance()` refuses a value that is not finite or that
|
|
409
541
|
goes backwards (a `TypeError`), and it is absent on every view
|
|
410
542
|
registered without an `eventTime`. An entity document has no collection
|
|
@@ -415,7 +547,12 @@ to place rows in and re-runs, so an `eventTime` on `store.live` is
|
|
|
415
547
|
|
|
416
548
|
Two documents, and only these two shapes: `$resample` and `$rolling`
|
|
417
549
|
whose series operand is the collection (`"$[*]"`, or a FLWOR over it
|
|
418
|
-
whose `$where` narrows and whose `$return` is the bare binding).
|
|
550
|
+
whose `$where` narrows and whose `$return` is the bare binding). A
|
|
551
|
+
spec that spells `at` and `value` explicitly — the spelling the query
|
|
552
|
+
language accepts — is maintained: the view folds with the kernel
|
|
553
|
+
reading the declared instant member and the `value` member the spec
|
|
554
|
+
names, and a `value` selector the view cannot follow re-runs with the
|
|
555
|
+
reason named, never a maintained view that dies on its first fold.
|
|
419
556
|
|
|
420
557
|
- **A bucket view keeps its rows by bucket.** A write touches one bucket
|
|
421
558
|
— two, when it moves a reading across a boundary — and exactly those
|
package/docs/MIGRATION-FORMAT.md
CHANGED
|
@@ -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
|
-
|
|
49
|
-
|
|
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,15 @@ 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.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
175
|
+
the shadow, the real and the reference connections before any DDL
|
|
176
|
+
(§10), so a hand-created index over a registered deterministic
|
|
177
|
+
function neither fails the shadow nor is silently dropped by it.
|
|
146
178
|
|
|
147
179
|
## 5. History and checksums
|
|
148
180
|
|
|
@@ -171,23 +203,120 @@ hash of the `baseline` model when no migration has run.
|
|
|
171
203
|
otherwise), the physical end shape is verified, and the real-data
|
|
172
204
|
validation of §3 runs.
|
|
173
205
|
- `dryRun: true` prints every statement and the affected document
|
|
174
|
-
counts, validates the chain on the shadow, and writes NOTHING
|
|
175
|
-
|
|
206
|
+
counts, validates the chain on the shadow, and writes NOTHING — not
|
|
207
|
+
even the history table: it PROBES for one and reads an absent one as
|
|
208
|
+
an empty history, so a dry run may be pointed at a production
|
|
209
|
+
database and leave its file byte-identical. The API default is to
|
|
210
|
+
run; a CLI SHOULD default to the dry run.
|
|
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.
|
|
176
216
|
- Each pending migration runs in ONE exclusive transaction
|
|
177
217
|
(`BEGIN IMMEDIATE` on SQLite — concurrent writers wait or time out
|
|
178
218
|
under the busy timeout) with a savepoint per step; any failure rolls
|
|
179
219
|
back the whole migration including its earlier steps. Where a driver
|
|
180
220
|
cannot open exclusively, the transaction still isolates; the busy
|
|
181
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.
|
|
182
262
|
- JSLT steps walk the collection in bounded batches
|
|
183
263
|
(`options.batchSize`, default 500) ordered by row identity, report
|
|
184
|
-
progress through `options.onProgress
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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.
|
|
188
276
|
- A transform MUST NOT change a caller-keyed document's key member —
|
|
189
277
|
the key column would go stale; the run refuses (`JD0023`).
|
|
190
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
|
+
|
|
191
320
|
## 7. Non-goals
|
|
192
321
|
|
|
193
322
|
- **Down migrations are not shipped in 0.1.** A JSLT transform is not
|
|
@@ -211,6 +340,7 @@ hash of the `baseline` model when no migration has run.
|
|
|
211
340
|
| `JD0021` | the migration is missing a required data transform |
|
|
212
341
|
| `JD0022` | an applied migration disagrees with the history record |
|
|
213
342
|
| `JD0023` | a migration step failed |
|
|
343
|
+
| `JD0024` | a document source or target could not be read or written |
|
|
214
344
|
|
|
215
345
|
These live in the same runtime `DB_CODES` table as the storage codes
|
|
216
346
|
(MODEL-FORMAT §7); the union of both documents is proven in sync with
|
|
@@ -225,13 +355,13 @@ every row has a shadow-verified test that migrates seeded data:
|
|
|
225
355
|
| Change | Strategy |
|
|
226
356
|
|---|---|
|
|
227
357
|
| 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 |
|
|
358
|
+
| 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
359
|
| change type / enum CHECK / key / epoch flavor | **rebuild** (§10) |
|
|
230
360
|
| 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) |
|
|
361
|
+
| 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 |
|
|
362
|
+
| entity added / dropped | create / `DROP TABLE` (destructive, named), children before parents so no foreign key dangles mid-migration |
|
|
233
363
|
| 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,
|
|
364
|
+
| 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
365
|
|
|
236
366
|
Two rules keep the diff honest:
|
|
237
367
|
|
|
@@ -279,12 +409,13 @@ the transaction** — a broken reference fails the migration rather
|
|
|
279
409
|
than shipping.
|
|
280
410
|
|
|
281
411
|
Two deviations from the cited twelve steps, recorded: (1) the
|
|
282
|
-
procedure
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
412
|
+
procedure's `PRAGMA foreign_keys=OFF/ON` bracket is honoured
|
|
413
|
+
literally, OUTSIDE the transaction (inside one the pragma is a no-op):
|
|
414
|
+
`node:sqlite` enables enforcement by default, and with it on a parent
|
|
415
|
+
table could not even be dropped, so a migration holding a rebuild
|
|
416
|
+
step turns enforcement off before `BEGIN IMMEDIATE` and back on after
|
|
417
|
+
it settles, and `foreign_key_check` inside the transaction provides
|
|
418
|
+
the guarantee the bracket suspended; (2) triggers and views are not re-created because this
|
|
288
419
|
store creates none — a hand-added trigger is outside the model and
|
|
289
420
|
outside the diff, which drift (§12) will name.
|
|
290
421
|
|
|
@@ -307,24 +438,100 @@ drop the index) on a schema the store accepts.
|
|
|
307
438
|
`jaren-db` drives the workflow (mirroring `jaren-emit`):
|
|
308
439
|
|
|
309
440
|
```
|
|
310
|
-
jaren-db plan
|
|
311
|
-
jaren-db
|
|
312
|
-
jaren-db
|
|
313
|
-
jaren-db
|
|
314
|
-
jaren-db
|
|
441
|
+
jaren-db plan --from <model> --to <model> [--store <db>] [--id x] [--out file]
|
|
442
|
+
jaren-db plan --model <model> [--snapshot <file>] [--store <db>] [--id x] --out file
|
|
443
|
+
jaren-db snapshot --model <model> [--snapshot <file>] [--types <file>]
|
|
444
|
+
jaren-db status --model <model> --store <db> --baseline <model> [--migrations <dir>] [--snapshot <file>]
|
|
445
|
+
jaren-db apply --store <db> --baseline <model> --migrations <dir> [--model <m>] [--dry-run] [--yes]
|
|
446
|
+
jaren-db check --model <model> --store <db> --baseline <model> [--migrations <dir>] [--snapshot <file>]
|
|
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>]
|
|
315
451
|
```
|
|
316
452
|
|
|
453
|
+
- **A model or a migration is a `.json` file or a MODULE.** `--model`,
|
|
454
|
+
`--from`, `--to` and `--baseline` accept a `.json` file or a module
|
|
455
|
+
(`.js`, `.mjs`, `.cjs` — and `.ts` where the host strips types: Node
|
|
456
|
+
≥ 24 does by default, and `--no-strip-types` is refused by name)
|
|
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`
|
|
459
|
+
export — the model pen's document, or any object whose `toJSON()`
|
|
460
|
+
emits one; `--migrations <dir>` reads `.json` files and modules
|
|
461
|
+
(`default` or `migration` — the migration pen's builder), sorted by
|
|
462
|
+
file name. A module whose export is not a document, or whose emission
|
|
463
|
+
is not JSON, fails with the module named. **Modules are pure:** the
|
|
464
|
+
CLI loads every module TWICE (two `import()`s under distinct
|
|
465
|
+
cache-busting queries) and refuses one whose two emissions differ —
|
|
466
|
+
no clock, no env, no randomness — because a migration that hashes
|
|
467
|
+
differently per load can never match its own history.
|
|
317
468
|
- `plan` diffs two model FILES (a database stores shape hashes, not
|
|
318
|
-
models — the from-model is the previous model file)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
469
|
+
models — the from-model is the previous model file), or, with
|
|
470
|
+
`--model`, the committed SNAPSHOT against the model: `--snapshot`
|
|
471
|
+
names it and defaults to `model.snapshot.json` beside the model; a
|
|
472
|
+
model whose shape equals the snapshot's plans nothing and exits 0;
|
|
473
|
+
otherwise the migration is written (`--out`) and the snapshot is
|
|
474
|
+
advanced to the model — without `--out` the plan is printed and the
|
|
475
|
+
snapshot stays, and the CLI says so. With `--store` it first compares
|
|
476
|
+
the from-model's physical shape with the database itself — never
|
|
477
|
+
with the history, which would refuse every database that has applied
|
|
478
|
+
a migration.
|
|
479
|
+
- `snapshot` writes the model's snapshot (`--snapshot`, the same
|
|
480
|
+
default) — from the model the store was created with, before the
|
|
481
|
+
first `plan --model`; with `--types <file>` it also writes emit's
|
|
482
|
+
TypeScript declaration for the model (`entityEmitModel` rendered by
|
|
483
|
+
`@jarenjs/emit`, loaded lazily — `@jarenjs/db` does not depend on emit,
|
|
484
|
+
and a host without it is told exactly what `--types` needs), so a
|
|
485
|
+
transform over a JSON snapshot can be typed by annotation. Two runs on
|
|
486
|
+
one input write nothing the second time.
|
|
487
|
+
- `check` is the CI command: exit 1 on an UNPLANNED MODEL CHANGE (a
|
|
488
|
+
snapshot in use whose shape is not the model's — the model moved and
|
|
489
|
+
nobody planned; named as such, never as the database's drift), when
|
|
490
|
+
migrations are pending, OR when the database drifted; 0 in sync.
|
|
491
|
+
`--model` is required — without it drift cannot be measured, and
|
|
492
|
+
`check` refuses rather than print `in sync`. `status` reports the same
|
|
493
|
+
verdict on its `model:` line.
|
|
494
|
+
- `apply` prints every statement, then asks; destructive steps (drop
|
|
495
|
+
table/column, rebuild) print what is lost and ask for that
|
|
496
|
+
separately. `--yes` answers both, `--dry-run` stops after the
|
|
497
|
+
printout. Without an interactive terminal there is nobody to ask, so
|
|
498
|
+
`apply` without `--yes` exits 1 after the printout with nothing
|
|
499
|
+
applied — a CI job passes `--yes` deliberately, never by default.
|
|
500
|
+
`apply --dry-run` is the CLI's printout, not §6's `dryRun: true`: it
|
|
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`.
|
|
504
|
+
- `status` lists applied/pending and reports drift (§12); on a
|
|
505
|
+
database without a history table it creates nothing (§6).
|
|
327
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.
|
|
328
535
|
|
|
329
536
|
## 12. Drift
|
|
330
537
|
|