@prisma-next/target-postgres 0.4.1 → 0.4.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.
- package/dist/codec-ids-CojIXVf9.mjs +29 -0
- package/dist/codec-ids-CojIXVf9.mjs.map +1 -0
- package/dist/codec-ids.d.mts +28 -0
- package/dist/codec-ids.d.mts.map +1 -0
- package/dist/codec-ids.mjs +3 -0
- package/dist/codec-types.d.mts +42 -0
- package/dist/codec-types.d.mts.map +1 -0
- package/dist/codec-types.mjs +3 -0
- package/dist/codecs-BoahtY_Q.mjs +385 -0
- package/dist/codecs-BoahtY_Q.mjs.map +1 -0
- package/dist/codecs-D-F2KJqt.d.mts +299 -0
- package/dist/codecs-D-F2KJqt.d.mts.map +1 -0
- package/dist/codecs.d.mts +2 -0
- package/dist/codecs.mjs +3 -0
- package/dist/control.d.mts +1 -1
- package/dist/control.mjs +9 -1982
- package/dist/control.mjs.map +1 -1
- package/dist/data-transform-CxFRBIUp.d.mts +32 -0
- package/dist/data-transform-CxFRBIUp.d.mts.map +1 -0
- package/dist/data-transform-VfEGzXWt.mjs +39 -0
- package/dist/data-transform-VfEGzXWt.mjs.map +1 -0
- package/dist/data-transform.d.mts +2 -0
- package/dist/data-transform.mjs +3 -0
- package/dist/default-normalizer-DNOpRoOF.mjs +131 -0
- package/dist/default-normalizer-DNOpRoOF.mjs.map +1 -0
- package/dist/default-normalizer.d.mts +19 -0
- package/dist/default-normalizer.d.mts.map +1 -0
- package/dist/default-normalizer.mjs +3 -0
- package/dist/{descriptor-meta-DkvCmY98.mjs → descriptor-meta-BVoVtyp-.mjs} +1 -1
- package/dist/{descriptor-meta-DkvCmY98.mjs.map → descriptor-meta-BVoVtyp-.mjs.map} +1 -1
- package/dist/errors-AFvEPZ1R.mjs +34 -0
- package/dist/errors-AFvEPZ1R.mjs.map +1 -0
- package/dist/errors.d.mts +27 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +3 -0
- package/dist/issue-planner-CFjB0_oO.mjs +879 -0
- package/dist/issue-planner-CFjB0_oO.mjs.map +1 -0
- package/dist/issue-planner.d.mts +85 -0
- package/dist/issue-planner.d.mts.map +1 -0
- package/dist/issue-planner.mjs +3 -0
- package/dist/migration.d.mts +5 -79
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +6 -428
- package/dist/migration.mjs.map +1 -1
- package/dist/native-type-normalizer-CInai_oY.mjs +38 -0
- package/dist/native-type-normalizer-CInai_oY.mjs.map +1 -0
- package/dist/native-type-normalizer.d.mts +18 -0
- package/dist/native-type-normalizer.d.mts.map +1 -0
- package/dist/native-type-normalizer.mjs +3 -0
- package/dist/op-factory-call-BKlruaiC.mjs +605 -0
- package/dist/op-factory-call-BKlruaiC.mjs.map +1 -0
- package/dist/op-factory-call-C3bWXKSP.d.mts +304 -0
- package/dist/op-factory-call-C3bWXKSP.d.mts.map +1 -0
- package/dist/op-factory-call.d.mts +3 -0
- package/dist/op-factory-call.mjs +3 -0
- package/dist/pack.d.mts +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/planner-CLUvVhUN.mjs +98 -0
- package/dist/planner-CLUvVhUN.mjs.map +1 -0
- package/dist/planner-ddl-builders-Dxvw1LHw.mjs +132 -0
- package/dist/planner-ddl-builders-Dxvw1LHw.mjs.map +1 -0
- package/dist/planner-ddl-builders.d.mts +22 -0
- package/dist/planner-ddl-builders.d.mts.map +1 -0
- package/dist/planner-ddl-builders.mjs +3 -0
- package/dist/planner-identity-values-Dju-o5GF.mjs +91 -0
- package/dist/planner-identity-values-Dju-o5GF.mjs.map +1 -0
- package/dist/planner-identity-values.d.mts +20 -0
- package/dist/planner-identity-values.d.mts.map +1 -0
- package/dist/planner-identity-values.mjs +3 -0
- package/dist/planner-produced-postgres-migration-CRRTno6Z.d.mts +20 -0
- package/dist/planner-produced-postgres-migration-CRRTno6Z.d.mts.map +1 -0
- package/dist/planner-produced-postgres-migration-DSSPq8QS.mjs +33 -0
- package/dist/planner-produced-postgres-migration-DSSPq8QS.mjs.map +1 -0
- package/dist/planner-produced-postgres-migration.d.mts +5 -0
- package/dist/planner-produced-postgres-migration.mjs +3 -0
- package/dist/planner-schema-lookup-B7lkypwn.mjs +29 -0
- package/dist/planner-schema-lookup-B7lkypwn.mjs.map +1 -0
- package/dist/planner-schema-lookup.d.mts +22 -0
- package/dist/planner-schema-lookup.d.mts.map +1 -0
- package/dist/planner-schema-lookup.mjs +3 -0
- package/dist/planner-sql-checks-7jkgm9TX.mjs +241 -0
- package/dist/planner-sql-checks-7jkgm9TX.mjs.map +1 -0
- package/dist/planner-sql-checks.d.mts +55 -0
- package/dist/planner-sql-checks.d.mts.map +1 -0
- package/dist/planner-sql-checks.mjs +3 -0
- package/dist/{planner-target-details-MXb3oeul.d.mts → planner-target-details-DH-azLu-.d.mts} +1 -1
- package/dist/{planner-target-details-MXb3oeul.d.mts.map → planner-target-details-DH-azLu-.d.mts.map} +1 -1
- package/dist/planner-target-details.d.mts +2 -0
- package/dist/planner-target-details.mjs +1 -0
- package/dist/planner.d.mts +68 -0
- package/dist/planner.d.mts.map +1 -0
- package/dist/planner.mjs +4 -0
- package/dist/postgres-migration-BjA3Zmts.d.mts +50 -0
- package/dist/postgres-migration-BjA3Zmts.d.mts.map +1 -0
- package/dist/postgres-migration-qtmtbONe.mjs +52 -0
- package/dist/postgres-migration-qtmtbONe.mjs.map +1 -0
- package/dist/render-ops-D6_DHdOK.mjs +8 -0
- package/dist/render-ops-D6_DHdOK.mjs.map +1 -0
- package/dist/render-ops.d.mts +11 -0
- package/dist/render-ops.d.mts.map +1 -0
- package/dist/render-ops.mjs +3 -0
- package/dist/render-typescript-1rF_SB4g.mjs +85 -0
- package/dist/render-typescript-1rF_SB4g.mjs.map +1 -0
- package/dist/render-typescript.d.mts +15 -0
- package/dist/render-typescript.d.mts.map +1 -0
- package/dist/render-typescript.mjs +3 -0
- package/dist/runtime.d.mts +15 -3
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +10 -1
- package/dist/runtime.mjs.map +1 -1
- package/dist/shared-Bxkt8pNO.d.mts +41 -0
- package/dist/shared-Bxkt8pNO.d.mts.map +1 -0
- package/dist/sql-utils-r-Lw535w.mjs +76 -0
- package/dist/sql-utils-r-Lw535w.mjs.map +1 -0
- package/dist/sql-utils.d.mts +59 -0
- package/dist/sql-utils.d.mts.map +1 -0
- package/dist/sql-utils.mjs +3 -0
- package/dist/statement-builders-BPnmt6wx.mjs +116 -0
- package/dist/statement-builders-BPnmt6wx.mjs.map +1 -0
- package/dist/statement-builders.d.mts +23 -0
- package/dist/statement-builders.d.mts.map +1 -0
- package/dist/statement-builders.mjs +3 -0
- package/dist/tables-BmdW_FWO.mjs +477 -0
- package/dist/tables-BmdW_FWO.mjs.map +1 -0
- package/dist/types-ClK03Ojd.d.mts +10 -0
- package/dist/types-ClK03Ojd.d.mts.map +1 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +1 -0
- package/package.json +40 -20
- package/src/core/codec-ids.ts +30 -0
- package/src/core/codecs.ts +645 -0
- package/src/core/default-normalizer.ts +131 -0
- package/src/core/descriptor-meta.ts +1 -1
- package/src/core/errors.ts +33 -0
- package/src/core/json-schema-type-expression.ts +131 -0
- package/src/core/migrations/op-factory-call.ts +1 -5
- package/src/core/migrations/operations/columns.ts +1 -1
- package/src/core/migrations/operations/constraints.ts +1 -1
- package/src/core/migrations/operations/data-transform.ts +27 -21
- package/src/core/migrations/operations/dependencies.ts +1 -1
- package/src/core/migrations/operations/enums.ts +1 -1
- package/src/core/migrations/operations/indexes.ts +1 -1
- package/src/core/migrations/operations/shared.ts +1 -1
- package/src/core/migrations/operations/tables.ts +1 -1
- package/src/core/migrations/planner-ddl-builders.ts +1 -1
- package/src/core/migrations/planner-recipes.ts +1 -1
- package/src/core/migrations/planner-sql-checks.ts +1 -1
- package/src/core/migrations/planner.ts +2 -4
- package/src/core/migrations/postgres-migration.ts +54 -1
- package/src/core/migrations/render-typescript.ts +22 -12
- package/src/core/migrations/runner.ts +2 -4
- package/src/core/native-type-normalizer.ts +49 -0
- package/src/core/sql-utils.ts +104 -0
- package/src/exports/codec-ids.ts +1 -0
- package/src/exports/codec-types.ts +51 -0
- package/src/exports/codecs.ts +2 -0
- package/src/exports/data-transform.ts +1 -0
- package/src/exports/default-normalizer.ts +1 -0
- package/src/exports/errors.ts +1 -0
- package/src/exports/issue-planner.ts +1 -0
- package/src/exports/migration.ts +6 -0
- package/src/exports/native-type-normalizer.ts +1 -0
- package/src/exports/op-factory-call.ts +25 -0
- package/src/exports/planner-ddl-builders.ts +8 -0
- package/src/exports/planner-identity-values.ts +1 -0
- package/src/exports/planner-produced-postgres-migration.ts +1 -0
- package/src/exports/planner-schema-lookup.ts +6 -0
- package/src/exports/planner-sql-checks.ts +11 -0
- package/src/exports/planner-target-details.ts +1 -0
- package/src/exports/planner.ts +1 -0
- package/src/exports/render-ops.ts +1 -0
- package/src/exports/render-typescript.ts +1 -0
- package/src/exports/runtime.ts +19 -4
- package/src/exports/sql-utils.ts +7 -0
- package/src/exports/statement-builders.ts +7 -0
- package/src/exports/types.ts +1 -0
- package/dist/postgres-migration-BsHJHV9O.mjs +0 -2793
- 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
|