@prisma-next/target-sqlite 0.14.0-dev.49 → 0.14.0-dev.50

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 (32) hide show
  1. package/dist/control.mjs +6 -18
  2. package/dist/control.mjs.map +1 -1
  3. package/dist/migration.mjs +2 -2
  4. package/dist/native-type-normalizer.mjs +13 -1
  5. package/dist/native-type-normalizer.mjs.map +1 -0
  6. package/dist/{op-factory-call-CMBTpb4M.mjs → op-factory-call-D_eYsHTp.mjs} +82 -65
  7. package/dist/op-factory-call-D_eYsHTp.mjs.map +1 -0
  8. package/dist/op-factory-call.mjs +1 -1
  9. package/dist/planner-DhvFrgPT.mjs +854 -0
  10. package/dist/planner-DhvFrgPT.mjs.map +1 -0
  11. package/dist/{planner-produced-sqlite-migration-BSjMKJhI.mjs → planner-produced-sqlite-migration-DyRmcteq.mjs} +2 -2
  12. package/dist/{planner-produced-sqlite-migration-BSjMKJhI.mjs.map → planner-produced-sqlite-migration-DyRmcteq.mjs.map} +1 -1
  13. package/dist/planner-produced-sqlite-migration.mjs +1 -1
  14. package/dist/planner.d.mts +46 -10
  15. package/dist/planner.d.mts.map +1 -1
  16. package/dist/planner.mjs +1 -1
  17. package/dist/{sqlite-migration-BdT_3SlM.mjs → sqlite-migration-D93wCdSD.mjs} +2 -2
  18. package/dist/{sqlite-migration-BdT_3SlM.mjs.map → sqlite-migration-D93wCdSD.mjs.map} +1 -1
  19. package/package.json +18 -18
  20. package/src/core/control-target.ts +9 -22
  21. package/src/core/migrations/column-ddl-rendering.ts +177 -0
  22. package/src/core/migrations/diff-database-schema.ts +197 -31
  23. package/src/core/migrations/issue-planner.ts +359 -488
  24. package/src/core/migrations/operations/tables.ts +107 -67
  25. package/src/core/migrations/planner-strategies.ts +118 -137
  26. package/src/core/migrations/planner.ts +112 -37
  27. package/src/core/sqlite-schema-verifier.ts +6 -3
  28. package/dist/native-type-normalizer-CiSyVmMP.mjs +0 -14
  29. package/dist/native-type-normalizer-CiSyVmMP.mjs.map +0 -1
  30. package/dist/op-factory-call-CMBTpb4M.mjs.map +0 -1
  31. package/dist/planner-EoQcJGzv.mjs +0 -676
  32. package/dist/planner-EoQcJGzv.mjs.map +0 -1
@@ -1,676 +0,0 @@
1
- import { t as parseSqliteDefault } from "./default-normalizer-DuoHj9-O.mjs";
2
- import { t as normalizeSqliteNativeType } from "./native-type-normalizer-CiSyVmMP.mjs";
3
- import { _ as resolveColumnTypeMetadata, a as DropColumnCall, d as buildRecreateSummary, g as renderDefaultLiteral, h as isInlineAutoincrementPrimaryKey, i as DataTransformCall, l as RecreateTableCall, m as buildColumnTypeSql, n as CreateIndexCall, o as DropIndexCall, p as buildColumnDefaultSql, r as CreateTableCall, s as DropTableCall, t as AddColumnCall, u as buildRecreatePostchecks } from "./op-factory-call-CMBTpb4M.mjs";
4
- import { t as CONTROL_TABLE_NAMES } from "./control-tables-7KwMyJ6i.mjs";
5
- import { t as TypeScriptRenderableSqliteMigration } from "./planner-produced-sqlite-migration-BSjMKJhI.mjs";
6
- import { DdlColumn, ForeignKeyConstraint, FunctionColumnDefault, LiteralColumnDefault, PrimaryKeyConstraint, UniqueConstraint } from "@prisma-next/sql-relational-core/ast";
7
- import { UNBOUND_NAMESPACE_ID } from "@prisma-next/framework-components/ir";
8
- import { contractToSchemaIR, extractCodecControlHooks, planFieldEventOperations, plannerFailure } from "@prisma-next/family-sql/control";
9
- import { verifySqlSchemaTree } from "@prisma-next/family-sql/diff";
10
- import { SchemaDiff } from "@prisma-next/framework-components/control";
11
- import { blindCast } from "@prisma-next/utils/casts";
12
- import { defaultIndexName } from "@prisma-next/sql-schema-ir/naming";
13
- import { ifDefined } from "@prisma-next/utils/defined";
14
- import { notOk, ok } from "@prisma-next/utils/result";
15
- //#region src/core/migrations/diff-database-schema.ts
16
- /** Renders a column default for the SQLite dialect. */
17
- function sqliteRenderDefault(def, _column) {
18
- if (def.kind === "function") {
19
- if (def.expression === "now()") return "datetime('now')";
20
- return def.expression;
21
- }
22
- return renderDefaultLiteral(def.value);
23
- }
24
- /** The SQLite expected-side projection: contract → flat relational schema IR. */
25
- function sqliteContractToSchema(contract) {
26
- return contractToSchemaIR(contract, {
27
- annotationNamespace: "sqlite",
28
- renderDefault: sqliteRenderDefault
29
- });
30
- }
31
- function computeSqliteSchemaComparison(input) {
32
- return verifySqlSchemaTree({
33
- contract: input.contract,
34
- actualSchema: input.actualSchema,
35
- buildExpectedSchema: sqliteContractToSchema,
36
- strict: input.strict,
37
- typeMetadataRegistry: input.typeMetadataRegistry,
38
- frameworkComponents: input.frameworkComponents,
39
- normalizeDefault: parseSqliteDefault,
40
- normalizeNativeType: normalizeSqliteNativeType
41
- });
42
- }
43
- /**
44
- * The SQLite `SchemaDiffer` — relational only. SQLite has a single flat
45
- * schema and no structural (policy) diff, so it runs the shared per-schema
46
- * relational diff and returns no `schemaDiffIssues`.
47
- */
48
- function diffSqliteDatabaseSchema(input) {
49
- const relational = computeSqliteSchemaComparison(input);
50
- return new SchemaDiff(relational.schema.issues, relational.schema.schemaDiffIssues);
51
- }
52
- /**
53
- * The same comparison as {@link diffSqliteDatabaseSchema}, wrapped in the
54
- * verify envelope (`ok`/`summary`/`code`/`target`/`timings`) plus the
55
- * pass/warn/fail tree the CLI renders.
56
- */
57
- function verifySqliteDatabaseSchema(input) {
58
- return computeSqliteSchemaComparison(input);
59
- }
60
- //#endregion
61
- //#region src/core/migrations/planner-strategies.ts
62
- /**
63
- * Look up a storage table by its explicit namespace coordinate. Returns
64
- * `undefined` when the namespace has no table by that name (or no such
65
- * namespace exists). Callers that get `undefined` MUST treat it as an
66
- * explicit conflict rather than silently falling back to a name-only
67
- * walk across namespaces — the SQLite target currently has a single
68
- * namespace, but this helper enforces the explicit-coordinate
69
- * discipline so a future multi-namespace SQLite shape inherits the
70
- * conflict-on-stale-coordinate behaviour the Postgres planner already
71
- * has.
72
- */
73
- function tableAt(storage, namespaceId, tableName) {
74
- const ns = storage.namespaces[namespaceId];
75
- return ns !== void 0 ? ns.entries.table?.[tableName] : void 0;
76
- }
77
- /**
78
- * Default namespace coordinate for an issue that does not carry one
79
- * explicitly. Hand-crafted unit-test issues fall back to `__unbound__`,
80
- * the only namespace any single-namespace contract carries —
81
- * verifier-emitted issues for legacy single-namespace contracts already
82
- * stamp this id explicitly. Typed structurally so issue variants
83
- * without a `namespaceId` slot flow through to the same fallback.
84
- */
85
- function resolveNamespaceIdForIssue(issue) {
86
- return issue.namespaceId ?? UNBOUND_NAMESPACE_ID;
87
- }
88
- const WIDENING_ISSUE_KINDS = new Set(["default_mismatch", "default_missing"]);
89
- const DESTRUCTIVE_ISSUE_KINDS = new Set([
90
- "extra_default",
91
- "type_mismatch",
92
- "primary_key_mismatch",
93
- "foreign_key_mismatch",
94
- "unique_constraint_mismatch",
95
- "extra_foreign_key",
96
- "extra_unique_constraint",
97
- "extra_primary_key"
98
- ]);
99
- function classifyIssue(issue) {
100
- if (issue.kind === "enum_values_changed") return null;
101
- if (!issue.table) return null;
102
- if (issue.kind === "nullability_mismatch") return issue.expected === "true" ? "widening" : "destructive";
103
- if (WIDENING_ISSUE_KINDS.has(issue.kind)) return "widening";
104
- if (DESTRUCTIVE_ISSUE_KINDS.has(issue.kind)) return "destructive";
105
- return null;
106
- }
107
- /**
108
- * Groups recreate-eligible issues by table, decides per-table operation class
109
- * (destructive wins over widening), and emits one `RecreateTableCall` per
110
- * table. Returns unchanged-or-smaller issue list — issues the strategy
111
- * consumed are removed so `mapIssueToCall` doesn't double-handle them.
112
- */
113
- const recreateTableStrategy = (issues, ctx) => {
114
- const byTable = /* @__PURE__ */ new Map();
115
- const consumed = /* @__PURE__ */ new Set();
116
- for (const issue of issues) {
117
- const cls = classifyIssue(issue);
118
- if (!cls) continue;
119
- if (issue.kind === "enum_values_changed") continue;
120
- if (!issue.table) continue;
121
- const table = issue.table;
122
- const entry = byTable.get(table);
123
- if (entry) {
124
- entry.issues.push(issue);
125
- if (cls === "destructive") entry.hasDestructive = true;
126
- } else byTable.set(table, {
127
- issues: [issue],
128
- hasDestructive: cls === "destructive",
129
- namespaceId: resolveNamespaceIdForIssue(issue)
130
- });
131
- consumed.add(issue);
132
- }
133
- if (byTable.size === 0) return { kind: "no_match" };
134
- const calls = [];
135
- for (const [tableName, entry] of byTable) {
136
- const contractTable = tableAt(ctx.toContract.storage, entry.namespaceId, tableName);
137
- const schemaTable = ctx.schema.tables[tableName];
138
- if (!contractTable || !schemaTable) continue;
139
- const operationClass = entry.hasDestructive ? "destructive" : "widening";
140
- const tableSpec = toTableSpec(contractTable, ctx.storageTypes);
141
- const seenIndexColumnKeys = /* @__PURE__ */ new Set();
142
- const indexes = [];
143
- for (const idx of contractTable.indexes) {
144
- const key = idx.columns.join(",");
145
- if (seenIndexColumnKeys.has(key)) continue;
146
- seenIndexColumnKeys.add(key);
147
- indexes.push({
148
- name: idx.name ?? defaultIndexName(tableName, idx.columns),
149
- columns: idx.columns
150
- });
151
- }
152
- for (const fk of contractTable.foreignKeys) {
153
- if (fk.index === false) continue;
154
- const key = fk.source.columns.join(",");
155
- if (seenIndexColumnKeys.has(key)) continue;
156
- seenIndexColumnKeys.add(key);
157
- indexes.push({
158
- name: defaultIndexName(tableName, fk.source.columns),
159
- columns: fk.source.columns
160
- });
161
- }
162
- calls.push(new RecreateTableCall({
163
- tableName,
164
- contractTable: tableSpec,
165
- schemaColumnNames: Object.keys(schemaTable.columns),
166
- indexes,
167
- summary: buildRecreateSummary(tableName, entry.issues),
168
- postchecks: buildRecreatePostchecks(tableName, entry.issues, tableSpec),
169
- operationClass
170
- }));
171
- }
172
- return {
173
- kind: "match",
174
- issues: issues.filter((i) => !consumed.has(i)),
175
- calls,
176
- recipe: true
177
- };
178
- };
179
- /**
180
- * When the policy allows `'data'` and the contract tightens one or more
181
- * columns from nullable to NOT NULL, emit a `DataTransformCall` stub per
182
- * tightened column. The user fills the backfill `UPDATE` in the rendered
183
- * `migration.ts` before the subsequent `RecreateTableCall` copies data into
184
- * the tightened schema (whose `INSERT INTO temp SELECT … FROM old` would
185
- * otherwise fail at runtime if any `NULL`s remain).
186
- *
187
- * Does NOT consume the tightening issue — `recreateTableStrategy` still
188
- * needs it to produce the actual recreate that enforces the NOT NULL at
189
- * the schema level. The backfill op and the recreate op end up in the
190
- * recipe slot in strategy order (backfill first, recreate second), which
191
- * matches the required execution order.
192
- *
193
- * Mirrors Postgres's `nullableTighteningCallStrategy` / `'data'`-class
194
- * gating. When `'data'` is not in the policy (the default `db update` /
195
- * `db init` path), the strategy short-circuits and the recreate alone
196
- * runs with its current destructive-class gating — preserving today's
197
- * behavior where a tightening blows up at runtime if NULLs are present.
198
- */
199
- const nullabilityTighteningBackfillStrategy = (issues, ctx) => {
200
- if (!ctx.policy.allowedOperationClasses.includes("data")) return { kind: "no_match" };
201
- const calls = [];
202
- for (const issue of issues) {
203
- if (issue.kind !== "nullability_mismatch") continue;
204
- if (!issue.table || !issue.column) continue;
205
- if (issue.expected === "true") continue;
206
- const namespaceId = resolveNamespaceIdForIssue(issue);
207
- const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
208
- if (!column || column.nullable === true) continue;
209
- calls.push(new DataTransformCall(`data_migration.backfill-${issue.table}-${issue.column}`, `Backfill NULLs in "${issue.table}"."${issue.column}" before NOT NULL tightening`, issue.table, issue.column));
210
- }
211
- if (calls.length === 0) return { kind: "no_match" };
212
- return {
213
- kind: "match",
214
- issues,
215
- calls,
216
- recipe: true
217
- };
218
- };
219
- const sqlitePlannerStrategies = [nullabilityTighteningBackfillStrategy, recreateTableStrategy];
220
- //#endregion
221
- //#region src/core/migrations/issue-planner.ts
222
- const ISSUE_KIND_ORDER = {
223
- extra_foreign_key: 10,
224
- extra_unique_constraint: 11,
225
- extra_primary_key: 12,
226
- extra_index: 13,
227
- extra_default: 14,
228
- extra_column: 15,
229
- extra_table: 16,
230
- missing_table: 20,
231
- missing_column: 30,
232
- type_mismatch: 40,
233
- nullability_mismatch: 41,
234
- default_missing: 42,
235
- default_mismatch: 43,
236
- primary_key_mismatch: 50,
237
- unique_constraint_mismatch: 51,
238
- index_mismatch: 52,
239
- foreign_key_mismatch: 60
240
- };
241
- function issueOrder(issue) {
242
- return ISSUE_KIND_ORDER[issue.kind] ?? 99;
243
- }
244
- function issueKey(issue) {
245
- return `${"table" in issue && typeof issue.table === "string" ? issue.table : ""}\u0000${"column" in issue && typeof issue.column === "string" ? issue.column : ""}\u0000${"indexOrConstraint" in issue && typeof issue.indexOrConstraint === "string" ? issue.indexOrConstraint : ""}`;
246
- }
247
- function issueConflict(kind, summary, location) {
248
- return {
249
- kind,
250
- summary,
251
- why: "Use `migration new` to author a custom migration for this change.",
252
- ...location ? { location } : {}
253
- };
254
- }
255
- function conflictKindForIssue(issue) {
256
- switch (issue.kind) {
257
- case "type_mismatch": return "typeMismatch";
258
- case "nullability_mismatch": return "nullabilityConflict";
259
- case "primary_key_mismatch":
260
- case "unique_constraint_mismatch":
261
- case "index_mismatch":
262
- case "extra_primary_key":
263
- case "extra_unique_constraint": return "indexIncompatible";
264
- case "foreign_key_mismatch":
265
- case "extra_foreign_key": return "foreignKeyConflict";
266
- default: return "missingButNonAdditive";
267
- }
268
- }
269
- function issueLocation(issue) {
270
- if (issue.kind === "enum_values_changed") return void 0;
271
- const location = {};
272
- if (issue.table) location.table = issue.table;
273
- if (issue.column) location.column = issue.column;
274
- if (issue.indexOrConstraint) location.constraint = issue.indexOrConstraint;
275
- return Object.keys(location).length > 0 ? location : void 0;
276
- }
277
- function conflictForDisallowedCall(call, allowed) {
278
- const summary = `Operation "${call.label}" requires class "${call.operationClass}", but policy allows only: ${allowed.join(", ")}`;
279
- const location = locationForCall(call);
280
- return {
281
- kind: conflictKindForCall(call),
282
- summary,
283
- why: "Use `migration new` to author a custom migration for this change.",
284
- ...location ? { location } : {}
285
- };
286
- }
287
- function conflictKindForCall(call) {
288
- switch (call.factoryName) {
289
- case "createIndex":
290
- case "dropIndex": return "indexIncompatible";
291
- default: return "missingButNonAdditive";
292
- }
293
- }
294
- function locationForCall(call) {
295
- const location = {};
296
- if ("tableName" in call) location.table = call.tableName;
297
- if ("columnName" in call) location.column = call.columnName;
298
- if ("indexName" in call) location.index = call.indexName;
299
- return Object.keys(location).length > 0 ? location : void 0;
300
- }
301
- function isMissing(issue) {
302
- if (issue.kind === "enum_values_changed") return false;
303
- return issue.actual === void 0;
304
- }
305
- /**
306
- * Resolves codec / `typeRef` / default rendering into a flat
307
- * `SqliteColumnSpec`. Mirrors Postgres's `toColumnSpec`. Once a column is
308
- * flattened, downstream Calls and operation factories never see
309
- * `StorageColumn` again — they deal in pre-rendered SQL fragments.
310
- */
311
- function toColumnSpec(name, column, storageTypes, inlineAutoincrementPrimaryKey = false) {
312
- return {
313
- name,
314
- typeSql: buildColumnTypeSql(column, blindCast(storageTypes)),
315
- defaultSql: buildColumnDefaultSql(column.default),
316
- nullable: column.nullable,
317
- ...inlineAutoincrementPrimaryKey ? { inlineAutoincrementPrimaryKey: true } : {}
318
- };
319
- }
320
- /**
321
- * Flattens a `StorageTable` into a `SqliteTableSpec` ready for
322
- * `CreateTableCall` / `RecreateTableCall`. Sole-column AUTOINCREMENT
323
- * primary keys are detected here and marked on the column spec so the
324
- * renderer emits `INTEGER PRIMARY KEY AUTOINCREMENT` inline.
325
- */
326
- function toTableSpec(table, storageTypes) {
327
- const columns = Object.entries(table.columns).map(([name, column]) => toColumnSpec(name, column, storageTypes, isInlineAutoincrementPrimaryKey(table, name)));
328
- const uniques = table.uniques.map((u) => ({
329
- columns: u.columns,
330
- ...u.name !== void 0 ? { name: u.name } : {}
331
- }));
332
- const foreignKeys = table.foreignKeys.map((fk) => ({
333
- columns: fk.source.columns,
334
- references: {
335
- table: fk.target.tableName,
336
- columns: fk.target.columns
337
- },
338
- constraint: fk.constraint !== false,
339
- ...fk.name !== void 0 ? { name: fk.name } : {},
340
- ...fk.onDelete !== void 0 ? { onDelete: fk.onDelete } : {},
341
- ...fk.onUpdate !== void 0 ? { onUpdate: fk.onUpdate } : {}
342
- }));
343
- return {
344
- columns,
345
- ...table.primaryKey ? { primaryKey: { columns: table.primaryKey.columns } } : {},
346
- uniques,
347
- foreignKeys
348
- };
349
- }
350
- function sqliteDefaultToDdlColumnDefault(columnDefault) {
351
- if (!columnDefault) return void 0;
352
- switch (columnDefault.kind) {
353
- case "literal": return new LiteralColumnDefault(columnDefault.value);
354
- case "function":
355
- if (columnDefault.expression === "autoincrement()") return void 0;
356
- return new FunctionColumnDefault(columnDefault.expression);
357
- default: throw new Error(`sqliteDefaultToDdlColumnDefault: unhandled kind "${blindCast(columnDefault).kind}"`);
358
- }
359
- }
360
- /**
361
- * Converts a `StorageTable` to the `DdlColumn[]` + `DdlTableConstraint[]`
362
- * pair used by `CreateTableCall`. This is the structured form consumed by
363
- * the DDL lowering path; `toTableSpec` / `toColumnSpec` remain in use for
364
- * `RecreateTableCall` and `AddColumnCall` (Phase 2).
365
- */
366
- function tableToDdlParts(table, storageTypes) {
367
- const columns = Object.entries(table.columns).map(([name, column]) => {
368
- const inlineAutoincrement = isInlineAutoincrementPrimaryKey(table, name);
369
- const typeSql = buildColumnTypeSql(column, blindCast(storageTypes));
370
- if (inlineAutoincrement) return new DdlColumn({
371
- name,
372
- type: `${typeSql} PRIMARY KEY AUTOINCREMENT`
373
- });
374
- const colDefault = sqliteDefaultToDdlColumnDefault(column.default);
375
- const resolved = resolveColumnTypeMetadata(column, blindCast(storageTypes));
376
- const codecRef = resolved.codecId ? {
377
- codecId: resolved.codecId,
378
- ...resolved.typeParams !== void 0 ? { typeParams: blindCast(resolved.typeParams) } : {}
379
- } : void 0;
380
- return new DdlColumn({
381
- name,
382
- type: typeSql,
383
- ...!column.nullable ? { notNull: true } : {},
384
- ...colDefault !== void 0 ? { default: colDefault } : {},
385
- ...codecRef !== void 0 ? { codecRef } : {}
386
- });
387
- });
388
- const constraints = [];
389
- const hasInlinePk = Object.entries(table.columns).some(([name]) => isInlineAutoincrementPrimaryKey(table, name));
390
- if (table.primaryKey && !hasInlinePk) constraints.push(new PrimaryKeyConstraint({ columns: table.primaryKey.columns }));
391
- for (const u of table.uniques) constraints.push(new UniqueConstraint({
392
- columns: u.columns,
393
- ...u.name !== void 0 ? { name: u.name } : {}
394
- }));
395
- for (const fk of table.foreignKeys) {
396
- if (fk.constraint === false) continue;
397
- constraints.push(new ForeignKeyConstraint({
398
- columns: fk.source.columns,
399
- refTable: fk.target.tableName,
400
- refColumns: fk.target.columns,
401
- ...ifDefined("name", fk.name),
402
- ...ifDefined("onDelete", fk.onDelete),
403
- ...ifDefined("onUpdate", fk.onUpdate)
404
- }));
405
- }
406
- return {
407
- columns,
408
- constraints
409
- };
410
- }
411
- const DEFAULT_POLICY = { allowedOperationClasses: [
412
- "additive",
413
- "widening",
414
- "destructive",
415
- "data"
416
- ] };
417
- function emptySchemaIR() {
418
- return { tables: {} };
419
- }
420
- function mapIssueToCall(issue, ctx) {
421
- switch (issue.kind) {
422
- case "missing_table": {
423
- if (!issue.table) return notOk(issueConflict("unsupportedOperation", "Missing table issue has no table name"));
424
- const namespaceId = resolveNamespaceIdForIssue(issue);
425
- const contractTable = tableAt(ctx.toContract.storage, namespaceId, issue.table);
426
- if (!contractTable) return notOk(issueConflict("unsupportedOperation", `Table "${issue.table}" in namespace "${namespaceId}" reported missing but not found in destination contract`));
427
- const { columns: ddlColumns, constraints: ddlConstraints } = tableToDdlParts(contractTable, ctx.storageTypes);
428
- const calls = [new CreateTableCall(issue.table, ddlColumns, ddlConstraints.length > 0 ? ddlConstraints : void 0)];
429
- const declaredIndexColumnKeys = /* @__PURE__ */ new Set();
430
- for (const index of contractTable.indexes) {
431
- const indexName = index.name ?? defaultIndexName(issue.table, index.columns);
432
- declaredIndexColumnKeys.add(index.columns.join(","));
433
- calls.push(new CreateIndexCall(issue.table, indexName, index.columns));
434
- }
435
- for (const fk of contractTable.foreignKeys) {
436
- if (fk.index === false) continue;
437
- if (declaredIndexColumnKeys.has(fk.source.columns.join(","))) continue;
438
- const indexName = defaultIndexName(issue.table, fk.source.columns);
439
- calls.push(new CreateIndexCall(issue.table, indexName, fk.source.columns));
440
- }
441
- return ok(calls);
442
- }
443
- case "missing_column": {
444
- if (!issue.table || !issue.column) return notOk(issueConflict("unsupportedOperation", "Missing column issue has no table/column name"));
445
- const namespaceId = resolveNamespaceIdForIssue(issue);
446
- const contractTable2 = tableAt(ctx.toContract.storage, namespaceId, issue.table);
447
- const column = contractTable2?.columns[issue.column];
448
- if (!column) return notOk(issueConflict("unsupportedOperation", `Column "${issue.table}"."${issue.column}" not in destination contract`));
449
- const contractTable = contractTable2;
450
- const columnSpec = toColumnSpec(issue.column, column, ctx.storageTypes, contractTable ? isInlineAutoincrementPrimaryKey(contractTable, issue.column) : false);
451
- return ok([new AddColumnCall(issue.table, columnSpec)]);
452
- }
453
- case "index_mismatch": {
454
- if (!issue.table) return notOk(issueConflict("indexIncompatible", "Index issue has no table name"));
455
- if (!isMissing(issue) || !issue.expected) return notOk(issueConflict("indexIncompatible", `Index on "${issue.table}" differs (expected: ${issue.expected}, actual: ${issue.actual})`, { table: issue.table }));
456
- const namespaceId = resolveNamespaceIdForIssue(issue);
457
- const columns = issue.expected.split(", ");
458
- const contractTable = tableAt(ctx.toContract.storage, namespaceId, issue.table);
459
- if (!contractTable) return notOk(issueConflict("unsupportedOperation", `Table "${issue.table}" not found in destination contract`));
460
- const indexName = contractTable.indexes.find((idx) => idx.columns.join(",") === columns.join(","))?.name ?? defaultIndexName(issue.table, columns);
461
- return ok([new CreateIndexCall(issue.table, indexName, columns)]);
462
- }
463
- case "extra_table":
464
- if (!issue.table) return notOk(issueConflict("unsupportedOperation", "Extra table issue has no table name"));
465
- if (CONTROL_TABLE_NAMES.has(issue.table)) return ok([]);
466
- return ok([new DropTableCall(issue.table)]);
467
- case "extra_column":
468
- if (!issue.table || !issue.column) return notOk(issueConflict("unsupportedOperation", "Extra column issue has no table/column name"));
469
- return ok([new DropColumnCall(issue.table, issue.column)]);
470
- case "extra_index":
471
- if (!issue.table || !issue.indexOrConstraint) return notOk(issueConflict("unsupportedOperation", "Extra index issue has no table/index name"));
472
- return ok([new DropIndexCall(issue.table, issue.indexOrConstraint)]);
473
- case "enum_values_changed": return notOk(issueConflict("unsupportedOperation", "Received enum_values_changed against a SQLite schema (sql.enums: false) — verifier bug"));
474
- case "type_mismatch":
475
- case "nullability_mismatch":
476
- case "default_mismatch":
477
- case "default_missing":
478
- case "extra_default":
479
- case "primary_key_mismatch":
480
- case "unique_constraint_mismatch":
481
- case "foreign_key_mismatch":
482
- case "extra_foreign_key":
483
- case "extra_unique_constraint":
484
- case "extra_primary_key": return notOk(issueConflict(conflictKindForIssue(issue), issue.message, issueLocation(issue)));
485
- default: return notOk(issueConflict("unsupportedOperation", `Unhandled issue kind: ${issue.kind}`));
486
- }
487
- }
488
- function classifyCall(call) {
489
- switch (call.factoryName) {
490
- case "createTable": return "create-table";
491
- case "addColumn": return "add-column";
492
- case "createIndex": return "create-index";
493
- case "dropColumn": return "drop-column";
494
- case "dropIndex": return "drop-index";
495
- case "dropTable": return "drop-table";
496
- case "recreateTable": return null;
497
- default: return null;
498
- }
499
- }
500
- function planIssues(options) {
501
- const policyProvided = options.policy !== void 0;
502
- const policy = options.policy ?? DEFAULT_POLICY;
503
- const schema = options.schema ?? emptySchemaIR();
504
- const frameworkComponents = options.frameworkComponents ?? [];
505
- const context = {
506
- toContract: options.toContract,
507
- fromContract: options.fromContract,
508
- codecHooks: options.codecHooks,
509
- storageTypes: options.storageTypes,
510
- schema,
511
- policy,
512
- frameworkComponents
513
- };
514
- const strategies = options.strategies ?? sqlitePlannerStrategies;
515
- let remaining = options.issues;
516
- const recipeCalls = [];
517
- const bucketableCalls = [];
518
- for (const strategy of strategies) {
519
- const result = strategy(remaining, context);
520
- if (result.kind === "match") {
521
- remaining = result.issues;
522
- if (result.recipe) recipeCalls.push(...result.calls);
523
- else bucketableCalls.push(...result.calls);
524
- }
525
- }
526
- const sorted = [...remaining].sort((a, b) => {
527
- const kindDelta = issueOrder(a) - issueOrder(b);
528
- if (kindDelta !== 0) return kindDelta;
529
- const keyA = issueKey(a);
530
- const keyB = issueKey(b);
531
- return keyA < keyB ? -1 : keyA > keyB ? 1 : 0;
532
- });
533
- const defaultCalls = [];
534
- const conflicts = [];
535
- for (const issue of sorted) {
536
- const result = mapIssueToCall(issue, context);
537
- if (result.ok) defaultCalls.push(...result.value);
538
- else conflicts.push(result.failure);
539
- }
540
- const allowed = policy.allowedOperationClasses;
541
- let gatedRecipe = recipeCalls;
542
- let gatedBucketable = bucketableCalls;
543
- let gatedDefault = defaultCalls;
544
- if (policyProvided) {
545
- const sink = (acc) => (call) => {
546
- if (allowed.includes(call.operationClass)) {
547
- acc.push(call);
548
- return;
549
- }
550
- conflicts.push(conflictForDisallowedCall(call, allowed));
551
- };
552
- const gatedRecipeBucket = [];
553
- const gatedBucketableBucket = [];
554
- const gatedDefaultBucket = [];
555
- recipeCalls.forEach(sink(gatedRecipeBucket));
556
- bucketableCalls.forEach(sink(gatedBucketableBucket));
557
- defaultCalls.forEach(sink(gatedDefaultBucket));
558
- gatedRecipe = gatedRecipeBucket;
559
- gatedBucketable = gatedBucketableBucket;
560
- gatedDefault = gatedDefaultBucket;
561
- }
562
- if (conflicts.length > 0) return notOk(conflicts);
563
- const combined = [...gatedDefault, ...gatedBucketable];
564
- const byCategory = (cat) => combined.filter((c) => classifyCall(c) === cat);
565
- return ok({ calls: [
566
- ...byCategory("create-table"),
567
- ...byCategory("add-column"),
568
- ...byCategory("create-index"),
569
- ...gatedRecipe,
570
- ...byCategory("drop-column"),
571
- ...byCategory("drop-index"),
572
- ...byCategory("drop-table")
573
- ] });
574
- }
575
- //#endregion
576
- //#region src/core/migrations/planner.ts
577
- function createSqliteMigrationPlanner(lowerer) {
578
- return new SqliteMigrationPlanner(lowerer);
579
- }
580
- /**
581
- * SQLite migration planner — a thin wrapper over `planIssues`.
582
- *
583
- * `plan()` verifies the live schema against the target contract (producing
584
- * `SchemaIssue[]`) and delegates to `planIssues` with the registered
585
- * strategies. Strategies absorb groups of related issues into composite
586
- * recipes (e.g. recreating a table to apply type/nullability/default/
587
- * constraint changes at once); anything not absorbed by a strategy flows
588
- * through `mapIssueToCall` in the issue planner as a one-off call.
589
- *
590
- * FK-backing indexes are surfaced by `verifySqlSchema`'s index expansion
591
- * (see `verify-sql-schema.ts:459-469`), so `mapIssueToCall` handles them
592
- * uniformly alongside user-declared indexes.
593
- */
594
- var SqliteMigrationPlanner = class {
595
- #lowerer;
596
- constructor(lowerer) {
597
- this.#lowerer = lowerer;
598
- }
599
- plan(options) {
600
- return this.planSql(options);
601
- }
602
- emptyMigration(context, spaceId) {
603
- return new TypeScriptRenderableSqliteMigration([], {
604
- from: context.fromHash,
605
- to: context.toHash
606
- }, spaceId, void 0, this.#lowerer);
607
- }
608
- planSql(options) {
609
- const policyResult = this.ensureAdditivePolicy(options.policy);
610
- if (policyResult) return policyResult;
611
- const schemaIssues = this.collectSchemaIssues(options);
612
- const codecHooks = extractCodecControlHooks(options.frameworkComponents);
613
- const storageTypes = options.contract.storage.types ?? {};
614
- const result = planIssues({
615
- issues: schemaIssues,
616
- toContract: options.contract,
617
- fromContract: options.fromContract,
618
- codecHooks,
619
- storageTypes,
620
- schema: sqliteFlatSchema(options.schema),
621
- policy: options.policy,
622
- frameworkComponents: options.frameworkComponents,
623
- strategies: sqlitePlannerStrategies
624
- });
625
- if (!result.ok) return plannerFailure(result.failure);
626
- const fieldEventOps = planFieldEventOperations({
627
- priorContract: options.fromContract,
628
- newContract: options.contract,
629
- codecHooks
630
- });
631
- const calls = [...result.value.calls, ...fieldEventOps];
632
- const destination = {
633
- storageHash: options.contract.storage.storageHash,
634
- ...options.contract.profileHash !== void 0 ? { profileHash: options.contract.profileHash } : {}
635
- };
636
- return {
637
- kind: "success",
638
- plan: new TypeScriptRenderableSqliteMigration(calls, {
639
- from: options.fromContract?.storage.storageHash ?? null,
640
- to: options.contract.storage.storageHash
641
- }, options.spaceId, destination, this.#lowerer)
642
- };
643
- }
644
- ensureAdditivePolicy(policy) {
645
- if (!policy.allowedOperationClasses.includes("additive")) return plannerFailure([{
646
- kind: "unsupportedOperation",
647
- summary: "Migration planner requires additive operations be allowed",
648
- why: "The planner requires the \"additive\" operation class to be allowed in the policy."
649
- }]);
650
- return null;
651
- }
652
- collectSchemaIssues(options) {
653
- const allowed = options.policy.allowedOperationClasses;
654
- const strict = allowed.includes("widening") || allowed.includes("destructive");
655
- const rawDiff = diffSqliteDatabaseSchema({
656
- contract: options.contract,
657
- actualSchema: options.schema,
658
- strict,
659
- typeMetadataRegistry: /* @__PURE__ */ new Map(),
660
- frameworkComponents: options.frameworkComponents
661
- });
662
- return (options.keepDiffIssue ? rawDiff.filter(options.keepDiffIssue) : rawDiff).issues;
663
- }
664
- };
665
- /**
666
- * SQLite has a single, flat schema — its introspected node IS a per-schema
667
- * `SqlSchemaIR`, never the multi-namespace tree the Postgres target builds. The
668
- * planner consumes that flat shape directly when building ops.
669
- */
670
- function sqliteFlatSchema(schema) {
671
- return blindCast(schema);
672
- }
673
- //#endregion
674
- export { verifySqliteDatabaseSchema as a, sqliteContractToSchema as i, createSqliteMigrationPlanner as n, diffSqliteDatabaseSchema as r, SqliteMigrationPlanner as t };
675
-
676
- //# sourceMappingURL=planner-EoQcJGzv.mjs.map