@prisma-next/target-postgres 0.4.1 → 0.5.0-dev.2

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 (178) hide show
  1. package/dist/codec-ids-CojIXVf9.mjs +29 -0
  2. package/dist/codec-ids-CojIXVf9.mjs.map +1 -0
  3. package/dist/codec-ids.d.mts +28 -0
  4. package/dist/codec-ids.d.mts.map +1 -0
  5. package/dist/codec-ids.mjs +3 -0
  6. package/dist/codec-types.d.mts +42 -0
  7. package/dist/codec-types.d.mts.map +1 -0
  8. package/dist/codec-types.mjs +3 -0
  9. package/dist/codecs-BoahtY_Q.mjs +385 -0
  10. package/dist/codecs-BoahtY_Q.mjs.map +1 -0
  11. package/dist/codecs-D-F2KJqt.d.mts +299 -0
  12. package/dist/codecs-D-F2KJqt.d.mts.map +1 -0
  13. package/dist/codecs.d.mts +2 -0
  14. package/dist/codecs.mjs +3 -0
  15. package/dist/control.d.mts +1 -1
  16. package/dist/control.mjs +9 -1982
  17. package/dist/control.mjs.map +1 -1
  18. package/dist/data-transform-CxFRBIUp.d.mts +32 -0
  19. package/dist/data-transform-CxFRBIUp.d.mts.map +1 -0
  20. package/dist/data-transform-VfEGzXWt.mjs +39 -0
  21. package/dist/data-transform-VfEGzXWt.mjs.map +1 -0
  22. package/dist/data-transform.d.mts +2 -0
  23. package/dist/data-transform.mjs +3 -0
  24. package/dist/default-normalizer-DNOpRoOF.mjs +131 -0
  25. package/dist/default-normalizer-DNOpRoOF.mjs.map +1 -0
  26. package/dist/default-normalizer.d.mts +19 -0
  27. package/dist/default-normalizer.d.mts.map +1 -0
  28. package/dist/default-normalizer.mjs +3 -0
  29. package/dist/{descriptor-meta-DkvCmY98.mjs → descriptor-meta-BVoVtyp-.mjs} +1 -1
  30. package/dist/{descriptor-meta-DkvCmY98.mjs.map → descriptor-meta-BVoVtyp-.mjs.map} +1 -1
  31. package/dist/errors-AFvEPZ1R.mjs +34 -0
  32. package/dist/errors-AFvEPZ1R.mjs.map +1 -0
  33. package/dist/errors.d.mts +27 -0
  34. package/dist/errors.d.mts.map +1 -0
  35. package/dist/errors.mjs +3 -0
  36. package/dist/issue-planner-CFjB0_oO.mjs +879 -0
  37. package/dist/issue-planner-CFjB0_oO.mjs.map +1 -0
  38. package/dist/issue-planner.d.mts +85 -0
  39. package/dist/issue-planner.d.mts.map +1 -0
  40. package/dist/issue-planner.mjs +3 -0
  41. package/dist/migration.d.mts +5 -79
  42. package/dist/migration.d.mts.map +1 -1
  43. package/dist/migration.mjs +6 -428
  44. package/dist/migration.mjs.map +1 -1
  45. package/dist/native-type-normalizer-CInai_oY.mjs +38 -0
  46. package/dist/native-type-normalizer-CInai_oY.mjs.map +1 -0
  47. package/dist/native-type-normalizer.d.mts +18 -0
  48. package/dist/native-type-normalizer.d.mts.map +1 -0
  49. package/dist/native-type-normalizer.mjs +3 -0
  50. package/dist/op-factory-call-BKlruaiC.mjs +605 -0
  51. package/dist/op-factory-call-BKlruaiC.mjs.map +1 -0
  52. package/dist/op-factory-call-C3bWXKSP.d.mts +304 -0
  53. package/dist/op-factory-call-C3bWXKSP.d.mts.map +1 -0
  54. package/dist/op-factory-call.d.mts +3 -0
  55. package/dist/op-factory-call.mjs +3 -0
  56. package/dist/pack.d.mts +1 -1
  57. package/dist/pack.mjs +1 -1
  58. package/dist/planner-CLUvVhUN.mjs +98 -0
  59. package/dist/planner-CLUvVhUN.mjs.map +1 -0
  60. package/dist/planner-ddl-builders-Dxvw1LHw.mjs +132 -0
  61. package/dist/planner-ddl-builders-Dxvw1LHw.mjs.map +1 -0
  62. package/dist/planner-ddl-builders.d.mts +22 -0
  63. package/dist/planner-ddl-builders.d.mts.map +1 -0
  64. package/dist/planner-ddl-builders.mjs +3 -0
  65. package/dist/planner-identity-values-Dju-o5GF.mjs +91 -0
  66. package/dist/planner-identity-values-Dju-o5GF.mjs.map +1 -0
  67. package/dist/planner-identity-values.d.mts +20 -0
  68. package/dist/planner-identity-values.d.mts.map +1 -0
  69. package/dist/planner-identity-values.mjs +3 -0
  70. package/dist/planner-produced-postgres-migration-CRRTno6Z.d.mts +20 -0
  71. package/dist/planner-produced-postgres-migration-CRRTno6Z.d.mts.map +1 -0
  72. package/dist/planner-produced-postgres-migration-DSSPq8QS.mjs +33 -0
  73. package/dist/planner-produced-postgres-migration-DSSPq8QS.mjs.map +1 -0
  74. package/dist/planner-produced-postgres-migration.d.mts +5 -0
  75. package/dist/planner-produced-postgres-migration.mjs +3 -0
  76. package/dist/planner-schema-lookup-B7lkypwn.mjs +29 -0
  77. package/dist/planner-schema-lookup-B7lkypwn.mjs.map +1 -0
  78. package/dist/planner-schema-lookup.d.mts +22 -0
  79. package/dist/planner-schema-lookup.d.mts.map +1 -0
  80. package/dist/planner-schema-lookup.mjs +3 -0
  81. package/dist/planner-sql-checks-7jkgm9TX.mjs +241 -0
  82. package/dist/planner-sql-checks-7jkgm9TX.mjs.map +1 -0
  83. package/dist/planner-sql-checks.d.mts +55 -0
  84. package/dist/planner-sql-checks.d.mts.map +1 -0
  85. package/dist/planner-sql-checks.mjs +3 -0
  86. package/dist/{planner-target-details-MXb3oeul.d.mts → planner-target-details-DH-azLu-.d.mts} +1 -1
  87. package/dist/{planner-target-details-MXb3oeul.d.mts.map → planner-target-details-DH-azLu-.d.mts.map} +1 -1
  88. package/dist/planner-target-details.d.mts +2 -0
  89. package/dist/planner-target-details.mjs +1 -0
  90. package/dist/planner.d.mts +68 -0
  91. package/dist/planner.d.mts.map +1 -0
  92. package/dist/planner.mjs +4 -0
  93. package/dist/postgres-migration-BjA3Zmts.d.mts +50 -0
  94. package/dist/postgres-migration-BjA3Zmts.d.mts.map +1 -0
  95. package/dist/postgres-migration-qtmtbONe.mjs +52 -0
  96. package/dist/postgres-migration-qtmtbONe.mjs.map +1 -0
  97. package/dist/render-ops-D6_DHdOK.mjs +8 -0
  98. package/dist/render-ops-D6_DHdOK.mjs.map +1 -0
  99. package/dist/render-ops.d.mts +11 -0
  100. package/dist/render-ops.d.mts.map +1 -0
  101. package/dist/render-ops.mjs +3 -0
  102. package/dist/render-typescript-1rF_SB4g.mjs +85 -0
  103. package/dist/render-typescript-1rF_SB4g.mjs.map +1 -0
  104. package/dist/render-typescript.d.mts +15 -0
  105. package/dist/render-typescript.d.mts.map +1 -0
  106. package/dist/render-typescript.mjs +3 -0
  107. package/dist/runtime.d.mts +15 -3
  108. package/dist/runtime.d.mts.map +1 -1
  109. package/dist/runtime.mjs +10 -1
  110. package/dist/runtime.mjs.map +1 -1
  111. package/dist/shared-Bxkt8pNO.d.mts +41 -0
  112. package/dist/shared-Bxkt8pNO.d.mts.map +1 -0
  113. package/dist/sql-utils-r-Lw535w.mjs +76 -0
  114. package/dist/sql-utils-r-Lw535w.mjs.map +1 -0
  115. package/dist/sql-utils.d.mts +59 -0
  116. package/dist/sql-utils.d.mts.map +1 -0
  117. package/dist/sql-utils.mjs +3 -0
  118. package/dist/statement-builders-BPnmt6wx.mjs +116 -0
  119. package/dist/statement-builders-BPnmt6wx.mjs.map +1 -0
  120. package/dist/statement-builders.d.mts +23 -0
  121. package/dist/statement-builders.d.mts.map +1 -0
  122. package/dist/statement-builders.mjs +3 -0
  123. package/dist/tables-BmdW_FWO.mjs +477 -0
  124. package/dist/tables-BmdW_FWO.mjs.map +1 -0
  125. package/dist/types-ClK03Ojd.d.mts +10 -0
  126. package/dist/types-ClK03Ojd.d.mts.map +1 -0
  127. package/dist/types.d.mts +2 -0
  128. package/dist/types.mjs +1 -0
  129. package/package.json +37 -20
  130. package/src/core/codec-ids.ts +30 -0
  131. package/src/core/codecs.ts +645 -0
  132. package/src/core/default-normalizer.ts +131 -0
  133. package/src/core/descriptor-meta.ts +1 -1
  134. package/src/core/errors.ts +33 -0
  135. package/src/core/json-schema-type-expression.ts +131 -0
  136. package/src/core/migrations/op-factory-call.ts +1 -5
  137. package/src/core/migrations/operations/columns.ts +1 -1
  138. package/src/core/migrations/operations/constraints.ts +1 -1
  139. package/src/core/migrations/operations/data-transform.ts +27 -21
  140. package/src/core/migrations/operations/dependencies.ts +1 -1
  141. package/src/core/migrations/operations/enums.ts +1 -1
  142. package/src/core/migrations/operations/indexes.ts +1 -1
  143. package/src/core/migrations/operations/shared.ts +1 -1
  144. package/src/core/migrations/operations/tables.ts +1 -1
  145. package/src/core/migrations/planner-ddl-builders.ts +1 -1
  146. package/src/core/migrations/planner-recipes.ts +1 -1
  147. package/src/core/migrations/planner-sql-checks.ts +1 -1
  148. package/src/core/migrations/planner.ts +2 -4
  149. package/src/core/migrations/postgres-migration.ts +54 -1
  150. package/src/core/migrations/render-typescript.ts +22 -12
  151. package/src/core/migrations/runner.ts +2 -4
  152. package/src/core/native-type-normalizer.ts +49 -0
  153. package/src/core/sql-utils.ts +104 -0
  154. package/src/exports/codec-ids.ts +1 -0
  155. package/src/exports/codec-types.ts +51 -0
  156. package/src/exports/codecs.ts +2 -0
  157. package/src/exports/data-transform.ts +1 -0
  158. package/src/exports/default-normalizer.ts +1 -0
  159. package/src/exports/errors.ts +1 -0
  160. package/src/exports/issue-planner.ts +1 -0
  161. package/src/exports/migration.ts +6 -0
  162. package/src/exports/native-type-normalizer.ts +1 -0
  163. package/src/exports/op-factory-call.ts +25 -0
  164. package/src/exports/planner-ddl-builders.ts +8 -0
  165. package/src/exports/planner-identity-values.ts +1 -0
  166. package/src/exports/planner-produced-postgres-migration.ts +1 -0
  167. package/src/exports/planner-schema-lookup.ts +6 -0
  168. package/src/exports/planner-sql-checks.ts +11 -0
  169. package/src/exports/planner-target-details.ts +1 -0
  170. package/src/exports/planner.ts +1 -0
  171. package/src/exports/render-ops.ts +1 -0
  172. package/src/exports/render-typescript.ts +1 -0
  173. package/src/exports/runtime.ts +19 -4
  174. package/src/exports/sql-utils.ts +7 -0
  175. package/src/exports/statement-builders.ts +7 -0
  176. package/src/exports/types.ts +1 -0
  177. package/dist/postgres-migration-BsHJHV9O.mjs +0 -2793
  178. package/dist/postgres-migration-BsHJHV9O.mjs.map +0 -1
@@ -0,0 +1,879 @@
1
+ import { i as quoteIdentifier } from "./sql-utils-r-Lw535w.mjs";
2
+ import { a as columnNullabilityCheck, c as qualifyTableName, i as columnHasNoDefaultCheck, r as columnExistsCheck, t as buildExpectedFormatType, u as tableIsEmptyCheck } from "./planner-sql-checks-7jkgm9TX.mjs";
3
+ import { C as SetNotNullCall, S as SetDefaultCall, _ as DropIndexCall, a as AddUniqueCall, b as RawSqlCall, c as CreateExtensionCall, d as CreateTableCall, f as DataTransformCall, g as DropEnumTypeCall, h as DropDefaultCall, i as AddPrimaryKeyCall, l as CreateIndexCall, m as DropConstraintCall, n as AddEnumValuesCall, o as AlterColumnTypeCall, p as DropColumnCall, r as AddForeignKeyCall, s as CreateEnumTypeCall, t as AddColumnCall, u as CreateSchemaCall, v as DropNotNullCall, x as RenameTypeCall, y as DropTableCall } from "./op-factory-call-BKlruaiC.mjs";
4
+ import { n as buildColumnDefaultSql, r as buildColumnTypeSql, t as buildAddColumnSql } from "./planner-ddl-builders-Dxvw1LHw.mjs";
5
+ import { n as resolveIdentityValue } from "./planner-identity-values-Dju-o5GF.mjs";
6
+ import { i as hasUniqueConstraint, n as hasForeignKey, t as buildSchemaLookupMap } from "./planner-schema-lookup-B7lkypwn.mjs";
7
+ import { ifDefined } from "@prisma-next/utils/defined";
8
+ import { collectInitDependencies } from "@prisma-next/family-sql/control";
9
+ import { notOk, ok } from "@prisma-next/utils/result";
10
+
11
+ //#region src/core/migrations/planner-target-details.ts
12
+ function buildTargetDetails(objectType, name, schema, table) {
13
+ return {
14
+ schema,
15
+ objectType,
16
+ name,
17
+ ...ifDefined("table", table)
18
+ };
19
+ }
20
+
21
+ //#endregion
22
+ //#region src/core/migrations/planner-recipes.ts
23
+ function buildAddColumnOperationIdentity(schema, tableName, columnName) {
24
+ return {
25
+ id: `column.${tableName}.${columnName}`,
26
+ label: `Add column ${columnName} to ${tableName}`,
27
+ summary: `Adds column ${columnName} to table ${tableName}`,
28
+ target: {
29
+ id: "postgres",
30
+ details: buildTargetDetails("table", tableName, schema)
31
+ }
32
+ };
33
+ }
34
+ function buildAddNotNullColumnWithTemporaryDefaultOperation(options) {
35
+ const { schema, tableName, columnName, column, codecHooks, storageTypes, temporaryDefault } = options;
36
+ const qualified = qualifyTableName(schema, tableName);
37
+ return {
38
+ ...buildAddColumnOperationIdentity(schema, tableName, columnName),
39
+ operationClass: "additive",
40
+ precheck: [{
41
+ description: `ensure column "${columnName}" is missing`,
42
+ sql: columnExistsCheck({
43
+ schema,
44
+ table: tableName,
45
+ column: columnName,
46
+ exists: false
47
+ })
48
+ }],
49
+ execute: [{
50
+ description: `add column "${columnName}"`,
51
+ sql: buildAddColumnSql(qualified, columnName, column, codecHooks, temporaryDefault, storageTypes)
52
+ }, {
53
+ description: `drop temporary default from column "${columnName}"`,
54
+ sql: `ALTER TABLE ${qualified} ALTER COLUMN ${quoteIdentifier(columnName)} DROP DEFAULT`
55
+ }],
56
+ postcheck: [
57
+ {
58
+ description: `verify column "${columnName}" exists`,
59
+ sql: columnExistsCheck({
60
+ schema,
61
+ table: tableName,
62
+ column: columnName
63
+ })
64
+ },
65
+ {
66
+ description: `verify column "${columnName}" is NOT NULL`,
67
+ sql: columnNullabilityCheck({
68
+ schema,
69
+ table: tableName,
70
+ column: columnName,
71
+ nullable: false
72
+ })
73
+ },
74
+ {
75
+ description: `verify column "${columnName}" has no default after temporary default removal`,
76
+ sql: columnHasNoDefaultCheck({
77
+ schema,
78
+ table: tableName,
79
+ column: columnName
80
+ })
81
+ }
82
+ ]
83
+ };
84
+ }
85
+
86
+ //#endregion
87
+ //#region src/core/migrations/planner-strategies.ts
88
+ const REBUILD_SUFFIX = "__prisma_next_new";
89
+ function buildColumnSpec(table, column, ctx, overrides) {
90
+ const col = ctx.toContract.storage.tables[table]?.columns[column];
91
+ if (!col) throw new Error(`Column "${table}"."${column}" not found in destination contract`);
92
+ const mutableHooks = ctx.codecHooks;
93
+ const mutableTypes = ctx.storageTypes;
94
+ return {
95
+ name: column,
96
+ typeSql: buildColumnTypeSql(col, mutableHooks, mutableTypes),
97
+ defaultSql: buildColumnDefaultSql(col.default, col),
98
+ nullable: overrides?.nullable ?? col.nullable
99
+ };
100
+ }
101
+ function buildAlterTypeOptions(table, column, ctx, using) {
102
+ const col = ctx.toContract.storage.tables[table]?.columns[column];
103
+ if (!col) throw new Error(`Column "${table}"."${column}" not found in destination contract`);
104
+ const mutableHooks = ctx.codecHooks;
105
+ const mutableTypes = ctx.storageTypes;
106
+ const qualifiedTargetType = buildColumnTypeSql(col, mutableHooks, mutableTypes, false);
107
+ return {
108
+ qualifiedTargetType,
109
+ formatTypeExpected: buildExpectedFormatType(col, mutableHooks, mutableTypes),
110
+ rawTargetTypeForLabel: qualifiedTargetType,
111
+ ...using !== void 0 ? { using } : {}
112
+ };
113
+ }
114
+ const notNullBackfillCallStrategy = (issues, ctx) => {
115
+ if (!ctx.policy.allowedOperationClasses.includes("data")) return { kind: "no_match" };
116
+ const matched = [];
117
+ const calls = [];
118
+ for (const issue of issues) {
119
+ if (issue.kind !== "missing_column" || !issue.table || !issue.column) continue;
120
+ const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
121
+ if (!column) continue;
122
+ if (column.nullable === true || column.default !== void 0) continue;
123
+ matched.push(issue);
124
+ const spec = buildColumnSpec(issue.table, issue.column, ctx, { nullable: true });
125
+ calls.push(new AddColumnCall(ctx.schemaName, issue.table, spec), new DataTransformCall(`backfill-${issue.table}-${issue.column}`, `backfill-${issue.table}-${issue.column}:check`, `backfill-${issue.table}-${issue.column}:run`), new SetNotNullCall(ctx.schemaName, issue.table, issue.column));
126
+ }
127
+ if (matched.length === 0) return { kind: "no_match" };
128
+ return {
129
+ kind: "match",
130
+ issues: issues.filter((i) => !matched.includes(i)),
131
+ calls,
132
+ recipe: true
133
+ };
134
+ };
135
+ const SAFE_WIDENINGS = new Set([
136
+ "int2→int4",
137
+ "int2→int8",
138
+ "int4→int8",
139
+ "float4→float8"
140
+ ]);
141
+ const typeChangeCallStrategy = (issues, ctx) => {
142
+ const dataAllowed = ctx.policy.allowedOperationClasses.includes("data");
143
+ const matched = [];
144
+ const calls = [];
145
+ for (const issue of issues) {
146
+ if (issue.kind !== "type_mismatch") continue;
147
+ if (!issue.table || !issue.column) continue;
148
+ const fromColumn = ctx.fromContract?.storage.tables[issue.table]?.columns[issue.column];
149
+ const toColumn = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
150
+ if (!fromColumn || !toColumn) continue;
151
+ const fromType = fromColumn.nativeType;
152
+ const toType = toColumn.nativeType;
153
+ if (fromType === toType) continue;
154
+ const isSafeWidening = SAFE_WIDENINGS.has(`${fromType}→${toType}`);
155
+ if (!isSafeWidening && !dataAllowed) continue;
156
+ matched.push(issue);
157
+ const alterOpts = buildAlterTypeOptions(issue.table, issue.column, ctx);
158
+ if (isSafeWidening) calls.push(new AlterColumnTypeCall(ctx.schemaName, issue.table, issue.column, alterOpts));
159
+ else calls.push(new DataTransformCall(`typechange-${issue.table}-${issue.column}`, `typechange-${issue.table}-${issue.column}:check`, `typechange-${issue.table}-${issue.column}:run`), new AlterColumnTypeCall(ctx.schemaName, issue.table, issue.column, alterOpts));
160
+ }
161
+ if (matched.length === 0) return { kind: "no_match" };
162
+ return {
163
+ kind: "match",
164
+ issues: issues.filter((i) => !matched.includes(i)),
165
+ calls,
166
+ recipe: true
167
+ };
168
+ };
169
+ const nullableTighteningCallStrategy = (issues, ctx) => {
170
+ if (!ctx.policy.allowedOperationClasses.includes("data")) return { kind: "no_match" };
171
+ const matched = [];
172
+ const calls = [];
173
+ for (const issue of issues) {
174
+ if (issue.kind !== "nullability_mismatch" || !issue.table || !issue.column) continue;
175
+ const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
176
+ if (!column) continue;
177
+ if (column.nullable === true) continue;
178
+ matched.push(issue);
179
+ 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(ctx.schemaName, issue.table, issue.column));
180
+ }
181
+ if (matched.length === 0) return { kind: "no_match" };
182
+ return {
183
+ kind: "match",
184
+ issues: issues.filter((i) => !matched.includes(i)),
185
+ calls,
186
+ recipe: true
187
+ };
188
+ };
189
+ function enumRebuildCallRecipe(typeName, ctx) {
190
+ const toType = ctx.toContract.storage.types?.[typeName];
191
+ if (!toType) return [];
192
+ const nativeType = toType.nativeType;
193
+ const desiredValues = toType.typeParams["values"] ?? [];
194
+ const tempName = `${nativeType}${REBUILD_SUFFIX}`;
195
+ const columnRefs = [];
196
+ for (const [tableName, table] of Object.entries(ctx.toContract.storage.tables)) for (const [columnName, column] of Object.entries(table.columns)) if (column.typeRef === typeName) columnRefs.push({
197
+ table: tableName,
198
+ column: columnName
199
+ });
200
+ return [
201
+ new CreateEnumTypeCall(ctx.schemaName, tempName, desiredValues),
202
+ ...columnRefs.map((ref) => {
203
+ const using = `${ref.column}::text::${tempName}`;
204
+ return new AlterColumnTypeCall(ctx.schemaName, ref.table, ref.column, {
205
+ qualifiedTargetType: tempName,
206
+ formatTypeExpected: tempName,
207
+ rawTargetTypeForLabel: tempName,
208
+ using
209
+ });
210
+ }),
211
+ new DropEnumTypeCall(ctx.schemaName, nativeType),
212
+ new RenameTypeCall(ctx.schemaName, tempName, nativeType)
213
+ ];
214
+ }
215
+ const enumChangeCallStrategy = (issues, ctx) => {
216
+ if (!ctx.policy.allowedOperationClasses.includes("data")) return { kind: "no_match" };
217
+ const matched = [];
218
+ const calls = [];
219
+ for (const issue of issues) {
220
+ if (issue.kind !== "enum_values_changed") continue;
221
+ matched.push(issue);
222
+ if (issue.removedValues.length > 0) calls.push(new DataTransformCall(`migrate-${issue.typeName}-values`, `migrate-${issue.typeName}-values:check`, `migrate-${issue.typeName}-values:run`), ...enumRebuildCallRecipe(issue.typeName, ctx));
223
+ else if (issue.addedValues.length === 0) calls.push(...enumRebuildCallRecipe(issue.typeName, ctx));
224
+ else {
225
+ const toType = ctx.toContract.storage.types?.[issue.typeName];
226
+ if (toType) calls.push(new AddEnumValuesCall(ctx.schemaName, issue.typeName, toType.nativeType, issue.addedValues));
227
+ }
228
+ }
229
+ if (matched.length === 0) return { kind: "no_match" };
230
+ return {
231
+ kind: "match",
232
+ issues: issues.filter((i) => !matched.includes(i)),
233
+ calls,
234
+ recipe: true
235
+ };
236
+ };
237
+ /**
238
+ * Dispatches storage types through their codec's `planTypeOperations` hook.
239
+ * Replaces the walk-schema `buildStorageTypeOperations` path: the hook is
240
+ * the authoritative source for codec-driven DDL (enum create/rebuild/add-
241
+ * value, custom type creation, etc.).
242
+ *
243
+ * Runs after `enumChangeCallStrategy` so the structured enum path (value
244
+ * add, rebuild recipe) gets first pick at `enum_values_changed` issues;
245
+ * this strategy then handles remaining `type_missing` / `enum_values_changed`
246
+ * issues for types whose hook produced at least one op.
247
+ */
248
+ const storageTypePlanCallStrategy = (issues, ctx) => {
249
+ const storageTypes = ctx.toContract.storage.types ?? {};
250
+ if (Object.keys(storageTypes).length === 0) return { kind: "no_match" };
251
+ const calls = [];
252
+ const handledTypeNames = /* @__PURE__ */ new Set();
253
+ for (const [typeName, typeInstance] of Object.entries(storageTypes).sort(([a], [b]) => a.localeCompare(b))) {
254
+ const hook = ctx.codecHooks.get(typeInstance.codecId);
255
+ if (!hook?.planTypeOperations) continue;
256
+ const planResult = hook.planTypeOperations({
257
+ typeName,
258
+ typeInstance,
259
+ contract: ctx.toContract,
260
+ schema: ctx.schema,
261
+ schemaName: ctx.schemaName,
262
+ policy: ctx.policy
263
+ });
264
+ if (!planResult) continue;
265
+ if (planResult.operations.length === 0) {
266
+ handledTypeNames.add(typeName);
267
+ continue;
268
+ }
269
+ handledTypeNames.add(typeName);
270
+ for (const op of planResult.operations) calls.push(new RawSqlCall({
271
+ ...op,
272
+ target: {
273
+ id: op.target.id,
274
+ details: buildTargetDetails("type", typeName, ctx.schemaName)
275
+ }
276
+ }));
277
+ }
278
+ const remaining = issues.filter((issue) => !((issue.kind === "type_missing" || issue.kind === "enum_values_changed") && issue.typeName && handledTypeNames.has(issue.typeName)));
279
+ if (calls.length === 0 && remaining.length === issues.length) return { kind: "no_match" };
280
+ return {
281
+ kind: "match",
282
+ issues: remaining,
283
+ calls
284
+ };
285
+ };
286
+ /**
287
+ * Dispatches component-declared database dependencies. Replaces the
288
+ * walk-schema `buildDatabaseDependencyOperations` path. Rather than consuming
289
+ * `dependency_missing` issues (which only carry the id), this strategy
290
+ * re-invokes `collectInitDependencies(frameworkComponents)` at plan time so
291
+ * the handler has access to the structured `install` ops each component
292
+ * declared — including arbitrary SQL launders — and dedupes by dependency id
293
+ * plus per-op id.
294
+ */
295
+ const dependencyInstallCallStrategy = (issues, ctx) => {
296
+ const installedIds = new Set(ctx.schema.dependencies.map((d) => d.id));
297
+ const dependencies = sortDependencies(collectInitDependencies(ctx.frameworkComponents).filter(isPostgresPlannerDependency));
298
+ const calls = [];
299
+ const handledDependencyIds = /* @__PURE__ */ new Set();
300
+ const seenOperationIds = /* @__PURE__ */ new Set();
301
+ for (const dep of dependencies) {
302
+ handledDependencyIds.add(dep.id);
303
+ if (installedIds.has(dep.id)) continue;
304
+ for (const installOp of dep.install) {
305
+ if (seenOperationIds.has(installOp.id)) continue;
306
+ seenOperationIds.add(installOp.id);
307
+ calls.push(liftInstallOpToCall(installOp));
308
+ }
309
+ }
310
+ const remaining = issues.filter((issue) => issue.kind !== "dependency_missing");
311
+ if (calls.length === 0 && remaining.length === issues.length) return { kind: "no_match" };
312
+ return {
313
+ kind: "match",
314
+ issues: remaining,
315
+ calls
316
+ };
317
+ };
318
+ /**
319
+ * Handles `missing_column` issues for NOT NULL columns without a contract
320
+ * default. Replaces the walk-schema `buildAddColumnItem` non-default branches.
321
+ *
322
+ * Two shapes:
323
+ * - Shared-temp-default safe: emit a single atomic composite op (add
324
+ * nullable → backfill identity value → `SET NOT NULL` → `DROP DEFAULT`).
325
+ * - Empty-table guarded: emit a hand-built op with a `tableIsEmptyCheck`
326
+ * precheck so the failure message is "table is not empty" rather than the
327
+ * raw PG NOT NULL violation.
328
+ *
329
+ * "Normal" missing_column cases (nullable or has a contract default) are left
330
+ * for `mapIssueToCall`'s default `AddColumnCall` emission.
331
+ */
332
+ const notNullAddColumnCallStrategy = (issues, ctx) => {
333
+ const matched = [];
334
+ const calls = [];
335
+ const schemaLookups = buildSchemaLookupMap(ctx.schema);
336
+ const mutableCodecHooks = ctx.codecHooks;
337
+ const mutableStorageTypes = ctx.storageTypes;
338
+ for (const issue of issues) {
339
+ if (issue.kind !== "missing_column" || !issue.table || !issue.column) continue;
340
+ const contractTable = ctx.toContract.storage.tables[issue.table];
341
+ const column = contractTable?.columns[issue.column];
342
+ if (!column) continue;
343
+ const notNull = column.nullable !== true;
344
+ const hasDefault = column.default !== void 0;
345
+ if (!notNull || hasDefault) continue;
346
+ const schemaTable = ctx.schema.tables[issue.table];
347
+ if (!schemaTable) continue;
348
+ const temporaryDefault = resolveIdentityValue(column, mutableCodecHooks, mutableStorageTypes);
349
+ const schemaLookup = schemaLookups.get(issue.table);
350
+ const canUseSharedTempDefault = temporaryDefault !== null && canUseSharedTemporaryDefaultStrategy({
351
+ table: contractTable,
352
+ schemaTable,
353
+ schemaLookup,
354
+ columnName: issue.column
355
+ });
356
+ matched.push(issue);
357
+ if (canUseSharedTempDefault && temporaryDefault !== null) {
358
+ calls.push(new RawSqlCall(buildAddNotNullColumnWithTemporaryDefaultOperation({
359
+ schema: ctx.schemaName,
360
+ tableName: issue.table,
361
+ columnName: issue.column,
362
+ column,
363
+ codecHooks: mutableCodecHooks,
364
+ storageTypes: mutableStorageTypes,
365
+ temporaryDefault
366
+ })));
367
+ continue;
368
+ }
369
+ const qualified = qualifyTableName(ctx.schemaName, issue.table);
370
+ calls.push(new RawSqlCall({
371
+ ...buildAddColumnOperationIdentity(ctx.schemaName, issue.table, issue.column),
372
+ operationClass: "additive",
373
+ precheck: [{
374
+ description: `ensure column "${issue.column}" is missing`,
375
+ sql: columnExistsCheck({
376
+ schema: ctx.schemaName,
377
+ table: issue.table,
378
+ column: issue.column,
379
+ exists: false
380
+ })
381
+ }, {
382
+ description: `ensure table "${issue.table}" is empty before adding NOT NULL column without default`,
383
+ sql: tableIsEmptyCheck(qualified)
384
+ }],
385
+ execute: [{
386
+ description: `add column "${issue.column}"`,
387
+ sql: buildAddColumnSql(qualified, issue.column, column, mutableCodecHooks, void 0, mutableStorageTypes)
388
+ }],
389
+ postcheck: [{
390
+ description: `verify column "${issue.column}" exists`,
391
+ sql: columnExistsCheck({
392
+ schema: ctx.schemaName,
393
+ table: issue.table,
394
+ column: issue.column
395
+ })
396
+ }, {
397
+ description: `verify column "${issue.column}" is NOT NULL`,
398
+ sql: columnNullabilityCheck({
399
+ schema: ctx.schemaName,
400
+ table: issue.table,
401
+ column: issue.column,
402
+ nullable: false
403
+ })
404
+ }]
405
+ }));
406
+ }
407
+ if (matched.length === 0) return { kind: "no_match" };
408
+ return {
409
+ kind: "match",
410
+ issues: issues.filter((i) => !matched.includes(i)),
411
+ calls
412
+ };
413
+ };
414
+ function canUseSharedTemporaryDefaultStrategy(options) {
415
+ const { table, schemaTable, schemaLookup, columnName } = options;
416
+ if (table.primaryKey?.columns.includes(columnName) && !schemaTable.primaryKey) return false;
417
+ for (const unique of table.uniques) {
418
+ if (!unique.columns.includes(columnName)) continue;
419
+ if (!schemaLookup || !hasUniqueConstraint(schemaLookup, unique.columns)) return false;
420
+ }
421
+ for (const foreignKey of table.foreignKeys) {
422
+ if (foreignKey.constraint === false || !foreignKey.columns.includes(columnName)) continue;
423
+ if (!schemaLookup || !hasForeignKey(schemaLookup, foreignKey)) return false;
424
+ }
425
+ return true;
426
+ }
427
+ function isPostgresPlannerDependency(dependency) {
428
+ return dependency.install.every((operation) => operation.target.id === "postgres");
429
+ }
430
+ function sortDependencies(dependencies) {
431
+ return [...dependencies].sort((a, b) => a.id.localeCompare(b.id));
432
+ }
433
+ /**
434
+ * Lift a component install op into migration IR. Structured shapes — extension
435
+ * and schema installs with predictable SQL — collapse to typed `*Call`
436
+ * subclasses so the scaffolded migration authoring surface stays readable.
437
+ * Everything else (arbitrary SQL) falls through to `RawSqlCall` as an escape
438
+ * hatch.
439
+ */
440
+ /**
441
+ * Component-declared install ops are wrapped as `RawSqlCall` so the
442
+ * component's original `label`, `precheck`, `execute`, `postcheck`, and op
443
+ * id are preserved verbatim. Structured conversion (to e.g.
444
+ * `CreateExtensionCall`) would drop the precheck/postcheck pair and
445
+ * change the DDL label, breaking walk-schema output parity. Classification
446
+ * as `'dep'` happens in `classifyCall` via the underlying op's id prefix.
447
+ */
448
+ function liftInstallOpToCall(op) {
449
+ return new RawSqlCall(op);
450
+ }
451
+ /**
452
+ * Ordered list of Postgres planner strategies, shared by `migration plan`
453
+ * and `db update` / `db init`. The issue planner runs each strategy in
454
+ * order, letting it consume any issues it handles, and routes whatever's
455
+ * left through `mapIssueToCall`. Behavior diverges purely on
456
+ * `policy.allowedOperationClasses`:
457
+ *
458
+ * - When `'data'` is allowed (`migration plan`), the data-safe strategies
459
+ * (`enumChangeCallStrategy`, `notNullBackfillCallStrategy`,
460
+ * `typeChangeCallStrategy`, `nullableTighteningCallStrategy`) consume their
461
+ * matching issues and emit `DataTransformCall` placeholders or recipe ops.
462
+ *
463
+ * - When `'data'` is not allowed (`db update` / `db init`), each data-safe
464
+ * strategy short-circuits to `no_match`, leaving the issue for the
465
+ * downstream walk-schema strategies (`storageTypePlanCallStrategy`,
466
+ * `dependencyInstallCallStrategy`, `notNullAddColumnCallStrategy`) or the
467
+ * `mapIssueToCall` default to handle with direct DDL.
468
+ *
469
+ * Order matters: data-safe strategies must run before the walk-schema
470
+ * strategies on overlapping issue kinds (e.g. `enum_values_changed`,
471
+ * `missing_column` for NOT NULL) so they take priority when active.
472
+ */
473
+ const postgresPlannerStrategies = [
474
+ enumChangeCallStrategy,
475
+ notNullBackfillCallStrategy,
476
+ typeChangeCallStrategy,
477
+ nullableTighteningCallStrategy,
478
+ storageTypePlanCallStrategy,
479
+ dependencyInstallCallStrategy,
480
+ notNullAddColumnCallStrategy
481
+ ];
482
+
483
+ //#endregion
484
+ //#region src/core/migrations/issue-planner.ts
485
+ const ISSUE_KIND_ORDER = {
486
+ dependency_missing: 1,
487
+ type_missing: 2,
488
+ type_values_mismatch: 3,
489
+ enum_values_changed: 3,
490
+ extra_foreign_key: 10,
491
+ extra_unique_constraint: 11,
492
+ extra_primary_key: 12,
493
+ extra_index: 13,
494
+ extra_default: 14,
495
+ extra_column: 15,
496
+ extra_table: 16,
497
+ missing_table: 20,
498
+ missing_column: 30,
499
+ type_mismatch: 40,
500
+ nullability_mismatch: 41,
501
+ default_missing: 42,
502
+ default_mismatch: 43,
503
+ primary_key_mismatch: 50,
504
+ unique_constraint_mismatch: 51,
505
+ index_mismatch: 52,
506
+ foreign_key_mismatch: 60
507
+ };
508
+ function issueOrder(issue) {
509
+ return ISSUE_KIND_ORDER[issue.kind] ?? 99;
510
+ }
511
+ function issueConflict(kind, summary, location) {
512
+ return {
513
+ kind,
514
+ summary,
515
+ why: "Use `migration new` to author a custom migration for this change.",
516
+ ...location ? { location } : {}
517
+ };
518
+ }
519
+ function isMissing(issue) {
520
+ if (issue.kind === "enum_values_changed") return false;
521
+ return issue.actual === void 0;
522
+ }
523
+ function toColumnSpec(name, column, codecHooks, storageTypes) {
524
+ return {
525
+ name,
526
+ typeSql: buildColumnTypeSql(column, codecHooks, storageTypes),
527
+ defaultSql: buildColumnDefaultSql(column.default, column),
528
+ nullable: column.nullable
529
+ };
530
+ }
531
+ function mapIssueToCall(issue, ctx) {
532
+ const { schemaName, codecHooks, storageTypes } = ctx;
533
+ switch (issue.kind) {
534
+ case "missing_table": {
535
+ if (!issue.table) return notOk(issueConflict("unsupportedOperation", "Missing table issue has no table name"));
536
+ const contractTable = ctx.toContract.storage.tables[issue.table];
537
+ if (!contractTable) return notOk(issueConflict("unsupportedOperation", `Table "${issue.table}" reported missing but not found in destination contract`));
538
+ const columns = Object.entries(contractTable.columns).map(([name, column]) => toColumnSpec(name, column, codecHooks, storageTypes));
539
+ const primaryKey = contractTable.primaryKey ? { columns: contractTable.primaryKey.columns } : void 0;
540
+ const calls = [new CreateTableCall(schemaName, issue.table, columns, primaryKey)];
541
+ for (const index of contractTable.indexes) {
542
+ const indexName = index.name ?? `${issue.table}_${index.columns.join("_")}_idx`;
543
+ calls.push(new CreateIndexCall(schemaName, issue.table, indexName, [...index.columns]));
544
+ }
545
+ const explicitIndexColumnSets = new Set(contractTable.indexes.map((idx) => idx.columns.join(",")));
546
+ for (const fk of contractTable.foreignKeys) {
547
+ if (fk.constraint) {
548
+ const fkSpec = {
549
+ name: fk.name ?? `${issue.table}_${fk.columns.join("_")}_fkey`,
550
+ columns: fk.columns,
551
+ references: {
552
+ table: fk.references.table,
553
+ columns: fk.references.columns
554
+ },
555
+ ...fk.onDelete !== void 0 && { onDelete: fk.onDelete },
556
+ ...fk.onUpdate !== void 0 && { onUpdate: fk.onUpdate }
557
+ };
558
+ calls.push(new AddForeignKeyCall(schemaName, issue.table, fkSpec));
559
+ }
560
+ if (fk.index && !explicitIndexColumnSets.has(fk.columns.join(","))) {
561
+ const indexName = `${issue.table}_${fk.columns.join("_")}_idx`;
562
+ calls.push(new CreateIndexCall(schemaName, issue.table, indexName, [...fk.columns]));
563
+ }
564
+ }
565
+ for (const unique of contractTable.uniques) {
566
+ const constraintName = unique.name ?? `${issue.table}_${unique.columns.join("_")}_key`;
567
+ calls.push(new AddUniqueCall(schemaName, issue.table, constraintName, [...unique.columns]));
568
+ }
569
+ return ok(calls);
570
+ }
571
+ case "missing_column":
572
+ if (!issue.table || !issue.column) return notOk(issueConflict("unsupportedOperation", "Missing column issue has no table/column name"));
573
+ {
574
+ const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
575
+ if (!column) return notOk(issueConflict("unsupportedOperation", `Column "${issue.table}"."${issue.column}" not in destination contract`));
576
+ return ok([new AddColumnCall(schemaName, issue.table, toColumnSpec(issue.column, column, codecHooks, storageTypes))]);
577
+ }
578
+ case "default_missing":
579
+ if (!issue.table || !issue.column) return notOk(issueConflict("unsupportedOperation", "Default missing issue has no table/column name"));
580
+ {
581
+ const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
582
+ if (!column?.default) return notOk(issueConflict("unsupportedOperation", `Column "${issue.table}"."${issue.column}" has no default in contract`));
583
+ const defaultSql = buildColumnDefaultSql(column.default, column);
584
+ if (!defaultSql) return ok([]);
585
+ return ok([new SetDefaultCall(schemaName, issue.table, issue.column, defaultSql)]);
586
+ }
587
+ case "extra_table":
588
+ if (!issue.table) return notOk(issueConflict("unsupportedOperation", "Extra table issue has no table name"));
589
+ return ok([new DropTableCall(schemaName, issue.table)]);
590
+ case "extra_column":
591
+ if (!issue.table || !issue.column) return notOk(issueConflict("unsupportedOperation", "Extra column issue has no table/column name"));
592
+ return ok([new DropColumnCall(schemaName, issue.table, issue.column)]);
593
+ case "extra_index":
594
+ if (!issue.table || !issue.indexOrConstraint) return notOk(issueConflict("unsupportedOperation", "Extra index issue has no table/index name"));
595
+ return ok([new DropIndexCall(schemaName, issue.table, issue.indexOrConstraint)]);
596
+ case "extra_unique_constraint":
597
+ case "extra_foreign_key":
598
+ case "extra_primary_key": {
599
+ if (!issue.table) return notOk(issueConflict("unsupportedOperation", "Extra constraint issue has no table/constraint name"));
600
+ const constraintName = issue.indexOrConstraint ?? (issue.kind === "extra_primary_key" ? `${issue.table}_pkey` : void 0);
601
+ if (!constraintName) return notOk(issueConflict("unsupportedOperation", "Extra constraint issue has no table/constraint name"));
602
+ return ok([new DropConstraintCall(schemaName, issue.table, constraintName, {
603
+ extra_unique_constraint: "unique",
604
+ extra_foreign_key: "foreignKey",
605
+ extra_primary_key: "primaryKey"
606
+ }[issue.kind])]);
607
+ }
608
+ case "extra_default":
609
+ if (!issue.table || !issue.column) return notOk(issueConflict("unsupportedOperation", "Extra default issue has no table/column name"));
610
+ return ok([new DropDefaultCall(schemaName, issue.table, issue.column)]);
611
+ case "nullability_mismatch": {
612
+ if (!issue.table || !issue.column) return notOk(issueConflict("nullabilityConflict", "Nullability mismatch has no table/column name"));
613
+ const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
614
+ if (!column) return notOk(issueConflict("nullabilityConflict", `Column "${issue.table}"."${issue.column}" not found in destination contract`));
615
+ return ok(column.nullable ? [new DropNotNullCall(schemaName, issue.table, issue.column)] : [new SetNotNullCall(schemaName, issue.table, issue.column)]);
616
+ }
617
+ case "type_mismatch":
618
+ if (!issue.table || !issue.column) return notOk(issueConflict("typeMismatch", "Type mismatch has no table/column name"));
619
+ {
620
+ const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
621
+ if (!column) return notOk(issueConflict("typeMismatch", `Column "${issue.table}"."${issue.column}" not in destination contract`));
622
+ const hooksMap = codecHooks;
623
+ const typesMap = storageTypes;
624
+ const qualifiedTargetType = buildColumnTypeSql(column, hooksMap, typesMap, false);
625
+ const formatTypeExpected = buildExpectedFormatType(column, hooksMap, typesMap);
626
+ return ok([new AlterColumnTypeCall(schemaName, issue.table, issue.column, {
627
+ qualifiedTargetType,
628
+ formatTypeExpected,
629
+ rawTargetTypeForLabel: qualifiedTargetType
630
+ })]);
631
+ }
632
+ case "default_mismatch":
633
+ if (!issue.table || !issue.column) return notOk(issueConflict("unsupportedOperation", "Default mismatch has no table/column name"));
634
+ {
635
+ const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
636
+ if (!column?.default) return ok([]);
637
+ const defaultSql = buildColumnDefaultSql(column.default, column);
638
+ if (!defaultSql) return ok([]);
639
+ return ok([new SetDefaultCall(schemaName, issue.table, issue.column, defaultSql, "widening")]);
640
+ }
641
+ case "primary_key_mismatch":
642
+ if (!issue.table) return notOk(issueConflict("indexIncompatible", "Primary key issue has no table name"));
643
+ if (isMissing(issue)) {
644
+ const pk = ctx.toContract.storage.tables[issue.table]?.primaryKey;
645
+ if (!pk) return notOk(issueConflict("indexIncompatible", `No primary key in contract for "${issue.table}"`));
646
+ const constraintName = pk.name ?? `${issue.table}_pkey`;
647
+ return ok([new AddPrimaryKeyCall(schemaName, issue.table, constraintName, pk.columns)]);
648
+ }
649
+ return notOk(issueConflict("indexIncompatible", `Primary key on "${issue.table}" has different columns (expected: ${issue.expected}, actual: ${issue.actual})`, { table: issue.table }));
650
+ case "unique_constraint_mismatch":
651
+ if (!issue.table) return notOk(issueConflict("indexIncompatible", "Unique constraint issue has no table name"));
652
+ if (isMissing(issue) && issue.expected) {
653
+ const columns = issue.expected.split(", ");
654
+ const constraintName = `${issue.table}_${columns.join("_")}_key`;
655
+ return ok([new AddUniqueCall(schemaName, issue.table, constraintName, columns)]);
656
+ }
657
+ return notOk(issueConflict("indexIncompatible", `Unique constraint on "${issue.table}" differs (expected: ${issue.expected}, actual: ${issue.actual})`, { table: issue.table }));
658
+ case "index_mismatch":
659
+ if (!issue.table) return notOk(issueConflict("indexIncompatible", "Index issue has no table name"));
660
+ if (isMissing(issue) && issue.expected) {
661
+ const columns = issue.expected.split(", ");
662
+ const indexName = `${issue.table}_${columns.join("_")}_idx`;
663
+ return ok([new CreateIndexCall(schemaName, issue.table, indexName, columns)]);
664
+ }
665
+ return notOk(issueConflict("indexIncompatible", `Index on "${issue.table}" differs (expected: ${issue.expected}, actual: ${issue.actual})`, { table: issue.table }));
666
+ case "foreign_key_mismatch":
667
+ if (!issue.table) return notOk(issueConflict("foreignKeyConflict", "Foreign key issue has no table name"));
668
+ if (isMissing(issue) && issue.expected) {
669
+ const arrowIdx = issue.expected.indexOf(" -> ");
670
+ if (arrowIdx >= 0) {
671
+ const columns = issue.expected.slice(0, arrowIdx).split(", ");
672
+ const fkName = `${issue.table}_${columns.join("_")}_fkey`;
673
+ const fk = ctx.toContract.storage.tables[issue.table]?.foreignKeys.find((k) => k.columns.join(", ") === columns.join(", "));
674
+ if (fk) {
675
+ const fkSpec = {
676
+ name: fkName,
677
+ columns: fk.columns,
678
+ references: {
679
+ table: fk.references.table,
680
+ columns: fk.references.columns
681
+ },
682
+ ...fk.onDelete !== void 0 && { onDelete: fk.onDelete },
683
+ ...fk.onUpdate !== void 0 && { onUpdate: fk.onUpdate }
684
+ };
685
+ return ok([new AddForeignKeyCall(schemaName, issue.table, fkSpec)]);
686
+ }
687
+ return notOk(issueConflict("foreignKeyConflict", `Foreign key on "${issue.table}" (${columns.join(", ")}) not found in destination contract`, { table: issue.table }));
688
+ }
689
+ }
690
+ return notOk(issueConflict("foreignKeyConflict", `Foreign key on "${issue.table}" differs (expected: ${issue.expected}, actual: ${issue.actual})`, { table: issue.table }));
691
+ case "type_missing": {
692
+ if (!issue.typeName) return notOk(issueConflict("unsupportedOperation", "Type missing issue has no typeName"));
693
+ const typeInstance = ctx.toContract.storage.types?.[issue.typeName];
694
+ if (!typeInstance) return notOk(issueConflict("unsupportedOperation", `Type "${issue.typeName}" reported missing but not found in destination contract`));
695
+ if (typeInstance.codecId.startsWith("pg/enum")) {
696
+ const values = typeInstance.typeParams["values"] ?? [];
697
+ return ok([new CreateEnumTypeCall(schemaName, typeInstance.nativeType, values)]);
698
+ }
699
+ return notOk(issueConflict("unsupportedOperation", `Type "${issue.typeName}" uses codec "${typeInstance.codecId}" — only enum types are supported`));
700
+ }
701
+ case "type_values_mismatch": return notOk(issueConflict("unsupportedOperation", `Type "${issue.typeName ?? "unknown"}" values differ — type alteration not yet supported`));
702
+ case "dependency_missing":
703
+ if (!issue.dependencyId) return notOk(issueConflict("unsupportedOperation", "Dependency missing issue has no dependencyId"));
704
+ if (issue.dependencyId.startsWith("ext:")) return ok([new CreateExtensionCall(issue.dependencyId.slice(4))]);
705
+ if (issue.dependencyId.startsWith("schema:")) return ok([new CreateSchemaCall(issue.dependencyId.slice(7))]);
706
+ return notOk(issueConflict("unsupportedOperation", `Unknown dependency type: ${issue.dependencyId}`));
707
+ default: return notOk(issueConflict("unsupportedOperation", `Unhandled issue kind: ${issue.kind}`));
708
+ }
709
+ }
710
+ /**
711
+ * Classifies calls into DDL sequencing buckets. The order matches the
712
+ * legacy walk-schema planner's emission order so `db init` and `db update`
713
+ * produce byte-identical plans for the shared shape (deps → drops → tables
714
+ * → columns → alters → PKs → uniques → indexes → FKs).
715
+ */
716
+ function classifyCall(call) {
717
+ switch (call.factoryName) {
718
+ case "createExtension":
719
+ case "createSchema":
720
+ case "createEnumType":
721
+ case "addEnumValues":
722
+ case "dropEnumType":
723
+ case "renameType": return "dep";
724
+ case "dropTable":
725
+ case "dropColumn":
726
+ case "dropConstraint":
727
+ case "dropIndex":
728
+ case "dropDefault": return "drop";
729
+ case "createTable": return "table";
730
+ case "addColumn": return "column";
731
+ case "alterColumnType":
732
+ case "setNotNull":
733
+ case "dropNotNull":
734
+ case "setDefault": return "alter";
735
+ case "addPrimaryKey": return "primaryKey";
736
+ case "addUnique": return "unique";
737
+ case "createIndex": return "index";
738
+ case "addForeignKey": return "foreignKey";
739
+ case "rawSql": {
740
+ const op = call.op;
741
+ if (op?.target?.details?.objectType === "type") return "dep";
742
+ const id = typeof op?.id === "string" ? op.id : "";
743
+ if (id.startsWith("extension.") || id.startsWith("schema.")) return "dep";
744
+ return "alter";
745
+ }
746
+ default: return "alter";
747
+ }
748
+ }
749
+ /** Stable lexical key used to order issues within the same kind bucket. */
750
+ function issueKey(issue) {
751
+ 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 : ""}`;
752
+ }
753
+ const DEFAULT_POLICY = { allowedOperationClasses: [
754
+ "additive",
755
+ "widening",
756
+ "destructive",
757
+ "data"
758
+ ] };
759
+ function emptySchemaIR() {
760
+ return {
761
+ tables: {},
762
+ dependencies: []
763
+ };
764
+ }
765
+ function conflictKindForCall(call) {
766
+ switch (call.factoryName) {
767
+ case "alterColumnType": return "typeMismatch";
768
+ case "setNotNull":
769
+ case "dropNotNull": return "nullabilityConflict";
770
+ case "addForeignKey":
771
+ case "dropConstraint": return "foreignKeyConflict";
772
+ case "createIndex":
773
+ case "dropIndex": return "indexIncompatible";
774
+ default: return "missingButNonAdditive";
775
+ }
776
+ }
777
+ function locationForCall(call) {
778
+ const anyCall = call;
779
+ const location = {};
780
+ if (anyCall.tableName) location.table = anyCall.tableName;
781
+ if (anyCall.columnName) location.column = anyCall.columnName;
782
+ if (anyCall.indexName) location.index = anyCall.indexName;
783
+ if (anyCall.constraintName) location.constraint = anyCall.constraintName;
784
+ if (anyCall.typeName) location.type = anyCall.typeName;
785
+ return Object.keys(location).length > 0 ? location : void 0;
786
+ }
787
+ function conflictForDisallowedCall(call, allowed) {
788
+ const summary = `Operation "${call.label}" requires class "${call.operationClass}", but policy allows only: ${allowed.join(", ")}`;
789
+ const location = locationForCall(call);
790
+ return {
791
+ kind: conflictKindForCall(call),
792
+ summary,
793
+ why: "Use `migration new` to author a custom migration for this change.",
794
+ ...location ? { location } : {}
795
+ };
796
+ }
797
+ function planIssues(options) {
798
+ const policyProvided = options.policy !== void 0;
799
+ const policy = options.policy ?? DEFAULT_POLICY;
800
+ const schema = options.schema ?? emptySchemaIR();
801
+ const frameworkComponents = options.frameworkComponents ?? [];
802
+ const context = {
803
+ toContract: options.toContract,
804
+ fromContract: options.fromContract,
805
+ schemaName: options.schemaName,
806
+ codecHooks: options.codecHooks,
807
+ storageTypes: options.storageTypes,
808
+ schema,
809
+ policy,
810
+ frameworkComponents
811
+ };
812
+ const strategies = options.strategies ?? postgresPlannerStrategies;
813
+ let remaining = options.issues;
814
+ const recipeCalls = [];
815
+ const bucketablePatternCalls = [];
816
+ for (const strategy of strategies) {
817
+ const result = strategy(remaining, context);
818
+ if (result.kind === "match") {
819
+ remaining = result.issues;
820
+ if (result.recipe) recipeCalls.push(...result.calls);
821
+ else bucketablePatternCalls.push(...result.calls);
822
+ }
823
+ }
824
+ const sorted = [...remaining].sort((a, b) => {
825
+ const kindDelta = issueOrder(a) - issueOrder(b);
826
+ if (kindDelta !== 0) return kindDelta;
827
+ const keyA = issueKey(a);
828
+ const keyB = issueKey(b);
829
+ return keyA < keyB ? -1 : keyA > keyB ? 1 : 0;
830
+ });
831
+ const defaultCalls = [];
832
+ const conflicts = [];
833
+ for (const issue of sorted) {
834
+ const result = mapIssueToCall(issue, context);
835
+ if (result.ok) defaultCalls.push(...result.value);
836
+ else conflicts.push(result.failure);
837
+ }
838
+ const allowed = policy.allowedOperationClasses;
839
+ let gatedDefault = defaultCalls;
840
+ let gatedRecipe = recipeCalls;
841
+ let gatedBucketable = bucketablePatternCalls;
842
+ if (policyProvided) {
843
+ const keepIfAllowed = (bucket) => (call) => {
844
+ if (allowed.includes(call.operationClass)) {
845
+ bucket.push(call);
846
+ return;
847
+ }
848
+ conflicts.push(conflictForDisallowedCall(call, allowed));
849
+ };
850
+ const gatedDefaultBucket = [];
851
+ const gatedRecipeBucket = [];
852
+ const gatedBucketableBucket = [];
853
+ defaultCalls.forEach(keepIfAllowed(gatedDefaultBucket));
854
+ recipeCalls.forEach(keepIfAllowed(gatedRecipeBucket));
855
+ bucketablePatternCalls.forEach(keepIfAllowed(gatedBucketableBucket));
856
+ gatedDefault = gatedDefaultBucket;
857
+ gatedRecipe = gatedRecipeBucket;
858
+ gatedBucketable = gatedBucketableBucket;
859
+ }
860
+ if (conflicts.length > 0) return notOk(conflicts);
861
+ const combinedBucketable = [...gatedDefault, ...gatedBucketable];
862
+ const byCategory = (cat) => combinedBucketable.filter((c) => classifyCall(c) === cat);
863
+ return ok({ calls: [
864
+ ...byCategory("dep"),
865
+ ...byCategory("drop"),
866
+ ...byCategory("table"),
867
+ ...byCategory("column"),
868
+ ...gatedRecipe,
869
+ ...byCategory("alter"),
870
+ ...byCategory("primaryKey"),
871
+ ...byCategory("unique"),
872
+ ...byCategory("index"),
873
+ ...byCategory("foreignKey")
874
+ ] });
875
+ }
876
+
877
+ //#endregion
878
+ export { postgresPlannerStrategies as n, planIssues as t };
879
+ //# sourceMappingURL=issue-planner-CFjB0_oO.mjs.map