@prisma-next/target-postgres 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 (97) hide show
  1. package/dist/control-policy-DUFgy9jO.mjs +1103 -0
  2. package/dist/control-policy-DUFgy9jO.mjs.map +1 -0
  3. package/dist/control.d.mts.map +1 -1
  4. package/dist/control.mjs +19 -40
  5. package/dist/control.mjs.map +1 -1
  6. package/dist/default-normalizer.mjs +247 -1
  7. package/dist/default-normalizer.mjs.map +1 -0
  8. package/dist/diff-database-schema-BmSmWcaY.mjs +313 -0
  9. package/dist/diff-database-schema-BmSmWcaY.mjs.map +1 -0
  10. package/dist/diff-database-schema.d.mts +46 -0
  11. package/dist/diff-database-schema.d.mts.map +1 -0
  12. package/dist/diff-database-schema.mjs +2 -0
  13. package/dist/issue-planner.d.mts +34 -10
  14. package/dist/issue-planner.d.mts.map +1 -1
  15. package/dist/issue-planner.mjs +2 -2
  16. package/dist/migration.d.mts +2 -2
  17. package/dist/migration.mjs +2 -2
  18. package/dist/native-type-normalizer.mjs +37 -1
  19. package/dist/native-type-normalizer.mjs.map +1 -0
  20. package/dist/{op-factory-call-BuoGT5UI.mjs → op-factory-call-BKRnMgrB.mjs} +2 -2
  21. package/dist/op-factory-call-BKRnMgrB.mjs.map +1 -0
  22. package/dist/{op-factory-call-Caqus-Qg.d.mts → op-factory-call-Dt-Jx5JS.d.mts} +2 -2
  23. package/dist/{op-factory-call-Caqus-Qg.d.mts.map → op-factory-call-Dt-Jx5JS.d.mts.map} +1 -1
  24. package/dist/op-factory-call.d.mts +2 -2
  25. package/dist/op-factory-call.mjs +2 -2
  26. package/dist/planner-BMBRPzsv.mjs +367 -0
  27. package/dist/planner-BMBRPzsv.mjs.map +1 -0
  28. package/dist/planner-ddl-builders-B8Nn6nHN.mjs.map +1 -1
  29. package/dist/planner-ddl-builders.d.mts +2 -2
  30. package/dist/planner-ddl-builders.d.mts.map +1 -1
  31. package/dist/planner-identity-values-CJPha2Sz.mjs.map +1 -1
  32. package/dist/planner-identity-values.d.mts +7 -1
  33. package/dist/planner-identity-values.d.mts.map +1 -1
  34. package/dist/planner-identity-values.mjs +2 -2
  35. package/dist/{planner-produced-postgres-migration-CtJi4sgS.d.mts → planner-produced-postgres-migration-C0a7I1-e.d.mts} +2 -2
  36. package/dist/{planner-produced-postgres-migration-CtJi4sgS.d.mts.map → planner-produced-postgres-migration-C0a7I1-e.d.mts.map} +1 -1
  37. package/dist/{planner-produced-postgres-migration-C2_H6Vvl.mjs → planner-produced-postgres-migration-Dp6IO_gj.mjs} +2 -2
  38. package/dist/{planner-produced-postgres-migration-C2_H6Vvl.mjs.map → planner-produced-postgres-migration-Dp6IO_gj.mjs.map} +1 -1
  39. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  40. package/dist/planner-produced-postgres-migration.mjs +1 -1
  41. package/dist/planner-sql-checks-Cze3vMfe.mjs.map +1 -1
  42. package/dist/planner-sql-checks.d.mts +1 -1
  43. package/dist/planner-sql-checks.d.mts.map +1 -1
  44. package/dist/planner.d.mts +27 -67
  45. package/dist/planner.d.mts.map +1 -1
  46. package/dist/planner.mjs +3 -2
  47. package/dist/{postgres-database-schema-node-os8ogEap.d.mts → postgres-database-schema-node-C9bTuMrd.d.mts} +14 -7
  48. package/dist/postgres-database-schema-node-C9bTuMrd.d.mts.map +1 -0
  49. package/dist/{postgres-migration-D0QIVE-p.mjs → postgres-migration-D0jwzYDT.mjs} +2 -2
  50. package/dist/{postgres-migration-D0QIVE-p.mjs.map → postgres-migration-D0jwzYDT.mjs.map} +1 -1
  51. package/dist/{postgres-migration-CfyKDT7b.d.mts → postgres-migration-DFaxuRyI.d.mts} +2 -2
  52. package/dist/{postgres-migration-CfyKDT7b.d.mts.map → postgres-migration-DFaxuRyI.d.mts.map} +1 -1
  53. package/dist/{postgres-table-schema-node-Clei_xel.mjs → postgres-table-schema-node-BgaSrxYN.mjs} +30 -29
  54. package/dist/postgres-table-schema-node-BgaSrxYN.mjs.map +1 -0
  55. package/dist/schema-node-kinds-ClScchhi.mjs +76 -0
  56. package/dist/schema-node-kinds-ClScchhi.mjs.map +1 -0
  57. package/dist/types.d.mts +1 -1
  58. package/dist/types.mjs +1 -1
  59. package/package.json +21 -20
  60. package/src/core/migrations/column-ddl-rendering.ts +117 -0
  61. package/src/core/migrations/contract-to-postgres-database-schema-node.ts +25 -1
  62. package/src/core/migrations/control-policy.ts +68 -48
  63. package/src/core/migrations/diff-database-schema.ts +242 -153
  64. package/src/core/migrations/issue-planner.ts +621 -667
  65. package/src/core/migrations/operations/constraints.ts +1 -1
  66. package/src/core/migrations/planner-ddl-builders.ts +5 -2
  67. package/src/core/migrations/planner-identity-values.ts +1 -1
  68. package/src/core/migrations/planner-sql-checks.ts +1 -1
  69. package/src/core/migrations/planner-strategies.ts +248 -208
  70. package/src/core/migrations/planner.ts +180 -74
  71. package/src/core/migrations/runner.ts +1 -4
  72. package/src/core/migrations/verify-postgres-namespaces.ts +26 -15
  73. package/src/core/postgres-schema-verifier.ts +10 -7
  74. package/src/core/psl-infer/infer-psl-contract.ts +5 -10
  75. package/src/core/schema-ir/postgres-database-schema-node.ts +3 -6
  76. package/src/core/schema-ir/postgres-namespace-schema-node.ts +8 -9
  77. package/src/core/schema-ir/postgres-policy-schema-node.ts +3 -6
  78. package/src/core/schema-ir/postgres-role-schema-node.ts +3 -6
  79. package/src/core/schema-ir/postgres-table-schema-node.ts +20 -9
  80. package/src/core/schema-ir/schema-node-kinds.ts +80 -2
  81. package/src/exports/control.ts +10 -43
  82. package/src/exports/diff-database-schema.ts +7 -0
  83. package/src/exports/issue-planner.ts +1 -1
  84. package/src/exports/op-factory-call.ts +1 -0
  85. package/src/exports/planner-identity-values.ts +4 -1
  86. package/src/exports/planner.ts +0 -1
  87. package/dist/default-normalizer-Dug8Fez9.mjs +0 -248
  88. package/dist/default-normalizer-Dug8Fez9.mjs.map +0 -1
  89. package/dist/issue-planner-Cbh-xTEr.mjs +0 -849
  90. package/dist/issue-planner-Cbh-xTEr.mjs.map +0 -1
  91. package/dist/native-type-normalizer-Bc9XJzWC.mjs +0 -38
  92. package/dist/native-type-normalizer-Bc9XJzWC.mjs.map +0 -1
  93. package/dist/op-factory-call-BuoGT5UI.mjs.map +0 -1
  94. package/dist/planner-Bq0Z2sVO.mjs +0 -641
  95. package/dist/planner-Bq0Z2sVO.mjs.map +0 -1
  96. package/dist/postgres-database-schema-node-os8ogEap.d.mts.map +0 -1
  97. package/dist/postgres-table-schema-node-Clei_xel.mjs.map +0 -1
@@ -1,849 +0,0 @@
1
- import { r as isPostgresSchema } from "./postgres-schema-CajYAwny.mjs";
2
- import { i as quoteIdentifier } from "./sql-utils-DcfMz4MQ.mjs";
3
- import { D as postgresDefaultToDdlColumnDefault, E as SetNotNullCall, O as buildTargetDetails, S as DropTableCall, T as SetDefaultCall, _ as DropConstraintCall, a as AddNotNullColumnWithTempDefaultCall, b as DropNotNullCall, c as AlterColumnTypeCall, f as CreateSchemaCall, g as DropColumnCall, h as DropCheckConstraintCall, i as AddNotNullColumnDirectCall, m as DataTransformCall, n as AddColumnCall, o as AddPrimaryKeyCall, p as CreateTableCall, r as AddForeignKeyCall, s as AddUniqueCall, t as AddCheckConstraintCall, u as CreateIndexCall, v as DropDefaultCall, w as RawSqlCall, y as DropIndexCall } from "./op-factory-call-BuoGT5UI.mjs";
4
- import { t as resolveColumnTypeMetadata } from "./planner-type-resolution-Bt2f_q-F.mjs";
5
- import { t as buildExpectedFormatType } from "./planner-sql-checks-Cze3vMfe.mjs";
6
- import { n as buildColumnTypeSql, t as buildColumnDefaultSql } from "./planner-ddl-builders-B8Nn6nHN.mjs";
7
- import { n as resolveIdentityValue } from "./planner-identity-values-CJPha2Sz.mjs";
8
- import { i as hasUniqueConstraint, n as hasForeignKey, t as buildSchemaLookupMap } from "./planner-schema-lookup-CiVaAQP-.mjs";
9
- import { UNBOUND_NAMESPACE_ID } from "@prisma-next/framework-components/ir";
10
- import { blindCast } from "@prisma-next/utils/casts";
11
- import { StorageTable } from "@prisma-next/sql-contract/types";
12
- import { resolveValueSetValues } from "@prisma-next/family-sql/control";
13
- import { ifDefined } from "@prisma-next/utils/defined";
14
- import * as contractFree from "@prisma-next/sql-relational-core/contract-free";
15
- import { col } from "@prisma-next/sql-relational-core/contract-free";
16
- import { arraysEqual } from "@prisma-next/family-sql/diff";
17
- import { notOk, ok } from "@prisma-next/utils/result";
18
- //#region src/core/migrations/planner-strategies.ts
19
- /**
20
- * Look up a storage table by its explicit namespace coordinate. Returns
21
- * `undefined` when the namespace has no table by that name (or no such
22
- * namespace exists). Callers that get `undefined` MUST treat it as an
23
- * explicit conflict — never silently fall back to a global default
24
- * schema or a name-only walk, because that footgun would resolve a
25
- * stale or duplicate table name to whichever namespace the iteration
26
- * order surfaced first (a real data-loss hazard in multi-namespace
27
- * contracts where two namespaces can carry the same table name).
28
- */
29
- function tableAt(storage, namespaceId, tableName) {
30
- const ns = storage.namespaces[namespaceId];
31
- if (ns === void 0) return void 0;
32
- return ns.entries.table?.[tableName];
33
- }
34
- /**
35
- * Default namespace coordinate for an issue that does not carry one
36
- * explicitly. Hand-crafted unit-test issues and `extra_table` issues
37
- * fall back to `__unbound__`, the only namespace any single-namespace
38
- * contract carries — verifier-emitted issues for legacy
39
- * single-namespace contracts already stamp this id explicitly. Typed
40
- * structurally so issue variants without a `namespaceId` slot
41
- * (e.g. `EnumValuesChangedIssue`) flow through to the same fallback.
42
- */
43
- function resolveNamespaceIdForIssue(issue) {
44
- return issue.namespaceId ?? UNBOUND_NAMESPACE_ID;
45
- }
46
- /**
47
- * Resolve the DDL schema name for a namespace coordinate. Postgres-aware
48
- * namespaces dispatch to their polymorphic `ddlSchemaName` override —
49
- * named schemas return their own id; the unbound singleton returns
50
- * `UNBOUND_NAMESPACE_ID`. Legacy single-namespace contracts whose
51
- * `__unbound__` slot is the framework-default `SqlUnboundNamespace`
52
- * (rather than the Postgres-aware `PostgresUnboundSchema`) flow the
53
- * coordinate through unchanged so downstream `qualifyTableName`
54
- * resolves polymorphically.
55
- */
56
- function resolveDdlSchemaForNamespace(ctx, namespaceId) {
57
- const namespace = ctx.toContract.storage.namespaces[namespaceId];
58
- if (isPostgresSchema(namespace)) return namespace.ddlSchemaName(ctx.toContract.storage);
59
- return namespaceId;
60
- }
61
- function buildColumnSpec(namespaceId, table, column, ctx, overrides) {
62
- const storageCol = tableAt(ctx.toContract.storage, namespaceId, table)?.columns[column];
63
- if (!storageCol) throw new Error(`Column "${table}"."${column}" not found in destination contract`);
64
- const mutableHooks = ctx.codecHooks;
65
- const mutableTypes = ctx.storageTypes;
66
- const typeSql = buildColumnTypeSql(storageCol, mutableHooks, mutableTypes);
67
- const ddlDefault = postgresDefaultToDdlColumnDefault(storageCol.default);
68
- const resolved = resolveColumnTypeMetadata(storageCol, mutableTypes);
69
- const typeParams = resolved.typeParams === void 0 ? void 0 : blindCast(resolved.typeParams);
70
- const codecRef = resolved.codecId ? {
71
- codecId: resolved.codecId,
72
- ...ifDefined("typeParams", typeParams)
73
- } : void 0;
74
- return col(column, typeSql, {
75
- ...!(overrides?.nullable ?? storageCol.nullable) ? { notNull: true } : {},
76
- ...ifDefined("default", ddlDefault),
77
- ...ifDefined("codecRef", codecRef)
78
- });
79
- }
80
- function buildAlterTypeOptions(namespaceId, table, column, ctx, using) {
81
- const col = tableAt(ctx.toContract.storage, namespaceId, table)?.columns[column];
82
- if (!col) throw new Error(`Column "${table}"."${column}" not found in destination contract`);
83
- const mutableHooks = ctx.codecHooks;
84
- const mutableTypes = ctx.storageTypes;
85
- const qualifiedTargetType = buildColumnTypeSql(col, mutableHooks, mutableTypes, false);
86
- return {
87
- qualifiedTargetType,
88
- formatTypeExpected: buildExpectedFormatType(col, mutableHooks, mutableTypes),
89
- rawTargetTypeForLabel: qualifiedTargetType,
90
- ...using !== void 0 ? { using } : {}
91
- };
92
- }
93
- const notNullBackfillCallStrategy = (issues, ctx) => {
94
- if (!ctx.policy.allowedOperationClasses.includes("data")) return { kind: "no_match" };
95
- const matched = [];
96
- const calls = [];
97
- for (const issue of issues) {
98
- if (issue.kind !== "missing_column" || !issue.table || !issue.column) continue;
99
- const namespaceId = resolveNamespaceIdForIssue(issue);
100
- const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
101
- if (!column) continue;
102
- if (column.nullable === true || column.default !== void 0) continue;
103
- matched.push(issue);
104
- const spec = buildColumnSpec(namespaceId, issue.table, issue.column, ctx, { nullable: true });
105
- const schemaForTable = resolveDdlSchemaForNamespace(ctx, namespaceId);
106
- calls.push(new AddColumnCall(schemaForTable, issue.table, spec), new DataTransformCall(`backfill-${issue.table}-${issue.column}`, `backfill-${issue.table}-${issue.column}:check`, `backfill-${issue.table}-${issue.column}:run`), new SetNotNullCall(schemaForTable, issue.table, issue.column));
107
- }
108
- if (matched.length === 0) return { kind: "no_match" };
109
- return {
110
- kind: "match",
111
- issues: issues.filter((i) => !matched.includes(i)),
112
- calls,
113
- recipe: true
114
- };
115
- };
116
- const SAFE_WIDENINGS = new Set([
117
- "int2→int4",
118
- "int2→int8",
119
- "int4→int8",
120
- "float4→float8"
121
- ]);
122
- const typeChangeCallStrategy = (issues, ctx) => {
123
- const dataAllowed = ctx.policy.allowedOperationClasses.includes("data");
124
- const matched = [];
125
- const calls = [];
126
- for (const issue of issues) {
127
- if (issue.kind !== "type_mismatch") continue;
128
- if (!issue.table || !issue.column) continue;
129
- const namespaceId = resolveNamespaceIdForIssue(issue);
130
- const fromColumn = ctx.fromContract ? tableAt(ctx.fromContract.storage, namespaceId, issue.table)?.columns[issue.column] : void 0;
131
- const toColumn = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
132
- if (!fromColumn || !toColumn) continue;
133
- const fromType = fromColumn.nativeType;
134
- const toType = toColumn.nativeType;
135
- if (fromType === toType) continue;
136
- const isSafeWidening = SAFE_WIDENINGS.has(`${fromType}→${toType}`);
137
- if (!isSafeWidening && !dataAllowed) continue;
138
- matched.push(issue);
139
- const alterOpts = buildAlterTypeOptions(namespaceId, issue.table, issue.column, ctx);
140
- const schemaForTable = resolveDdlSchemaForNamespace(ctx, namespaceId);
141
- if (isSafeWidening) calls.push(new AlterColumnTypeCall(schemaForTable, issue.table, issue.column, alterOpts));
142
- else calls.push(new DataTransformCall(`typechange-${issue.table}-${issue.column}`, `typechange-${issue.table}-${issue.column}:check`, `typechange-${issue.table}-${issue.column}:run`), new AlterColumnTypeCall(schemaForTable, issue.table, issue.column, alterOpts));
143
- }
144
- if (matched.length === 0) return { kind: "no_match" };
145
- return {
146
- kind: "match",
147
- issues: issues.filter((i) => !matched.includes(i)),
148
- calls,
149
- recipe: true
150
- };
151
- };
152
- const nullableTighteningCallStrategy = (issues, ctx) => {
153
- if (!ctx.policy.allowedOperationClasses.includes("data")) return { kind: "no_match" };
154
- const matched = [];
155
- const calls = [];
156
- for (const issue of issues) {
157
- if (issue.kind !== "nullability_mismatch" || !issue.table || !issue.column) continue;
158
- const namespaceId = resolveNamespaceIdForIssue(issue);
159
- const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
160
- if (!column) continue;
161
- if (column.nullable === true) continue;
162
- matched.push(issue);
163
- const schemaForTable = resolveDdlSchemaForNamespace(ctx, namespaceId);
164
- calls.push(new DataTransformCall(`handle-nulls-${issue.table}-${issue.column}`, `handle-nulls-${issue.table}-${issue.column}:check`, `handle-nulls-${issue.table}-${issue.column}:run`), new SetNotNullCall(schemaForTable, issue.table, issue.column));
165
- }
166
- if (matched.length === 0) return { kind: "no_match" };
167
- return {
168
- kind: "match",
169
- issues: issues.filter((i) => !matched.includes(i)),
170
- calls,
171
- recipe: true
172
- };
173
- };
174
- /**
175
- * Collects every check constraint from a table in the contract storage.
176
- * Returns an empty array when the table has no checks or the table is absent.
177
- */
178
- function collectContractChecks(storage, namespaceId, tableName) {
179
- const ns = storage.namespaces[namespaceId];
180
- const tableRaw = ns !== void 0 ? ns.entries.table?.[tableName] : void 0;
181
- if (!(tableRaw instanceof StorageTable)) return [];
182
- const checks = tableRaw.checks;
183
- if (!checks || checks.length === 0) return [];
184
- return checks.map((c) => ({
185
- name: c.name,
186
- column: c.column,
187
- permittedValues: resolveValueSetValues(c.valueSet, storage, `check "${c.name}" on "${tableName}"`)
188
- }));
189
- }
190
- /**
191
- * Compares two value arrays as unordered sets.
192
- */
193
- function checkValueSetsEqual(a, b) {
194
- if (a.length !== b.length) return false;
195
- const bSet = new Set(b);
196
- return a.every((v) => bSet.has(v));
197
- }
198
- /**
199
- * Plans check-constraint migrations for `enumType`-authored columns.
200
- *
201
- * Walks every namespace's tables in the target contract. For each table that
202
- * carries `checks`, diffs the contract-expected checks against the live
203
- * schema's checks:
204
- *
205
- * - Check in contract, absent from live DB → `AddCheckConstraintCall`.
206
- * - Check in live DB, absent from contract → `DropCheckConstraintCall`.
207
- * - Check on both sides but value sets differ → `DropCheckConstraintCall`
208
- * then `AddCheckConstraintCall` (drop + recreate; a check predicate cannot
209
- * be altered in place).
210
- *
211
- * Consumes `check_missing`, `check_removed`, and `check_mismatch` issues.
212
- */
213
- const checkConstraintPlanCallStrategy = (issues, ctx) => {
214
- const calls = [];
215
- const handledIssueKeys = /* @__PURE__ */ new Set();
216
- for (const [namespaceId, ns] of Object.entries(ctx.toContract.storage.namespaces)) for (const tableName of Object.keys(ns.entries.table ?? {})) {
217
- const contractChecks = collectContractChecks(ctx.toContract.storage, namespaceId, tableName);
218
- if (contractChecks.length === 0) continue;
219
- const liveChecks = ctx.schema.tables[tableName]?.checks ?? [];
220
- const ddlSchema = resolveDdlSchemaForNamespace(ctx, namespaceId);
221
- for (const contractCheck of contractChecks) {
222
- const liveCheck = liveChecks.find((c) => c.name === contractCheck.name);
223
- const issueKey = `${tableName}${contractCheck.name}`;
224
- if (!liveCheck) {
225
- calls.push(new AddCheckConstraintCall(ddlSchema, tableName, contractCheck.name, contractCheck.column, contractCheck.permittedValues));
226
- handledIssueKeys.add(issueKey);
227
- } else if (!checkValueSetsEqual(contractCheck.permittedValues, liveCheck.permittedValues)) {
228
- calls.push(new DropCheckConstraintCall(ddlSchema, tableName, contractCheck.name), new AddCheckConstraintCall(ddlSchema, tableName, contractCheck.name, contractCheck.column, contractCheck.permittedValues));
229
- handledIssueKeys.add(issueKey);
230
- } else handledIssueKeys.add(issueKey);
231
- }
232
- for (const liveCheck of liveChecks) if (!contractChecks.some((c) => c.name === liveCheck.name)) {
233
- const issueKey = `${tableName}${liveCheck.name}`;
234
- calls.push(new DropCheckConstraintCall(ddlSchema, tableName, liveCheck.name));
235
- handledIssueKeys.add(issueKey);
236
- }
237
- }
238
- if (calls.length === 0 && handledIssueKeys.size === 0) return { kind: "no_match" };
239
- return {
240
- kind: "match",
241
- issues: issues.filter((issue) => {
242
- if (issue.kind !== "check_missing" && issue.kind !== "check_removed" && issue.kind !== "check_mismatch") return true;
243
- if (!issue.table || !issue.indexOrConstraint) return true;
244
- const key = `${issue.table}${issue.indexOrConstraint}`;
245
- return !handledIssueKeys.has(key);
246
- }),
247
- calls
248
- };
249
- };
250
- /**
251
- * Dispatches codec-typed storage types through their codec's
252
- * `planTypeOperations` hook (the authoritative source for codec-driven DDL
253
- * such as custom type creation).
254
- */
255
- const storageTypePlanCallStrategy = (issues, ctx) => {
256
- const storageTypes = ctx.toContract.storage.types ?? {};
257
- if (Object.keys(storageTypes).length === 0) return { kind: "no_match" };
258
- const calls = [];
259
- const handledTypeNames = /* @__PURE__ */ new Set();
260
- for (const [typeName, typeInstance] of Object.entries(storageTypes).sort(([a], [b]) => a.localeCompare(b))) {
261
- const codecInstance = typeInstance;
262
- const hook = ctx.codecHooks.get(codecInstance.codecId);
263
- if (!hook?.planTypeOperations) continue;
264
- const planResult = hook.planTypeOperations({
265
- typeName,
266
- typeInstance: codecInstance,
267
- contract: ctx.toContract,
268
- schema: ctx.schema,
269
- schemaName: ctx.schemaName,
270
- policy: ctx.policy
271
- });
272
- if (!planResult) continue;
273
- if (planResult.operations.length === 0) {
274
- handledTypeNames.add(typeName);
275
- continue;
276
- }
277
- handledTypeNames.add(typeName);
278
- for (const op of planResult.operations) calls.push(new RawSqlCall({
279
- ...op,
280
- target: {
281
- id: op.target.id,
282
- details: buildTargetDetails("type", typeName, ctx.schemaName)
283
- }
284
- }));
285
- }
286
- const remaining = issues.filter((issue) => !((issue.kind === "type_missing" || issue.kind === "enum_values_changed") && issue.typeName && handledTypeNames.has(issue.typeName)));
287
- if (calls.length === 0 && remaining.length === issues.length) return { kind: "no_match" };
288
- return {
289
- kind: "match",
290
- issues: remaining,
291
- calls
292
- };
293
- };
294
- /**
295
- * Handles `missing_column` issues for NOT NULL columns without a contract
296
- * default. Replaces the walk-schema `buildAddColumnItem` non-default branches.
297
- *
298
- * Two shapes:
299
- * - Shared-temp-default safe: emit a single atomic composite op (add
300
- * nullable → backfill identity value → `SET NOT NULL` → `DROP DEFAULT`).
301
- * - Empty-table guarded: emit a hand-built op with a `tableIsEmptyCheck`
302
- * precheck so the failure message is "table is not empty" rather than the
303
- * raw PG NOT NULL violation.
304
- *
305
- * "Normal" missing_column cases (nullable or has a contract default) are left
306
- * for `mapIssueToCall`'s default `AddColumnCall` emission.
307
- */
308
- const notNullAddColumnCallStrategy = (issues, ctx) => {
309
- const matched = [];
310
- const calls = [];
311
- const schemaLookups = buildSchemaLookupMap(ctx.schema);
312
- const mutableCodecHooks = ctx.codecHooks;
313
- const mutableStorageTypes = ctx.storageTypes;
314
- for (const issue of issues) {
315
- if (issue.kind !== "missing_column" || !issue.table || !issue.column) continue;
316
- const namespaceId = resolveNamespaceIdForIssue(issue);
317
- const contractTable = tableAt(ctx.toContract.storage, namespaceId, issue.table);
318
- const column = contractTable?.columns[issue.column];
319
- if (!column) continue;
320
- const notNull = column.nullable !== true;
321
- const hasDefault = column.default !== void 0;
322
- if (!notNull || hasDefault) continue;
323
- const schemaTable = ctx.schema.tables[issue.table];
324
- if (!schemaTable) continue;
325
- const temporaryDefault = resolveIdentityValue(column, mutableCodecHooks, mutableStorageTypes);
326
- const schemaLookup = schemaLookups.get(issue.table);
327
- const canUseSharedTempDefault = temporaryDefault !== null && canUseSharedTemporaryDefaultStrategy({
328
- table: contractTable,
329
- schemaTable,
330
- schemaLookup,
331
- columnName: issue.column
332
- });
333
- matched.push(issue);
334
- const schemaForTable = resolveDdlSchemaForNamespace(ctx, namespaceId);
335
- if (canUseSharedTempDefault && temporaryDefault !== null) {
336
- calls.push(new AddNotNullColumnWithTempDefaultCall({
337
- schemaName: schemaForTable,
338
- tableName: issue.table,
339
- columnName: issue.column,
340
- column,
341
- codecHooks: mutableCodecHooks,
342
- storageTypes: mutableStorageTypes,
343
- temporaryDefault
344
- }));
345
- continue;
346
- }
347
- calls.push(new AddNotNullColumnDirectCall(schemaForTable, issue.table, issue.column, buildColumnSpec(namespaceId, issue.table, issue.column, ctx)));
348
- }
349
- if (matched.length === 0) return { kind: "no_match" };
350
- return {
351
- kind: "match",
352
- issues: issues.filter((i) => !matched.includes(i)),
353
- calls
354
- };
355
- };
356
- function canUseSharedTemporaryDefaultStrategy(options) {
357
- const { table, schemaTable, schemaLookup, columnName } = options;
358
- if (table.primaryKey?.columns.includes(columnName) && !schemaTable.primaryKey) return false;
359
- for (const unique of table.uniques) {
360
- if (!unique.columns.includes(columnName)) continue;
361
- if (!schemaLookup || !hasUniqueConstraint(schemaLookup, unique.columns)) return false;
362
- }
363
- for (const foreignKey of table.foreignKeys) {
364
- if (foreignKey.constraint === false || !foreignKey.source.columns.includes(columnName)) continue;
365
- if (!schemaLookup || !hasForeignKey(schemaLookup, foreignKey)) return false;
366
- }
367
- return true;
368
- }
369
- /**
370
- * Ordered list of Postgres planner strategies, shared by `migration plan`
371
- * and `db update` / `db init`. The issue planner runs each strategy in
372
- * order, letting it consume any issues it handles, and routes whatever's
373
- * left through `mapIssueToCall`. Behavior diverges purely on
374
- * `policy.allowedOperationClasses`:
375
- *
376
- * - When `'data'` is allowed (`migration plan`), the data-safe strategies
377
- * (`notNullBackfillCallStrategy`, `typeChangeCallStrategy`,
378
- * `nullableTighteningCallStrategy`) consume their matching issues and emit
379
- * `DataTransformCall` placeholders or recipe ops.
380
- *
381
- * - When `'data'` is not allowed (`db update` / `db init`), the
382
- * placeholder-emitting strategies short-circuit to `no_match`, leaving
383
- * the issue for the downstream walk-schema strategies
384
- * (`storageTypePlanCallStrategy`, `notNullAddColumnCallStrategy`) or the
385
- * `mapIssueToCall` default to handle with direct DDL.
386
- *
387
- * Codec-typed storage type entries are dispatched through
388
- * `storageTypePlanCallStrategy`.
389
- */
390
- const postgresPlannerStrategies = [
391
- notNullBackfillCallStrategy,
392
- typeChangeCallStrategy,
393
- nullableTighteningCallStrategy,
394
- checkConstraintPlanCallStrategy,
395
- storageTypePlanCallStrategy,
396
- notNullAddColumnCallStrategy
397
- ];
398
- //#endregion
399
- //#region src/core/migrations/issue-planner.ts
400
- /**
401
- * Deterministic name for the element-non-null CHECK constraint on a scalar-array
402
- * column. Distinct `_elem_not_null` suffix avoids collision with the enum
403
- * value-set `_check` constraints. Re-emitting the same schema produces the same
404
- * name, so `pg_get_constraintdef`-based verify sees no drift.
405
- */
406
- function elementNonNullCheckName(tableName, columnName) {
407
- return `${tableName}_${columnName}_elem_not_null`;
408
- }
409
- /**
410
- * Predicate enforcing that a scalar-array column carries no NULL element. The
411
- * array column itself may be NULL (container nullability is the column's NOT NULL
412
- * clause); `array_position` over a NULL array yields NULL, which a CHECK treats
413
- * as satisfied, so a nullable array column is unaffected.
414
- */
415
- function elementNonNullCheckExpression(columnName) {
416
- return `array_position(${quoteIdentifier(columnName)}, NULL) IS NULL`;
417
- }
418
- const ISSUE_KIND_ORDER = {
419
- missing_schema: 1,
420
- type_missing: 2,
421
- type_values_mismatch: 3,
422
- enum_values_changed: 3,
423
- extra_foreign_key: 10,
424
- extra_unique_constraint: 11,
425
- extra_primary_key: 12,
426
- extra_index: 13,
427
- extra_default: 14,
428
- extra_column: 15,
429
- extra_table: 16,
430
- missing_table: 20,
431
- missing_column: 30,
432
- type_mismatch: 40,
433
- nullability_mismatch: 41,
434
- default_missing: 42,
435
- default_mismatch: 43,
436
- primary_key_mismatch: 50,
437
- unique_constraint_mismatch: 51,
438
- index_mismatch: 52,
439
- foreign_key_mismatch: 60,
440
- check_missing: 53,
441
- check_mismatch: 54,
442
- check_removed: 55
443
- };
444
- function issueOrder(issue) {
445
- return ISSUE_KIND_ORDER[issue.kind] ?? 99;
446
- }
447
- function issueConflict(kind, summary, location) {
448
- return {
449
- kind,
450
- summary,
451
- why: "Use `migration new` to author a custom migration for this change.",
452
- ...location ? { location } : {}
453
- };
454
- }
455
- function isMissing(issue) {
456
- if (issue.kind === "enum_values_changed") return false;
457
- return issue.actual === void 0;
458
- }
459
- function toDdlColumn(name, column, codecHooks, storageTypes) {
460
- const typeSql = buildColumnTypeSql(column, codecHooks, storageTypes);
461
- const ddlDefault = postgresDefaultToDdlColumnDefault(column.default);
462
- const resolved = resolveColumnTypeMetadata(column, storageTypes);
463
- const codecRef = resolved.codecId ? {
464
- codecId: resolved.codecId,
465
- ...resolved.typeParams !== void 0 ? { typeParams: blindCast(resolved.typeParams) } : {}
466
- } : void 0;
467
- return contractFree.col(name, typeSql, {
468
- ...!column.nullable ? { notNull: true } : {},
469
- ...ifDefined("default", ddlDefault),
470
- ...ifDefined("codecRef", codecRef)
471
- });
472
- }
473
- function mapIssueToCall(issue, ctx) {
474
- const { schemaName, codecHooks, storageTypes } = ctx;
475
- const tableSchema = (issue) => {
476
- if (issue.kind === "extra_table") return schemaName;
477
- if (!("table" in issue) || !issue.table) return schemaName;
478
- return resolveDdlSchemaForNamespace(ctx, resolveNamespaceIdForIssue(issue));
479
- };
480
- switch (issue.kind) {
481
- case "missing_schema": {
482
- const namespaceId = issue.namespaceId;
483
- if (!namespaceId) return notOk(issueConflict("unsupportedOperation", "Missing schema issue has no namespaceId"));
484
- return ok([new CreateSchemaCall(resolveDdlSchemaForNamespace(ctx, namespaceId))]);
485
- }
486
- case "missing_table": {
487
- if (!issue.table) return notOk(issueConflict("unsupportedOperation", "Missing table issue has no table name"));
488
- const namespaceId = resolveNamespaceIdForIssue(issue);
489
- const contractTable = tableAt(ctx.toContract.storage, namespaceId, issue.table);
490
- if (!contractTable) return notOk(issueConflict("unsupportedOperation", `Table "${issue.table}" in namespace "${namespaceId}" reported missing but not found in destination contract`));
491
- const schemaForTable = tableSchema(issue);
492
- const missingTableName = issue.table;
493
- const ddlColumns = Object.entries(contractTable.columns).map(([name, column]) => toDdlColumn(name, column, codecHooks, storageTypes));
494
- const primaryKeyConstraints = contractTable.primaryKey ? [contractFree.primaryKey(contractTable.primaryKey.columns, { ...contractTable.primaryKey.name ? { name: contractTable.primaryKey.name } : {} })] : [];
495
- const elementNonNullChecks = Object.entries(contractTable.columns).filter(([, column]) => column.many === true).map(([columnName]) => contractFree.checkExpression(elementNonNullCheckName(missingTableName, columnName), elementNonNullCheckExpression(columnName)));
496
- const allTableConstraints = [...primaryKeyConstraints, ...elementNonNullChecks];
497
- const ddlConstraints = allTableConstraints.length > 0 ? allTableConstraints : void 0;
498
- const calls = [new CreateTableCall(schemaForTable, issue.table, ddlColumns, ddlConstraints)];
499
- for (const index of contractTable.indexes) {
500
- const indexName = index.name ?? `${issue.table}_${index.columns.join("_")}_idx`;
501
- const extras = {};
502
- if (index.type !== void 0) extras.type = index.type;
503
- if (index.options !== void 0) extras.options = index.options;
504
- calls.push(new CreateIndexCall(schemaForTable, issue.table, indexName, [...index.columns], extras));
505
- }
506
- const explicitIndexColumnSets = new Set(contractTable.indexes.map((idx) => idx.columns.join(",")));
507
- for (const fk of contractTable.foreignKeys) {
508
- if (fk.constraint) {
509
- const fkSpec = {
510
- name: fk.name ?? `${issue.table}_${fk.source.columns.join("_")}_fkey`,
511
- columns: fk.source.columns,
512
- references: {
513
- schema: fk.target.namespaceId,
514
- table: fk.target.tableName,
515
- columns: fk.target.columns
516
- },
517
- ...fk.onDelete !== void 0 && { onDelete: fk.onDelete },
518
- ...fk.onUpdate !== void 0 && { onUpdate: fk.onUpdate }
519
- };
520
- calls.push(new AddForeignKeyCall(schemaForTable, issue.table, fkSpec));
521
- }
522
- if (fk.index && !explicitIndexColumnSets.has(fk.source.columns.join(","))) {
523
- const indexName = `${issue.table}_${fk.source.columns.join("_")}_idx`;
524
- calls.push(new CreateIndexCall(schemaForTable, issue.table, indexName, [...fk.source.columns]));
525
- }
526
- }
527
- for (const unique of contractTable.uniques) {
528
- const constraintName = unique.name ?? `${issue.table}_${unique.columns.join("_")}_key`;
529
- calls.push(new AddUniqueCall(schemaForTable, issue.table, constraintName, [...unique.columns]));
530
- }
531
- return ok(calls);
532
- }
533
- case "missing_column":
534
- if (!issue.table || !issue.column) return notOk(issueConflict("unsupportedOperation", "Missing column issue has no table/column name"));
535
- {
536
- const namespaceId = resolveNamespaceIdForIssue(issue);
537
- const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
538
- if (!column) return notOk(issueConflict("unsupportedOperation", `Column "${issue.table}"."${issue.column}" not in destination contract`));
539
- return ok([new AddColumnCall(tableSchema(issue), issue.table, toDdlColumn(issue.column, column, codecHooks, storageTypes))]);
540
- }
541
- case "default_missing":
542
- if (!issue.table || !issue.column) return notOk(issueConflict("unsupportedOperation", "Default missing issue has no table/column name"));
543
- {
544
- const namespaceId = resolveNamespaceIdForIssue(issue);
545
- const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
546
- if (!column?.default) return notOk(issueConflict("unsupportedOperation", `Column "${issue.table}"."${issue.column}" has no default in contract`));
547
- const defaultSql = buildColumnDefaultSql(column.default, column);
548
- if (!defaultSql) return ok([]);
549
- return ok([new SetDefaultCall(tableSchema(issue), issue.table, issue.column, defaultSql)]);
550
- }
551
- case "extra_table":
552
- if (!issue.table) return notOk(issueConflict("unsupportedOperation", "Extra table issue has no table name"));
553
- return ok([new DropTableCall(tableSchema(issue), issue.table)]);
554
- case "extra_column":
555
- if (!issue.table || !issue.column) return notOk(issueConflict("unsupportedOperation", "Extra column issue has no table/column name"));
556
- return ok([new DropColumnCall(tableSchema(issue), issue.table, issue.column)]);
557
- case "extra_index":
558
- if (!issue.table || !issue.indexOrConstraint) return notOk(issueConflict("unsupportedOperation", "Extra index issue has no table/index name"));
559
- return ok([new DropIndexCall(tableSchema(issue), issue.table, issue.indexOrConstraint)]);
560
- case "extra_unique_constraint":
561
- case "extra_foreign_key":
562
- case "extra_primary_key": {
563
- if (!issue.table) return notOk(issueConflict("unsupportedOperation", "Extra constraint issue has no table/constraint name"));
564
- const constraintName = issue.indexOrConstraint ?? (issue.kind === "extra_primary_key" ? `${issue.table}_pkey` : void 0);
565
- if (!constraintName) return notOk(issueConflict("unsupportedOperation", "Extra constraint issue has no table/constraint name"));
566
- return ok([new DropConstraintCall(tableSchema(issue), issue.table, constraintName, {
567
- extra_unique_constraint: "unique",
568
- extra_foreign_key: "foreignKey",
569
- extra_primary_key: "primaryKey"
570
- }[issue.kind])]);
571
- }
572
- case "extra_default":
573
- if (!issue.table || !issue.column) return notOk(issueConflict("unsupportedOperation", "Extra default issue has no table/column name"));
574
- return ok([new DropDefaultCall(tableSchema(issue), issue.table, issue.column)]);
575
- case "nullability_mismatch": {
576
- if (!issue.table || !issue.column) return notOk(issueConflict("nullabilityConflict", "Nullability mismatch has no table/column name"));
577
- const namespaceId = resolveNamespaceIdForIssue(issue);
578
- const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
579
- if (!column) return notOk(issueConflict("nullabilityConflict", `Column "${issue.table}"."${issue.column}" not found in destination contract`));
580
- const schemaForTable = tableSchema(issue);
581
- return ok(column.nullable ? [new DropNotNullCall(schemaForTable, issue.table, issue.column)] : [new SetNotNullCall(schemaForTable, issue.table, issue.column)]);
582
- }
583
- case "type_mismatch":
584
- if (!issue.table || !issue.column) return notOk(issueConflict("typeMismatch", "Type mismatch has no table/column name"));
585
- {
586
- const namespaceId = resolveNamespaceIdForIssue(issue);
587
- const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
588
- if (!column) return notOk(issueConflict("typeMismatch", `Column "${issue.table}"."${issue.column}" not in destination contract`));
589
- const hooksMap = codecHooks;
590
- const typesMap = storageTypes;
591
- const qualifiedTargetType = buildColumnTypeSql(column, hooksMap, typesMap, false);
592
- const formatTypeExpected = buildExpectedFormatType(column, hooksMap, typesMap);
593
- return ok([new AlterColumnTypeCall(tableSchema(issue), issue.table, issue.column, {
594
- qualifiedTargetType,
595
- formatTypeExpected,
596
- rawTargetTypeForLabel: qualifiedTargetType
597
- })]);
598
- }
599
- case "default_mismatch":
600
- if (!issue.table || !issue.column) return notOk(issueConflict("unsupportedOperation", "Default mismatch has no table/column name"));
601
- {
602
- const namespaceId = resolveNamespaceIdForIssue(issue);
603
- const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
604
- if (!column?.default) return ok([]);
605
- const defaultSql = buildColumnDefaultSql(column.default, column);
606
- if (!defaultSql) return ok([]);
607
- return ok([new SetDefaultCall(tableSchema(issue), issue.table, issue.column, defaultSql, "widening")]);
608
- }
609
- case "primary_key_mismatch":
610
- if (!issue.table) return notOk(issueConflict("indexIncompatible", "Primary key issue has no table name"));
611
- if (isMissing(issue)) {
612
- const namespaceId = resolveNamespaceIdForIssue(issue);
613
- const pk = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.primaryKey;
614
- if (!pk) return notOk(issueConflict("indexIncompatible", `No primary key in contract for "${issue.table}"`));
615
- const constraintName = pk.name ?? `${issue.table}_pkey`;
616
- return ok([new AddPrimaryKeyCall(tableSchema(issue), issue.table, constraintName, pk.columns)]);
617
- }
618
- return notOk(issueConflict("indexIncompatible", `Primary key on "${issue.table}" has different columns (expected: ${issue.expected}, actual: ${issue.actual})`, { table: issue.table }));
619
- case "unique_constraint_mismatch":
620
- if (!issue.table) return notOk(issueConflict("indexIncompatible", "Unique constraint issue has no table name"));
621
- if (isMissing(issue) && issue.expected) {
622
- const columns = issue.expected.split(", ");
623
- const constraintName = `${issue.table}_${columns.join("_")}_key`;
624
- return ok([new AddUniqueCall(tableSchema(issue), issue.table, constraintName, columns)]);
625
- }
626
- return notOk(issueConflict("indexIncompatible", `Unique constraint on "${issue.table}" differs (expected: ${issue.expected}, actual: ${issue.actual})`, { table: issue.table }));
627
- case "index_mismatch":
628
- if (!issue.table) return notOk(issueConflict("indexIncompatible", "Index issue has no table name"));
629
- if (isMissing(issue) && issue.expected) {
630
- const namespaceId = resolveNamespaceIdForIssue(issue);
631
- const columns = issue.expected.split(", ");
632
- const contractIndex = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.indexes.find((idx) => arraysEqual(idx.columns, columns));
633
- const indexName = contractIndex?.name ?? `${issue.table}_${columns.join("_")}_idx`;
634
- const extras = {};
635
- if (contractIndex?.type !== void 0) extras.type = contractIndex.type;
636
- if (contractIndex?.options !== void 0) extras.options = contractIndex.options;
637
- return ok([new CreateIndexCall(tableSchema(issue), issue.table, indexName, columns, extras)]);
638
- }
639
- return notOk(issueConflict("indexIncompatible", `Index on "${issue.table}" differs (expected: ${issue.expected}, actual: ${issue.actual})`, { table: issue.table }));
640
- case "check_missing":
641
- if (!issue.table || !issue.indexOrConstraint) return notOk(issueConflict("unsupportedOperation", "Check missing issue has no table/constraint name"));
642
- return notOk(issueConflict("unsupportedOperation", `Check constraint "${issue.indexOrConstraint}" missing on "${issue.table}" — handled by checkConstraintPlanCallStrategy`));
643
- case "check_mismatch":
644
- if (!issue.table || !issue.indexOrConstraint) return notOk(issueConflict("unsupportedOperation", "Check mismatch issue has no table/constraint name"));
645
- return notOk(issueConflict("unsupportedOperation", `Check constraint "${issue.indexOrConstraint}" values mismatch on "${issue.table}" — handled by checkConstraintPlanCallStrategy`));
646
- case "check_removed":
647
- if (!issue.table || !issue.indexOrConstraint) return notOk(issueConflict("unsupportedOperation", "Check removed issue has no table/constraint name"));
648
- return ok([new DropCheckConstraintCall(tableSchema(issue), issue.table, issue.indexOrConstraint)]);
649
- case "foreign_key_mismatch":
650
- if (!issue.table) return notOk(issueConflict("foreignKeyConflict", "Foreign key issue has no table name"));
651
- if (isMissing(issue) && issue.expected) {
652
- const arrowIdx = issue.expected.indexOf(" -> ");
653
- if (arrowIdx >= 0) {
654
- const namespaceId = resolveNamespaceIdForIssue(issue);
655
- const columns = issue.expected.slice(0, arrowIdx).split(", ");
656
- const fkName = `${issue.table}_${columns.join("_")}_fkey`;
657
- const fk = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.foreignKeys.find((k) => k.source.columns.join(", ") === columns.join(", "));
658
- if (fk) {
659
- const fkSpec = {
660
- name: fkName,
661
- columns: fk.source.columns,
662
- references: {
663
- schema: fk.target.namespaceId,
664
- table: fk.target.tableName,
665
- columns: fk.target.columns
666
- },
667
- ...fk.onDelete !== void 0 && { onDelete: fk.onDelete },
668
- ...fk.onUpdate !== void 0 && { onUpdate: fk.onUpdate }
669
- };
670
- return ok([new AddForeignKeyCall(tableSchema(issue), issue.table, fkSpec)]);
671
- }
672
- return notOk(issueConflict("foreignKeyConflict", `Foreign key on "${issue.table}" (${columns.join(", ")}) not found in destination contract`, { table: issue.table }));
673
- }
674
- }
675
- return notOk(issueConflict("foreignKeyConflict", `Foreign key on "${issue.table}" differs (expected: ${issue.expected}, actual: ${issue.actual})`, { table: issue.table }));
676
- case "type_missing": {
677
- if (!issue.typeName) return notOk(issueConflict("unsupportedOperation", "Type missing issue has no typeName"));
678
- const typeInstance = ctx.toContract.storage.types?.[issue.typeName];
679
- if (!typeInstance) return notOk(issueConflict("unsupportedOperation", `Type "${issue.typeName}" reported missing but not found in destination contract`));
680
- return notOk(issueConflict("unsupportedOperation", `Type "${issue.typeName}" uses codec "${typeInstance.codecId}" — only value-set types are supported`));
681
- }
682
- case "type_values_mismatch": return notOk(issueConflict("unsupportedOperation", `Type "${issue.typeName ?? "unknown"}" values differ — type alteration not yet supported`));
683
- default: return notOk(issueConflict("unsupportedOperation", `Unhandled issue kind: ${issue.kind}`));
684
- }
685
- }
686
- /**
687
- * Classifies calls into DDL sequencing buckets. The order matches the
688
- * legacy walk-schema planner's emission order so `db init` and `db update`
689
- * produce byte-identical plans for the shared shape (deps → drops → tables
690
- * → columns → alters → PKs → uniques → indexes → FKs).
691
- */
692
- function classifyCall(call) {
693
- switch (call.factoryName) {
694
- case "createExtension":
695
- case "createSchema": return "dep";
696
- case "dropTable":
697
- case "dropColumn":
698
- case "dropConstraint":
699
- case "dropCheckConstraint":
700
- case "dropIndex":
701
- case "dropDefault": return "drop";
702
- case "addCheckConstraint": return "unique";
703
- case "createTable": return "table";
704
- case "enableRowLevelSecurity": return "rlsEnable";
705
- case "createRlsPolicy": return "rlsPolicy";
706
- case "dropRlsPolicy": return "drop";
707
- case "addColumn": return "column";
708
- case "alterColumnType":
709
- case "setNotNull":
710
- case "dropNotNull":
711
- case "setDefault": return "alter";
712
- case "addPrimaryKey": return "primaryKey";
713
- case "addUnique": return "unique";
714
- case "createIndex": return "index";
715
- case "addForeignKey": return "foreignKey";
716
- case "rawSql":
717
- if (call.op?.target?.details?.objectType === "type") return "dep";
718
- return "alter";
719
- default: return "alter";
720
- }
721
- }
722
- /** Stable lexical key used to order issues within the same kind bucket. */
723
- function issueKey(issue) {
724
- 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 : ""}`;
725
- }
726
- const DEFAULT_POLICY = { allowedOperationClasses: [
727
- "additive",
728
- "widening",
729
- "destructive",
730
- "data"
731
- ] };
732
- function emptySchemaIR() {
733
- return { tables: {} };
734
- }
735
- function conflictKindForCall(call) {
736
- switch (call.factoryName) {
737
- case "alterColumnType": return "typeMismatch";
738
- case "setNotNull":
739
- case "dropNotNull": return "nullabilityConflict";
740
- case "addForeignKey":
741
- case "dropConstraint": return "foreignKeyConflict";
742
- case "createIndex":
743
- case "dropIndex": return "indexIncompatible";
744
- default: return "missingButNonAdditive";
745
- }
746
- }
747
- function locationForCall(call) {
748
- const anyCall = call;
749
- const location = {};
750
- if (anyCall.tableName) location.table = anyCall.tableName;
751
- if (anyCall.columnName) location.column = anyCall.columnName;
752
- if (anyCall.indexName) location.index = anyCall.indexName;
753
- if (anyCall.constraintName) location.constraint = anyCall.constraintName;
754
- if (anyCall.typeName) location.type = anyCall.typeName;
755
- return Object.keys(location).length > 0 ? location : void 0;
756
- }
757
- function conflictForDisallowedCall(call, allowed) {
758
- const summary = `Operation "${call.label}" requires class "${call.operationClass}", but policy allows only: ${allowed.join(", ")}`;
759
- const location = locationForCall(call);
760
- return {
761
- kind: conflictKindForCall(call),
762
- summary,
763
- why: "Use `migration new` to author a custom migration for this change.",
764
- ...location ? { location } : {}
765
- };
766
- }
767
- function planIssues(options) {
768
- const policyProvided = options.policy !== void 0;
769
- const policy = options.policy ?? DEFAULT_POLICY;
770
- const schema = options.schema ?? emptySchemaIR();
771
- const frameworkComponents = options.frameworkComponents ?? [];
772
- const context = {
773
- toContract: options.toContract,
774
- fromContract: options.fromContract,
775
- schemaName: options.schemaName,
776
- codecHooks: options.codecHooks,
777
- storageTypes: options.storageTypes,
778
- schema,
779
- policy,
780
- frameworkComponents
781
- };
782
- const strategies = options.strategies ?? postgresPlannerStrategies;
783
- let remaining = options.issues;
784
- const recipeCalls = [];
785
- const bucketablePatternCalls = [];
786
- for (const strategy of strategies) {
787
- const result = strategy(remaining, context);
788
- if (result.kind === "match") {
789
- remaining = result.issues;
790
- if (result.recipe) recipeCalls.push(...result.calls);
791
- else bucketablePatternCalls.push(...result.calls);
792
- }
793
- }
794
- const sorted = [...remaining].sort((a, b) => {
795
- const kindDelta = issueOrder(a) - issueOrder(b);
796
- if (kindDelta !== 0) return kindDelta;
797
- const keyA = issueKey(a);
798
- const keyB = issueKey(b);
799
- return keyA < keyB ? -1 : keyA > keyB ? 1 : 0;
800
- });
801
- const defaultCalls = [];
802
- const conflicts = [];
803
- for (const issue of sorted) {
804
- const result = mapIssueToCall(issue, context);
805
- if (result.ok) defaultCalls.push(...result.value);
806
- else conflicts.push(result.failure);
807
- }
808
- const allowed = policy.allowedOperationClasses;
809
- let gatedDefault = defaultCalls;
810
- let gatedRecipe = recipeCalls;
811
- let gatedBucketable = bucketablePatternCalls;
812
- if (policyProvided) {
813
- const keepIfAllowed = (bucket) => (call) => {
814
- if (allowed.includes(call.operationClass)) {
815
- bucket.push(call);
816
- return;
817
- }
818
- conflicts.push(conflictForDisallowedCall(call, allowed));
819
- };
820
- const gatedDefaultBucket = [];
821
- const gatedRecipeBucket = [];
822
- const gatedBucketableBucket = [];
823
- defaultCalls.forEach(keepIfAllowed(gatedDefaultBucket));
824
- recipeCalls.forEach(keepIfAllowed(gatedRecipeBucket));
825
- bucketablePatternCalls.forEach(keepIfAllowed(gatedBucketableBucket));
826
- gatedDefault = gatedDefaultBucket;
827
- gatedRecipe = gatedRecipeBucket;
828
- gatedBucketable = gatedBucketableBucket;
829
- }
830
- if (conflicts.length > 0) return notOk(conflicts);
831
- const combinedBucketable = [...gatedDefault, ...gatedBucketable];
832
- const byCategory = (cat) => combinedBucketable.filter((c) => classifyCall(c) === cat);
833
- return ok({ calls: [
834
- ...byCategory("dep"),
835
- ...byCategory("drop"),
836
- ...byCategory("table"),
837
- ...byCategory("column"),
838
- ...gatedRecipe,
839
- ...byCategory("alter"),
840
- ...byCategory("primaryKey"),
841
- ...byCategory("unique"),
842
- ...byCategory("index"),
843
- ...byCategory("foreignKey")
844
- ] });
845
- }
846
- //#endregion
847
- export { postgresPlannerStrategies as n, planIssues as t };
848
-
849
- //# sourceMappingURL=issue-planner-Cbh-xTEr.mjs.map