@substrat-run/kernel 0.148.0 → 0.149.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/attachment-sweep.d.ts +208 -0
  2. package/dist/attachment-sweep.d.ts.map +1 -0
  3. package/dist/attachment-sweep.js +318 -0
  4. package/dist/attachment-sweep.js.map +1 -0
  5. package/dist/index.d.ts +15 -9
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +11 -7
  8. package/dist/index.js.map +1 -1
  9. package/dist/invocation-line.d.ts +85 -0
  10. package/dist/invocation-line.d.ts.map +1 -1
  11. package/dist/invocation-line.js +43 -0
  12. package/dist/invocation-line.js.map +1 -1
  13. package/dist/migration-digest.d.ts +56 -0
  14. package/dist/migration-digest.d.ts.map +1 -1
  15. package/dist/migration-digest.js +92 -0
  16. package/dist/migration-digest.js.map +1 -1
  17. package/dist/migration-fence.d.ts +14 -0
  18. package/dist/migration-fence.d.ts.map +1 -0
  19. package/dist/migration-fence.js +515 -0
  20. package/dist/migration-fence.js.map +1 -0
  21. package/dist/migration-replay.d.ts +52 -0
  22. package/dist/migration-replay.d.ts.map +1 -0
  23. package/dist/migration-replay.js +140 -0
  24. package/dist/migration-replay.js.map +1 -0
  25. package/dist/module-migrations.d.ts +3 -3
  26. package/dist/module-migrations.js +3 -3
  27. package/dist/peer.d.ts +32 -2
  28. package/dist/peer.d.ts.map +1 -1
  29. package/dist/peer.js +99 -3
  30. package/dist/peer.js.map +1 -1
  31. package/dist/permission-eval.d.ts +8 -0
  32. package/dist/permission-eval.d.ts.map +1 -1
  33. package/dist/permission-eval.js +14 -0
  34. package/dist/permission-eval.js.map +1 -1
  35. package/dist/platform-sweep.d.ts +94 -1
  36. package/dist/platform-sweep.d.ts.map +1 -1
  37. package/dist/platform-sweep.js +130 -0
  38. package/dist/platform-sweep.js.map +1 -1
  39. package/dist/promote-review.d.ts +125 -0
  40. package/dist/promote-review.d.ts.map +1 -0
  41. package/dist/promote-review.js +181 -0
  42. package/dist/promote-review.js.map +1 -0
  43. package/dist/scope-host.d.ts +204 -42
  44. package/dist/scope-host.d.ts.map +1 -1
  45. package/dist/scope-host.js.map +1 -1
  46. package/dist/scope-store.d.ts +5 -5
  47. package/dist/scope-store.d.ts.map +1 -1
  48. package/dist/spine-restore.d.ts +2 -0
  49. package/dist/spine-restore.d.ts.map +1 -1
  50. package/dist/spine-restore.js +5 -0
  51. package/dist/spine-restore.js.map +1 -1
  52. package/dist/store-driver-releases.d.ts +306 -0
  53. package/dist/store-driver-releases.d.ts.map +1 -0
  54. package/dist/store-driver-releases.js +581 -0
  55. package/dist/store-driver-releases.js.map +1 -0
  56. package/dist/table-ownership.d.ts +5 -2
  57. package/dist/table-ownership.d.ts.map +1 -1
  58. package/dist/table-ownership.js +19 -10
  59. package/dist/table-ownership.js.map +1 -1
  60. package/dist/testing.d.ts +2 -0
  61. package/dist/testing.d.ts.map +1 -1
  62. package/dist/testing.js +2 -0
  63. package/dist/testing.js.map +1 -1
  64. package/dist/upload-link.d.ts +60 -7
  65. package/dist/upload-link.d.ts.map +1 -1
  66. package/dist/upload-link.js +189 -25
  67. package/dist/upload-link.js.map +1 -1
  68. package/dist/version-migrations.d.ts +63 -1
  69. package/dist/version-migrations.d.ts.map +1 -1
  70. package/dist/version-migrations.js +174 -8
  71. package/dist/version-migrations.js.map +1 -1
  72. package/dist/vertical-scope-stores.d.ts +9 -9
  73. package/dist/vertical-scope-stores.d.ts.map +1 -1
  74. package/dist/vertical-scope-stores.js +29 -29
  75. package/dist/vertical-scope-stores.js.map +1 -1
  76. package/package.json +2 -2
  77. package/dist/store-kind-releases.d.ts +0 -137
  78. package/dist/store-kind-releases.d.ts.map +0 -1
  79. package/dist/store-kind-releases.js +0 -252
  80. package/dist/store-kind-releases.js.map +0 -1
@@ -202,6 +202,98 @@ export async function planMigrations(modules, applied) {
202
202
  }
203
203
  return { pending };
204
204
  }
205
+ /**
206
+ * The platform binding that carries the migration set digest of the version a script serves
207
+ * (#2349). The uploader injects it beside `SUBSTRAT_VERSION_ID`, and only for a version whose
208
+ * migrations the control plane stores: an unheld version gets no binding.
209
+ */
210
+ export const MIGRATIONS_DIGEST_BINDING = 'SUBSTRAT_MIGRATIONS_DIGEST';
211
+ /** How many times this isolate computed a set digest. Read by tests through `@substrat-run/kernel/testing`. */
212
+ let setDigestComputations = 0;
213
+ export const migrationSetDigestComputations = () => setDigestComputations;
214
+ /** How many registered migrations a set divergence names before it says how many more there are. */
215
+ const DIVERGENCE_LISTED = 20;
216
+ /**
217
+ * Whether a migration version is one the kernel derives from a declaration (`search/…`,
218
+ * `state/…`, `list/…`, see `moduleMigrations`) rather than one a module authored. Every derived
219
+ * generator names its versions under one of these prefixes, which `migration-set-digest.test.ts`
220
+ * holds against `moduleMigrations` for a module declaring all three.
221
+ */
222
+ export function isDerivedMigrationVersion(version) {
223
+ return /^(search|state|list)\//.test(version);
224
+ }
225
+ async function setDigestOf(entries) {
226
+ setDigestComputations++;
227
+ // Modules by id, each module's migrations in the order it applies them: the order the CLI
228
+ // stores modules in is the permission entry's, and the order a host registers them is its
229
+ // own, so only the order inside a module is part of the set.
230
+ const sorted = entries
231
+ .map((e, i) => ({ e, i }))
232
+ .sort((a, b) => (a.e.moduleId < b.e.moduleId ? -1 : a.e.moduleId > b.e.moduleId ? 1 : a.i - b.i))
233
+ .map(({ e }) => [e.moduleId, e.version, e.sqlDigest]);
234
+ return migrationDigest(JSON.stringify(sorted));
235
+ }
236
+ /**
237
+ * The digest of a set of AUTHORED migrations (#2349): SHA-256 over each migration's module,
238
+ * version and the SHA-256 of its exact SQL, modules ordered by id. Only the SQL is covered, not
239
+ * what a migration declares it may lose, nor the store classes. The control plane computes it
240
+ * from a version's stored migrations at upload; a scope host computes it from its registered
241
+ * steps (`migrationSetDivergence`).
242
+ *
243
+ * Kernel-derived migrations are left out on both sides. Their SQL is the kernel's spelling of a
244
+ * declaration (#2066 holds an applied one to its version, not its digest), and whether a stored
245
+ * set carries them at all depends on the CLI that pushed it (one that could not resolve the
246
+ * vertical's kernel stores the authored set alone). Both sides decide by version name
247
+ * (`isDerivedMigrationVersion`), since a stored set is only names and SQL, so a stored set from
248
+ * any CLI compares as its authored part. The cost: an AUTHORED migration versioned under one of
249
+ * those prefixes is left out on both sides too, and never compared here. That cannot refuse a
250
+ * correct deployment; the journal still records its digest, and #2066 holds it once applied.
251
+ *
252
+ * Not through `cachedDigest`: the control plane hashes every version it serves, and that cache
253
+ * would keep each one's SQL for the life of the isolate.
254
+ */
255
+ export async function migrationSetDigest(migrations) {
256
+ return setDigestOf(await Promise.all(migrations
257
+ .filter((m) => !isDerivedMigrationVersion(m.version))
258
+ .map(async (m) => ({ moduleId: m.moduleId, version: m.version, sqlDigest: await migrationDigest(m.sql) }))));
259
+ }
260
+ /**
261
+ * An integrity check at apply time (#2349): why a scope host must not apply its pending
262
+ * migrations, or null when it may. `stored` is the set digest the uploader injected for the
263
+ * version this script serves (`MIGRATIONS_DIGEST_BINDING`); the registered side is every
264
+ * AUTHORED step the host's modules register. A kernel-derived step is never compared, so a
265
+ * difference in derived steps alone cannot refuse (see `migrationSetDigest`).
266
+ *
267
+ * Called only when a pass has migrations pending, so a scope with nothing to apply never
268
+ * computes it. Without a stored digest (a version whose migrations the control plane does not
269
+ * hold, a script uploaded before the binding, local dev) nothing is compared.
270
+ *
271
+ * This runs inside the deployed adapter, so it guards against accidental divergence — a build
272
+ * whose worker registers other migrations than the push declared — not as an isolation boundary.
273
+ */
274
+ export async function migrationSetDivergence(modules, stored) {
275
+ if (!stored)
276
+ return null;
277
+ const entries = [];
278
+ for (const mod of modules) {
279
+ for (const step of await mod.steps) {
280
+ if (!step.authored || isDerivedMigrationVersion(step.migration.version))
281
+ continue;
282
+ entries.push({ moduleId: mod.id, version: step.migration.version, sqlDigest: step.digest });
283
+ }
284
+ }
285
+ const registered = await setDigestOf(entries);
286
+ if (registered === stored)
287
+ return null;
288
+ const keys = entries.map((e) => `${e.moduleId}@${e.version}`);
289
+ const listed = keys.length === 0
290
+ ? 'none'
291
+ : keys.slice(0, DIVERGENCE_LISTED).join(', ') +
292
+ (keys.length > DIVERGENCE_LISTED ? ` and ${keys.length - DIVERGENCE_LISTED} more` : '');
293
+ return (`the authored migrations this deployment registers are not the ones stored for the version it serves ` +
294
+ `(registered set sha256 ${registered}, stored ${stored}); registered: ${listed}. Nothing was applied. ` +
295
+ `Push the version again from the build that is deployed, so the stored migrations are the ones it runs (#2349)`);
296
+ }
205
297
  /** The error a scope fails closed with when `module@version` cannot be applied or trusted. */
206
298
  export function migrationFailedError(key, cause) {
207
299
  return new Error(`migration failed for ${key} — scope fails closed: ${cause}`);
@@ -1 +1 @@
1
- {"version":3,"file":"migration-digest.js","sourceRoot":"","sources":["../src/migration-digest.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"migration-digest.js","sourceRoot":"","sources":["../src/migration-digest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { ScopedSql, SqlMigration } from './scope-host.js';
2
+ /**
3
+ * The tables the fence measures, as SQLite names them: the scope's own, without the spine,
4
+ * SQLite's and workerd's internals, virtual tables (an index over data held elsewhere, or a
5
+ * module's own store the fence cannot count without its content table) and their shadow tables.
6
+ * Exported so a replay outside a host (`lint:migration-fence`) populates exactly what is measured.
7
+ */
8
+ export declare function fencedTables(sql: ScopedSql): string[];
9
+ /**
10
+ * Wrap one authored migration's execution in the fence. `run` executes every statement of it; it
11
+ * runs inside the caller's transaction, so a throw from here rolls all of it back.
12
+ */
13
+ export declare function fenceMigration(sql: ScopedSql, migration: SqlMigration, key: string, run: () => void): void;
14
+ //# sourceMappingURL=migration-fence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-fence.d.ts","sourceRoot":"","sources":["../src/migration-fence.ts"],"names":[],"mappings":"AAoEA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AA4E/D;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,EAAE,CAerD;AA8TD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,IAAI,GAAG,IAAI,CA4E1G"}
@@ -0,0 +1,515 @@
1
+ /**
2
+ * The data-loss fence: an authored migration may not lose data a scope holds unless it says so (#115).
3
+ *
4
+ * The planner (`@substrat-run/model-emit`) refuses every destructive diff when it writes a
5
+ * migration, but nothing judged what an AUTHORED migration does to the rows already there: a
6
+ * hand-written `DROP TABLE`, a `DROP COLUMN`, a `DELETE`, or a create-copy-rename rebuild that
7
+ * forgot a column ran unchecked against a scope full of data. This is the check, and it reads
8
+ * what the SQL DID rather than what its text says, so there is no SQL grammar to get wrong.
9
+ *
10
+ * **Measured, not parsed.** Parsing decides only whether to measure: a migration made only of
11
+ * provably additive statements (`CREATE …`, `ALTER TABLE … ADD …`) cannot lose anything and is not
12
+ * measured. Any other statement (any DML, `DROP`, `ALTER … RENAME`/`DROP COLUMN`, `PRAGMA`, `WITH`,
13
+ * a shape nobody listed) measures EVERY module table, not just the ones it names, so a trigger or a
14
+ * foreign-key cascade that reaches another table is seen too. Measured, per table: the rows, and
15
+ * per column the values in each storage class (`typeof`). Once before the first statement and
16
+ * once after the last, inside the migration's transaction, so the intermediate states of a rebuild
17
+ * do not matter and a refusal rolls the whole migration back. Every measuring query stays inside a
18
+ * Durable Object's own limits (`DO_SQL_LIMITS`), or the fence would close a scope for a reason of
19
+ * its own.
20
+ *
21
+ * **Production data is rows that exist.** A table with no rows may be dropped or rebuilt freely,
22
+ * whatever else is true of the scope; the same migration is refused in a scope where it would
23
+ * lose something. There is no phase flag to forget to flip.
24
+ *
25
+ * **What counts as a loss**, for a table that held rows: it is gone (under the name `ALTER TABLE …
26
+ * RENAME TO` last gave it, or under its original name, so a rebuild back into place keeps it —
27
+ * each table after the migration is claimed by one table before it, a rename first, and a `DROP
28
+ * TABLE` releases the name it removed); it has fewer
29
+ * rows; one of its columns that held values is gone (`RENAME COLUMN` is followed) or holds fewer
30
+ * of them in some storage class — which is how a retype shows, `'1.50'` turning into `1`. Counts
31
+ * cannot tell a faithful widening (`5` → `5.0`) from a rewrite (`5` → `0.05`), so any change of
32
+ * storage class is reported, and one made on purpose is declared with `discards`. `NOT NULL`
33
+ * without a default needs no rule: SQLite refuses it itself.
34
+ *
35
+ * **What it does not see**, because it counts — the residual classes, each named in the property
36
+ * test (`migration-fence.property.test.ts`) that holds the rest to ground truth:
37
+ * - in-place replacement: values rewritten at the same count and class (`UPDATE`), rows replaced
38
+ * by as many other rows (`DELETE` + `INSERT`), or a column dropped, re-added and refilled;
39
+ * - a dropped table whose name is continued by a table the migration created, filled from
40
+ * somewhere else, holding at least as many rows, even renamed away again (`DROP TABLE a; CREATE
41
+ * TABLE a AS SELECT * FROM b`, or `…; CREATE TABLE n AS SELECT * FROM b; ALTER TABLE n RENAME TO
42
+ * a; ALTER TABLE a RENAME TO d`): counts cannot tell it from the create-copy-drop-rename
43
+ * rebuild, or a rename after it, which the fence must accept;
44
+ * - which source a move's target gained from: a target is credited with everything it gained.
45
+ * And one refusal by design: data copied under another table's name is refused until `moves`
46
+ * declares it.
47
+ *
48
+ * **Declared losses pass.** `SqlMigration.moves` names data that went elsewhere, and is verified:
49
+ * the target must GAIN at least what its sources held, per class — and for a table moved whole,
50
+ * every source column that held values must arrive in a target column of the same name, unless
51
+ * that column is moved or discarded by name itself. `SqlMigration.discards` names data
52
+ * deliberately given up, with the reason a reviewer reads. A declaration naming a table or column
53
+ * the scope does not hold before the migration (a move's target excepted, which the migration may
54
+ * create) is an authoring error and refuses the migration too.
55
+ *
56
+ * **TEMP objects are refused.** A Durable Object refuses them outright, so a migration making one
57
+ * could never run on the hosted adapter, and a TEMP table shadows the scope's own tables for every
58
+ * unqualified statement after it. An authored migration that makes one is refused before it runs,
59
+ * on both adapters, and every measuring read names `main` so nothing else can answer for a table.
60
+ *
61
+ * **ATTACH and DETACH are refused** for the same first reason (#2321): a Durable Object refuses
62
+ * them, so the migration could never run hosted. And an attached database is a FILE, which the
63
+ * fence does not measure and a replay outside a host (`substrat push`) would otherwise create on
64
+ * the machine running it. Refused before anything runs, on both adapters.
65
+ *
66
+ * Kernel-derived DDL is exempt: its tables are indexes, rebuilt from the data this protects.
67
+ */
68
+ import { blankReason, namesSpineTable, substratError, tokenizeSql } from '@substrat-run/contracts';
69
+ import { DO_SQL_LIMITS } from './sql-limits.js';
70
+ import { executableSqlStatements } from './sql-statements.js';
71
+ /** The storage classes a value can have; NULL is not a value. */
72
+ const CLASSES = ['integer', 'real', 'text', 'blob'];
73
+ /**
74
+ * Columns measured per query: each one is a result column per storage class, and a Durable
75
+ * Object refuses a result set wider than `DO_SQL_LIMITS.columns`.
76
+ */
77
+ const COLUMNS_PER_SCAN = Math.floor(DO_SQL_LIMITS.columns / CLASSES.length);
78
+ /** SQLite folds ASCII letters only: `"ä"` and `"Ä"` are two tables, and must stay two keys. */
79
+ const lower = (s) => s.replace(/[A-Z]+/g, (m) => m.toLowerCase());
80
+ const quote = (name) => `"${name.replace(/"/g, '""')}"`;
81
+ const total = (c) => CLASSES.reduce((n, k) => n + c[k], 0);
82
+ const NONE = { integer: 0, real: 0, text: 0, blob: 0 };
83
+ /** The words of one statement, keywords lowercased; quoted names are kept apart from keywords. */
84
+ function words(statement) {
85
+ return tokenizeSql(statement).map((t) => ({ text: t.quoted ? t.text : lower(t.text), quoted: t.quoted, parts: t.parts }));
86
+ }
87
+ /**
88
+ * The schema a name token names, and the name in it, as SQLite reads them: `main.n` and
89
+ * `"main"."n"` are `main` and `n`; `"main.n"` is ONE identifier, a name with a dot in it and no
90
+ * schema. The one reading of a qualifier, for the `temp` check and for following `main` names.
91
+ */
92
+ function schemaAndName(w) {
93
+ const parts = w.parts ?? [w.text];
94
+ return parts.length === 2 ? { schema: lower(parts[0]), name: parts[1] } : { name: parts.join('.') };
95
+ }
96
+ /** A table name as the main schema holds it, or a name no main table has if another schema is named. */
97
+ function mainName(w) {
98
+ const { schema, name } = schemaAndName(w);
99
+ return schema === undefined || schema === 'main' ? lower(name) : `\u0000${schema}.${lower(name)}`;
100
+ }
101
+ /** A statement that can only define something: it fires nothing and removes nothing. */
102
+ function additive(statement) {
103
+ const w = words(statement);
104
+ const kw = (i) => (w[i] && !w[i].quoted ? w[i].text : undefined);
105
+ if (kw(0) === 'create')
106
+ return true;
107
+ // `ALTER TABLE <name> ADD [COLUMN] …` — the name is one token, dotted or not.
108
+ return kw(0) === 'alter' && kw(1) === 'table' && kw(3) === 'add';
109
+ }
110
+ /**
111
+ * The tables a virtual-table module keeps behind each virtual table, by suffix. Only these exact
112
+ * names are left out: a real table that merely starts with a virtual table's name is measured. A
113
+ * module not listed here has its shadow tables measured too, which can refuse a migration that
114
+ * only reshaped an index — the conservative answer.
115
+ */
116
+ const SHADOW_SUFFIXES = {
117
+ fts5: ['data', 'idx', 'content', 'docsize', 'config'],
118
+ fts4: ['content', 'segments', 'segdir', 'docsize', 'stat'],
119
+ fts3: ['content', 'segments', 'segdir', 'docsize', 'stat'],
120
+ rtree: ['node', 'parent', 'rowid'],
121
+ };
122
+ /**
123
+ * The tables the fence measures, as SQLite names them: the scope's own, without the spine,
124
+ * SQLite's and workerd's internals, virtual tables (an index over data held elsewhere, or a
125
+ * module's own store the fence cannot count without its content table) and their shadow tables.
126
+ * Exported so a replay outside a host (`lint:migration-fence`) populates exactly what is measured.
127
+ */
128
+ export function fencedTables(sql) {
129
+ const rows = sql.query("SELECT name, sql FROM main.sqlite_master WHERE type = 'table'");
130
+ const own = rows.filter((r) => {
131
+ const n = lower(r.name);
132
+ return !namesSpineTable(n) && !n.startsWith('sqlite_') && !n.startsWith('_cf_');
133
+ });
134
+ const skip = new Set();
135
+ for (const r of own) {
136
+ const m = /^\s*create\s+virtual\s+table\s+.*?\busing\s+(\w+)/is.exec(r.sql ?? '');
137
+ if (!m)
138
+ continue;
139
+ const name = lower(r.name);
140
+ skip.add(name);
141
+ for (const suffix of SHADOW_SUFFIXES[lower(m[1])] ?? [])
142
+ skip.add(`${name}_${suffix}`);
143
+ }
144
+ return own.map((r) => r.name).filter((name) => !skip.has(lower(name)));
145
+ }
146
+ function measure(sql) {
147
+ const out = new Map();
148
+ for (const table of fencedTables(sql)) {
149
+ // Qualified: a TEMP object of the same name would otherwise answer for the scope's table.
150
+ const names = sql.query("SELECT name FROM pragma_table_info(?, 'main')", [table]).map((c) => c.name);
151
+ const rows = Number(sql.query(`SELECT COUNT(*) AS n FROM main.${quote(table)}`)[0].n);
152
+ const columns = new Map();
153
+ for (let at = 0; at < names.length && rows > 0; at += COLUMNS_PER_SCAN) {
154
+ const chunk = names.slice(at, at + COLUMNS_PER_SCAN);
155
+ const select = chunk.flatMap((c, i) => CLASSES.map((k) => `COALESCE(SUM(typeof(${quote(c)}) = '${k}'), 0) AS c${i}_${k}`));
156
+ const r = sql.query(`SELECT ${select.join(', ')} FROM main.${quote(table)}`)[0];
157
+ chunk.forEach((c, i) => {
158
+ columns.set(lower(c), Object.fromEntries(CLASSES.map((k) => [k, Number(r[`c${i}_${k}`])])));
159
+ });
160
+ }
161
+ if (rows === 0)
162
+ for (const c of names)
163
+ columns.set(lower(c), NONE);
164
+ out.set(lower(table), { rows, columns });
165
+ }
166
+ return out;
167
+ }
168
+ function targetOf(spec) {
169
+ const parts = spec.trim().split('.');
170
+ if (parts.length > 2 || parts.some((p) => p === ''))
171
+ return undefined;
172
+ return parts.length === 1 ? { table: lower(parts[0]) } : { table: lower(parts[0]), column: lower(parts[1]) };
173
+ }
174
+ const show = (t) => (t.column ? `${t.table}.${t.column}` : t.table);
175
+ /** What a column a `DROP COLUMN` removed maps to: a name no column after the migration has. */
176
+ const DROPPED = '\u0000dropped';
177
+ /**
178
+ * Statement by statement, which original table holds each name right now. A name is held by at
179
+ * most one table — SQLite refuses a rename onto a name that exists — so a `DROP TABLE` releases it
180
+ * and a later rename onto it is that table's alone, whatever order the scope created them in.
181
+ *
182
+ * A table this migration made, renamed onto a dropped table's ORIGINAL name, continues the dropped
183
+ * one: that is the create-copy-drop-rename rebuild, which the counts then judge, and its later
184
+ * renames, of the table or its columns, are followed. A bare `CREATE TABLE` at that name is not a
185
+ * continuation: renames after it are not followed, which can only refuse.
186
+ */
187
+ function followRenames(statements, before) {
188
+ const tables = new Map([...before.keys()].map((t) => [t, [t]]));
189
+ const columns = new Map([...before].map(([t, c]) => [t, new Map([...c.columns.keys()].map((k) => [k, k]))]));
190
+ const holder = new Map([...before.keys()].map((t) => [t, t]));
191
+ const dropped = new Set();
192
+ const rebuilt = new Set();
193
+ const rebuiltAt = (name) => {
194
+ if (!dropped.has(name) || holder.has(name))
195
+ return;
196
+ dropped.delete(name);
197
+ rebuilt.add(name);
198
+ holder.set(name, name);
199
+ tables.get(name).push(name);
200
+ };
201
+ for (const statement of statements) {
202
+ const w = words(statement);
203
+ const kw = (i) => (w[i] && !w[i].quoted ? w[i].text : undefined);
204
+ if (kw(0) === 'drop' && kw(1) === 'table') {
205
+ const at = kw(2) === 'if' && kw(3) === 'exists' ? 4 : 2;
206
+ if (!w[at])
207
+ continue;
208
+ const name = mainName(w[at]);
209
+ const original = holder.get(name);
210
+ if (original === undefined)
211
+ continue;
212
+ holder.delete(name);
213
+ dropped.add(original);
214
+ continue;
215
+ }
216
+ if (kw(0) !== 'alter' || kw(1) !== 'table' || !w[2])
217
+ continue;
218
+ const table = mainName(w[2]);
219
+ const original = holder.get(table);
220
+ if (!original) {
221
+ // A table this migration made, renamed onto a dropped table's original name: the rebuild.
222
+ if (kw(3) === 'rename' && kw(4) === 'to' && w[5])
223
+ rebuiltAt(mainName(w[5]));
224
+ continue;
225
+ }
226
+ const renamed = columns.get(original);
227
+ if (kw(3) === 'drop') {
228
+ // DROP [COLUMN] c: the column is gone, and its name is free for a later rename.
229
+ const k = kw(4) === 'column' ? 5 : 4;
230
+ if (!w[k])
231
+ continue;
232
+ const name = lower(w[k].text);
233
+ for (const [orig, current] of renamed)
234
+ if (current === name)
235
+ renamed.set(orig, DROPPED);
236
+ continue;
237
+ }
238
+ if (kw(3) !== 'rename')
239
+ continue;
240
+ if (kw(4) === 'to' && w[5]) {
241
+ holder.delete(table);
242
+ holder.set(mainName(w[5]), original);
243
+ tables.get(original).push(mainName(w[5]));
244
+ }
245
+ else {
246
+ // RENAME [COLUMN] a TO b
247
+ const k = kw(4) === 'column' ? 5 : 4;
248
+ if (!w[k] || kw(k + 1) !== 'to' || !w[k + 2])
249
+ continue;
250
+ const from = lower(w[k].text);
251
+ const to = lower(w[k + 2].text);
252
+ for (const [orig, current] of renamed)
253
+ if (current === from)
254
+ renamed.set(orig, to);
255
+ }
256
+ }
257
+ return { tables, columns, dropped, rebuilt };
258
+ }
259
+ /**
260
+ * Which table after the migration each table before it became, one to one. A table renamed onto a
261
+ * name that exists afterwards, and not dropped since, claims it first; every other table falls
262
+ * back to its original name, then (unless it was dropped) to an earlier name it carried — never
263
+ * to a name another table already claimed. So
264
+ * `DROP TABLE b; ALTER TABLE a RENAME TO b` leaves `b` to `a`, and the `b` that was dropped is
265
+ * gone. Undefined: the table is gone.
266
+ */
267
+ function endsOf(before, after, renames) {
268
+ const ends = new Map();
269
+ const claimed = new Set();
270
+ for (const [table, names] of renames.tables) {
271
+ const last = names[names.length - 1];
272
+ // `holder` gave each name to one table at a time, so two undropped tables cannot end on one.
273
+ if (names.length > 1 && !renames.dropped.has(table) && after.has(last)) {
274
+ // A rebuilt table may carry a column under its original name, so it is not followed strictly.
275
+ ends.set(table, { end: last, followed: !renames.rebuilt.has(table) });
276
+ claimed.add(last);
277
+ }
278
+ }
279
+ for (const [table, names] of renames.tables) {
280
+ if (ends.has(table) || !before.has(table))
281
+ continue;
282
+ // A dropped table may come back only under its original name — a rebuild back into place. A
283
+ // name it carried before the drop now belongs to whatever was created there afterwards.
284
+ const candidates = renames.dropped.has(table) ? [table] : [table, ...[...names].reverse()];
285
+ const end = candidates.find((n) => after.has(n) && !claimed.has(n));
286
+ if (end !== undefined)
287
+ claimed.add(end);
288
+ // Followed strictly only when the table never moved: a table rebuilt back into place (moved
289
+ // aside, or dropped and recreated) may carry a column under its original name.
290
+ const followed = names.length === 1 && !renames.dropped.has(table);
291
+ ends.set(table, end === undefined ? undefined : { end, followed });
292
+ }
293
+ return ends;
294
+ }
295
+ /**
296
+ * Compare the two measures: every table that held rows, and every column that held values, must
297
+ * still hold at least as much — following renames — unless it is moved or discarded.
298
+ */
299
+ function lossesOf(before, after, renames) {
300
+ const losses = [];
301
+ const ends = endsOf(before, after, renames);
302
+ for (const [table, b] of before) {
303
+ if (b.rows === 0)
304
+ continue;
305
+ const resolved = ends.get(table);
306
+ if (resolved === undefined) {
307
+ losses.push({ target: { table }, what: `table ${table} held ${b.rows} row(s) and is gone` });
308
+ continue;
309
+ }
310
+ const a = after.get(resolved.end);
311
+ if (a.rows < b.rows)
312
+ losses.push({ target: { table }, what: `table ${table} held ${b.rows} row(s), now ${a.rows}` });
313
+ const renamed = renames.columns.get(table);
314
+ for (const [column, held] of b.columns) {
315
+ if (total(held) === 0)
316
+ continue;
317
+ // Followed through its renames; on a table rebuilt back into place, the rename the old table
318
+ // took before it moved aside still names the column, and its original name is the fallback.
319
+ const mapped = renamed.get(column);
320
+ // A column a `DROP COLUMN` removed is gone, whatever later takes its name.
321
+ const kept = mapped === DROPPED ? undefined : (a.columns.get(mapped) ?? (resolved.followed ? undefined : a.columns.get(column)));
322
+ // A kernel-derived column (`_substrat_archived_at`, …) leaving is the kernel's own check's to
323
+ // refuse, right after this one in the same transaction (#2090), with the remedy that fits:
324
+ // no declaration can excuse it. Its VALUES are still held here.
325
+ if (kept === undefined && namesSpineTable(column))
326
+ continue;
327
+ const target = { table, column };
328
+ if (kept === undefined) {
329
+ losses.push({ target, what: `column ${table}.${column} held ${total(held)} value(s) and is gone` });
330
+ continue;
331
+ }
332
+ const short = CLASSES.filter((k) => kept[k] < held[k]);
333
+ if (short.length > 0) {
334
+ losses.push({
335
+ target,
336
+ what: `column ${table}.${column} held ${total(held)} value(s), now ${total(kept)}` +
337
+ (total(kept) >= total(held) ? ` (${short.map((k) => `${k} ${held[k]} → ${kept[k]}`).join(', ')}: retyped)` : ''),
338
+ });
339
+ }
340
+ }
341
+ }
342
+ return losses;
343
+ }
344
+ const declarationError = (key, message, reason = 'migration_declaration') => substratError('validation_failed', `migration ${key}: ${message} (#115)`, { reason });
345
+ /**
346
+ * A statement that makes a TEMP object: `CREATE TEMP|TEMPORARY …`, or one named into the `temp`
347
+ * schema (`CREATE TABLE temp.x`, `"temp"."x"`), however the name is spelled.
348
+ */
349
+ function createsTemp(statement) {
350
+ const w = words(statement);
351
+ const kw = (i) => (w[i] && !w[i].quoted ? w[i].text : undefined);
352
+ if (kw(0) !== 'create')
353
+ return false;
354
+ if (kw(1) === 'temp' || kw(1) === 'temporary')
355
+ return true;
356
+ let at = 1;
357
+ if (kw(at) === 'unique' || kw(at) === 'virtual')
358
+ at += 1;
359
+ if (!['table', 'view', 'index', 'trigger'].includes(kw(at) ?? ''))
360
+ return false;
361
+ at += 1;
362
+ if (kw(at) === 'if' && kw(at + 1) === 'not' && kw(at + 2) === 'exists')
363
+ at += 3;
364
+ // `"temp.x"` is one identifier, a main table whose name holds a dot; `temp.x` is the schema.
365
+ return w[at] !== undefined && schemaAndName(w[at]).schema === 'temp';
366
+ }
367
+ /** A statement that attaches or detaches a database: `ATTACH [DATABASE] …`, `DETACH [DATABASE] …`. */
368
+ function attaches(statement) {
369
+ const w = words(statement);
370
+ return w[0] !== undefined && !w[0].quoted && (w[0].text === 'attach' || w[0].text === 'detach');
371
+ }
372
+ /**
373
+ * What a declaration counts: a column's values per class, or a table's rows — carried in one
374
+ * class slot so a move compares tables and columns with the same loop.
375
+ */
376
+ function countOf(m, t) {
377
+ const table = m.get(t.table);
378
+ if (!table)
379
+ return undefined;
380
+ if (!t.column)
381
+ return { ...NONE, integer: table.rows };
382
+ return table.columns.get(t.column);
383
+ }
384
+ const covers = (declared, lost) => declared.table === lost.table && (declared.column === undefined || declared.column === lost.column);
385
+ /** Each class `got` falls short of `need`, as `<class> <need> → <got>`. */
386
+ const shortfall = (need, got) => CLASSES.filter((k) => got[k] < need[k]).map((k) => `${k} ${need[k]} → ${got[k]}`);
387
+ const plus = (a, b) => Object.fromEntries(CLASSES.map((k) => [k, a[k] + b[k]]));
388
+ const gain = (before, after) => Object.fromEntries(CLASSES.map((k) => [k, (after?.[k] ?? 0) - (before?.[k] ?? 0)]));
389
+ /**
390
+ * Verify the declared moves: each target gained, per class, what its sources held; and a table
391
+ * moved whole brought every column that held values into a target column of the same name, unless
392
+ * that column is declared by name itself.
393
+ */
394
+ function moveFailures(before, after, moves, discards) {
395
+ const failures = [];
396
+ const byTarget = new Map();
397
+ for (const m of moves) {
398
+ const entry = byTarget.get(show(m.target)) ?? { target: m.target, sources: [] };
399
+ entry.sources.push(m.source);
400
+ byTarget.set(show(m.target), entry);
401
+ }
402
+ for (const { target, sources } of byTarget.values()) {
403
+ if (!countOf(after, target)) {
404
+ failures.push(`moves names ${show(target)} as a target, which does not exist after the migration`);
405
+ continue;
406
+ }
407
+ const need = sources.reduce((n, s) => plus(n, countOf(before, s)), NONE);
408
+ const gained = gain(countOf(before, target), countOf(after, target));
409
+ const short = shortfall(need, gained);
410
+ if (short.length > 0) {
411
+ const from = `moves into ${show(target)} from ${sources.map(show).join(', ')}`;
412
+ failures.push(target.column
413
+ ? `${from}: the sources' values did not all arrive (${short.join(', ')})`
414
+ : `${from}: the sources held ${need.integer} row(s), the target gained ${gained.integer}`);
415
+ }
416
+ if (target.column)
417
+ continue;
418
+ // A table moved whole: its columns arrive by name, unless declared by name.
419
+ const byColumn = new Map();
420
+ for (const source of sources) {
421
+ for (const [column, held] of before.get(source.table).columns) {
422
+ const named = { table: source.table, column };
423
+ if (total(held) === 0 || namesSpineTable(column))
424
+ continue;
425
+ if (moves.some((m) => m.source.column === column && m.source.table === source.table))
426
+ continue;
427
+ if (discards.some((d) => covers(d, named)))
428
+ continue;
429
+ const entry = byColumn.get(column) ?? { need: NONE, from: [] };
430
+ byColumn.set(column, { need: plus(entry.need, held), from: [...entry.from, show(named)] });
431
+ }
432
+ }
433
+ for (const [column, { need: held, from }] of byColumn) {
434
+ const into = { table: target.table, column };
435
+ const now = countOf(after, into);
436
+ const missing = now === undefined ? [] : shortfall(held, gain(countOf(before, into), now));
437
+ if (now === undefined || missing.length > 0) {
438
+ failures.push(`moves ${from.join(', ')} into ${target.table}, but ${now === undefined ? `${show(into)} does not exist` : `${show(into)} did not gain them (${missing.join(', ')})`} — ` +
439
+ `move the column by name (moves: { '${from[0]}': '<table.column>' }) or discard it`);
440
+ }
441
+ }
442
+ }
443
+ return failures;
444
+ }
445
+ /**
446
+ * Wrap one authored migration's execution in the fence. `run` executes every statement of it; it
447
+ * runs inside the caller's transaction, so a throw from here rolls all of it back.
448
+ */
449
+ export function fenceMigration(sql, migration, key, run) {
450
+ const statements = executableSqlStatements(migration.sql);
451
+ const temp = statements.find(createsTemp);
452
+ if (temp !== undefined) {
453
+ throw declarationError(key, `it creates a TEMP object (\`${temp.slice(0, 80)}\`). A Durable Object refuses TEMP objects, so this migration ` +
454
+ 'cannot run on the hosted adapter, and a TEMP table shadows the scope\'s own tables for every unqualified statement ' +
455
+ 'after it. Use a regular table and DROP it before the migration ends', 'migration_temp');
456
+ }
457
+ const attach = statements.find(attaches);
458
+ if (attach !== undefined) {
459
+ throw declarationError(key, `it attaches or detaches a database (\`${attach.slice(0, 80)}\`). A Durable Object refuses ATTACH and DETACH, so this ` +
460
+ 'migration cannot run on the hosted adapter, and an attached database is a file outside the scope the fence protects. ' +
461
+ "Keep the migration inside the scope's own database", 'migration_attach');
462
+ }
463
+ const moves = Object.entries(migration.moves ?? {});
464
+ const discards = Object.entries(migration.discards ?? {});
465
+ // The push boundary refuses a `__proto__` declaration key, which a record parsed from JSON would
466
+ // silently drop (#2322); refusing it here too means the author hears it at lint time, not when
467
+ // the push leaves the migrations off.
468
+ for (const [what, record] of [['moves', migration.moves], ['discards', migration.discards]]) {
469
+ if (record && Object.hasOwn(record, '__proto__'))
470
+ throw declarationError(key, `${what} names __proto__, which a declaration cannot carry`);
471
+ }
472
+ if (moves.length === 0 && discards.length === 0 && statements.every(additive)) {
473
+ run();
474
+ return;
475
+ }
476
+ const before = measure(sql);
477
+ // Every declaration is read before anything runs, so a malformed one changes nothing.
478
+ const parsedMoves = moves.map(([from, to]) => {
479
+ const source = targetOf(from);
480
+ const target = targetOf(to);
481
+ if (!source || !target)
482
+ throw declarationError(key, `moves: { '${from}': '${to}' } is not 'table' → 'table' or 'table.column' → 'table.column'`);
483
+ if (!source.column !== !target.column)
484
+ throw declarationError(key, `moves: { '${from}': '${to}' } moves a table to a column or a column to a table`);
485
+ if (!countOf(before, source))
486
+ throw declarationError(key, `moves names ${show(source)}, which this scope does not hold before the migration`);
487
+ return { source, target };
488
+ });
489
+ const parsedDiscards = discards.map(([spec, why]) => {
490
+ const target = targetOf(spec);
491
+ if (!target)
492
+ throw declarationError(key, `discards: { '${spec}': … } is not 'table' or 'table.column'`);
493
+ // The push boundary's own rule (`blankReason`): a reason of only whitespace or invisible
494
+ // characters says nothing a reviewer can read, so neither side accepts it (#2322).
495
+ if (typeof why !== 'string' || blankReason(why))
496
+ throw declarationError(key, `discards: { '${spec}': … } gives no reason; the reason is what a reviewer reads`);
497
+ if (!countOf(before, target))
498
+ throw declarationError(key, `discards names ${show(target)}, which this scope does not hold before the migration`);
499
+ return target;
500
+ });
501
+ run();
502
+ const after = measure(sql);
503
+ const failures = moveFailures(before, after, parsedMoves, parsedDiscards);
504
+ const unexcused = lossesOf(before, after, followRenames(statements, before)).filter((l) => !parsedDiscards.some((d) => covers(d, l.target)) && !parsedMoves.some((m) => covers(m.source, l.target)));
505
+ if (failures.length === 0 && unexcused.length === 0)
506
+ return;
507
+ const lines = [...failures, ...unexcused.map((l) => l.what)];
508
+ const remedy = unexcused.length === 0
509
+ ? ''
510
+ : ` If this is intended, declare it on the migration — discards: { ${unexcused
511
+ .map((l) => `'${show(l.target)}': '<why this data may go>'`)
512
+ .join(', ')} } — or, where the data moved, moves: { '<source>': '<where it went>' }.`;
513
+ throw substratError('forbidden', `migration ${key} would lose data this scope holds, so it was not applied (#115): ${lines.join('; ')}.${remedy}`, { reason: 'data_loss' });
514
+ }
515
+ //# sourceMappingURL=migration-fence.js.map