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