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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/control-policy-DUFgy9jO.mjs +1103 -0
  2. package/dist/control-policy-DUFgy9jO.mjs.map +1 -0
  3. package/dist/control.d.mts.map +1 -1
  4. package/dist/control.mjs +19 -40
  5. package/dist/control.mjs.map +1 -1
  6. package/dist/default-normalizer.mjs +247 -1
  7. package/dist/default-normalizer.mjs.map +1 -0
  8. package/dist/diff-database-schema-BmSmWcaY.mjs +313 -0
  9. package/dist/diff-database-schema-BmSmWcaY.mjs.map +1 -0
  10. package/dist/diff-database-schema.d.mts +46 -0
  11. package/dist/diff-database-schema.d.mts.map +1 -0
  12. package/dist/diff-database-schema.mjs +2 -0
  13. package/dist/issue-planner.d.mts +34 -10
  14. package/dist/issue-planner.d.mts.map +1 -1
  15. package/dist/issue-planner.mjs +2 -2
  16. package/dist/migration.d.mts +2 -2
  17. package/dist/migration.mjs +2 -2
  18. package/dist/native-type-normalizer.mjs +37 -1
  19. package/dist/native-type-normalizer.mjs.map +1 -0
  20. package/dist/{op-factory-call-BuoGT5UI.mjs → op-factory-call-BKRnMgrB.mjs} +2 -2
  21. package/dist/op-factory-call-BKRnMgrB.mjs.map +1 -0
  22. package/dist/{op-factory-call-Caqus-Qg.d.mts → op-factory-call-Dt-Jx5JS.d.mts} +2 -2
  23. package/dist/{op-factory-call-Caqus-Qg.d.mts.map → op-factory-call-Dt-Jx5JS.d.mts.map} +1 -1
  24. package/dist/op-factory-call.d.mts +2 -2
  25. package/dist/op-factory-call.mjs +2 -2
  26. package/dist/planner-BMBRPzsv.mjs +367 -0
  27. package/dist/planner-BMBRPzsv.mjs.map +1 -0
  28. package/dist/planner-ddl-builders-B8Nn6nHN.mjs.map +1 -1
  29. package/dist/planner-ddl-builders.d.mts +2 -2
  30. package/dist/planner-ddl-builders.d.mts.map +1 -1
  31. package/dist/planner-identity-values-CJPha2Sz.mjs.map +1 -1
  32. package/dist/planner-identity-values.d.mts +7 -1
  33. package/dist/planner-identity-values.d.mts.map +1 -1
  34. package/dist/planner-identity-values.mjs +2 -2
  35. package/dist/{planner-produced-postgres-migration-CtJi4sgS.d.mts → planner-produced-postgres-migration-C0a7I1-e.d.mts} +2 -2
  36. package/dist/{planner-produced-postgres-migration-CtJi4sgS.d.mts.map → planner-produced-postgres-migration-C0a7I1-e.d.mts.map} +1 -1
  37. package/dist/{planner-produced-postgres-migration-C2_H6Vvl.mjs → planner-produced-postgres-migration-Dp6IO_gj.mjs} +2 -2
  38. package/dist/{planner-produced-postgres-migration-C2_H6Vvl.mjs.map → planner-produced-postgres-migration-Dp6IO_gj.mjs.map} +1 -1
  39. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  40. package/dist/planner-produced-postgres-migration.mjs +1 -1
  41. package/dist/planner-sql-checks-Cze3vMfe.mjs.map +1 -1
  42. package/dist/planner-sql-checks.d.mts +1 -1
  43. package/dist/planner-sql-checks.d.mts.map +1 -1
  44. package/dist/planner.d.mts +27 -67
  45. package/dist/planner.d.mts.map +1 -1
  46. package/dist/planner.mjs +3 -2
  47. package/dist/{postgres-database-schema-node-os8ogEap.d.mts → postgres-database-schema-node-C9bTuMrd.d.mts} +14 -7
  48. package/dist/postgres-database-schema-node-C9bTuMrd.d.mts.map +1 -0
  49. package/dist/{postgres-migration-D0QIVE-p.mjs → postgres-migration-D0jwzYDT.mjs} +2 -2
  50. package/dist/{postgres-migration-D0QIVE-p.mjs.map → postgres-migration-D0jwzYDT.mjs.map} +1 -1
  51. package/dist/{postgres-migration-CfyKDT7b.d.mts → postgres-migration-DFaxuRyI.d.mts} +2 -2
  52. package/dist/{postgres-migration-CfyKDT7b.d.mts.map → postgres-migration-DFaxuRyI.d.mts.map} +1 -1
  53. package/dist/{postgres-table-schema-node-Clei_xel.mjs → postgres-table-schema-node-BgaSrxYN.mjs} +30 -29
  54. package/dist/postgres-table-schema-node-BgaSrxYN.mjs.map +1 -0
  55. package/dist/schema-node-kinds-ClScchhi.mjs +76 -0
  56. package/dist/schema-node-kinds-ClScchhi.mjs.map +1 -0
  57. package/dist/types.d.mts +1 -1
  58. package/dist/types.mjs +1 -1
  59. package/package.json +21 -20
  60. package/src/core/migrations/column-ddl-rendering.ts +117 -0
  61. package/src/core/migrations/contract-to-postgres-database-schema-node.ts +25 -1
  62. package/src/core/migrations/control-policy.ts +68 -48
  63. package/src/core/migrations/diff-database-schema.ts +242 -153
  64. package/src/core/migrations/issue-planner.ts +621 -667
  65. package/src/core/migrations/operations/constraints.ts +1 -1
  66. package/src/core/migrations/planner-ddl-builders.ts +5 -2
  67. package/src/core/migrations/planner-identity-values.ts +1 -1
  68. package/src/core/migrations/planner-sql-checks.ts +1 -1
  69. package/src/core/migrations/planner-strategies.ts +248 -208
  70. package/src/core/migrations/planner.ts +180 -74
  71. package/src/core/migrations/runner.ts +1 -4
  72. package/src/core/migrations/verify-postgres-namespaces.ts +26 -15
  73. package/src/core/postgres-schema-verifier.ts +10 -7
  74. package/src/core/psl-infer/infer-psl-contract.ts +5 -10
  75. package/src/core/schema-ir/postgres-database-schema-node.ts +3 -6
  76. package/src/core/schema-ir/postgres-namespace-schema-node.ts +8 -9
  77. package/src/core/schema-ir/postgres-policy-schema-node.ts +3 -6
  78. package/src/core/schema-ir/postgres-role-schema-node.ts +3 -6
  79. package/src/core/schema-ir/postgres-table-schema-node.ts +20 -9
  80. package/src/core/schema-ir/schema-node-kinds.ts +80 -2
  81. package/src/exports/control.ts +10 -43
  82. package/src/exports/diff-database-schema.ts +7 -0
  83. package/src/exports/issue-planner.ts +1 -1
  84. package/src/exports/op-factory-call.ts +1 -0
  85. package/src/exports/planner-identity-values.ts +4 -1
  86. package/src/exports/planner.ts +0 -1
  87. package/dist/default-normalizer-Dug8Fez9.mjs +0 -248
  88. package/dist/default-normalizer-Dug8Fez9.mjs.map +0 -1
  89. package/dist/issue-planner-Cbh-xTEr.mjs +0 -849
  90. package/dist/issue-planner-Cbh-xTEr.mjs.map +0 -1
  91. package/dist/native-type-normalizer-Bc9XJzWC.mjs +0 -38
  92. package/dist/native-type-normalizer-Bc9XJzWC.mjs.map +0 -1
  93. package/dist/op-factory-call-BuoGT5UI.mjs.map +0 -1
  94. package/dist/planner-Bq0Z2sVO.mjs +0 -641
  95. package/dist/planner-Bq0Z2sVO.mjs.map +0 -1
  96. package/dist/postgres-database-schema-node-os8ogEap.d.mts.map +0 -1
  97. package/dist/postgres-table-schema-node-Clei_xel.mjs.map +0 -1
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Migration strategies.
3
3
  *
4
- * Each strategy examines the issue list, consumes issues it handles, and
5
- * returns the `PostgresOpFactoryCall[]` to address them. The issue planner
6
- * runs each strategy in order and routes whatever's left through
7
- * `mapIssueToCall`.
4
+ * Each strategy examines the node-typed diff-issue list, consumes issues it
5
+ * handles, and returns the `PostgresOpFactoryCall[]` to address them. The
6
+ * issue planner runs each strategy in order and routes whatever's left
7
+ * through `mapNodeIssueToCall`.
8
8
  *
9
9
  * The full ordered list is exported as `postgresPlannerStrategies` and is
10
10
  * used unchanged by both `migration plan` and `db update` / `db init`. The
@@ -14,8 +14,16 @@
14
14
  * nullability tightening, unsafe type changes) emit
15
15
  * `DataTransformCall` placeholders that the user fills in.
16
16
  * - When `'data'` is excluded, those strategies short-circuit so the
17
- * downstream walk-schema strategies (codec-hook type ops and temp-default
18
- * backfill) and `mapIssueToCall` defaults emit direct DDL instead.
17
+ * downstream strategies (codec-hook type ops and temp-default backfill)
18
+ * and `mapNodeIssueToCall` defaults emit direct DDL instead.
19
+ *
20
+ * Structural decisions (type/nullability drift, DDL fragments) read the diff
21
+ * node pair and resolve column DDL from the node's `codecRef` (`column-ddl-
22
+ * rendering.ts`). The retained subsystems — codec type-operations
23
+ * (`storageTypePlanCallStrategy`) and the NOT-NULL temp-default deferred DDL
24
+ * (`notNullAddColumnCallStrategy`) — keep reading the contract + codec hooks
25
+ * via `StrategyContext`, per the slice's scope (see `diff-database-schema.ts`
26
+ * / the slice spec).
19
27
  */
20
28
 
21
29
  import type { Contract } from '@prisma-next/contract/types';
@@ -26,20 +34,28 @@ import type {
26
34
  } from '@prisma-next/family-sql/control';
27
35
  import { resolveValueSetValues } from '@prisma-next/family-sql/control';
28
36
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
29
- import type { SchemaIssue } from '@prisma-next/framework-components/control';
37
+ import type { SchemaDiffIssue } from '@prisma-next/framework-components/control';
30
38
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
31
39
  import {
32
40
  type SqlStorage,
33
41
  StorageTable,
34
42
  type StorageTypeInstance,
35
43
  } from '@prisma-next/sql-contract/types';
36
- import type { CodecRef, DdlColumn } from '@prisma-next/sql-relational-core/ast';
37
- import { col } from '@prisma-next/sql-relational-core/contract-free';
38
- import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
44
+ import * as contractFree from '@prisma-next/sql-relational-core/contract-free';
45
+ import {
46
+ RelationalSchemaNodeKind,
47
+ type SqlColumnIR,
48
+ type SqlSchemaIR,
49
+ } from '@prisma-next/sql-schema-ir/types';
39
50
  import { blindCast } from '@prisma-next/utils/casts';
40
- import { ifDefined } from '@prisma-next/utils/defined';
41
- import type { JsonValue } from '@prisma-next/utils/json';
42
51
  import { isPostgresSchema } from '../postgres-schema';
52
+ import {
53
+ renderColumnAlterType,
54
+ renderColumnDdl,
55
+ resolveColumnTemporaryDefault,
56
+ } from './column-ddl-rendering';
57
+ import { resolveNamespaceIdForDdlSchema } from './control-policy';
58
+ import { emissionSchemaName, issueNode, issueSchemaName, issueTableName } from './issue-planner';
43
59
  import {
44
60
  AddCheckConstraintCall,
45
61
  AddColumnCall,
@@ -48,17 +64,13 @@ import {
48
64
  AlterColumnTypeCall,
49
65
  DataTransformCall,
50
66
  DropCheckConstraintCall,
67
+ DropNotNullCall,
51
68
  type PostgresOpFactoryCall,
52
- postgresDefaultToDdlColumnDefault,
53
69
  RawSqlCall,
54
70
  SetNotNullCall,
55
71
  } from './op-factory-call';
56
- import { buildColumnTypeSql } from './planner-ddl-builders';
57
- import { resolveIdentityValue } from './planner-identity-values';
58
72
  import { buildSchemaLookupMap, hasForeignKey, hasUniqueConstraint } from './planner-schema-lookup';
59
- import { buildExpectedFormatType } from './planner-sql-checks';
60
73
  import { buildTargetDetails, type PostgresPlanTargetDetails } from './planner-target-details';
61
- import { resolveColumnTypeMetadata } from './planner-type-resolution';
62
74
 
63
75
  /**
64
76
  * Look up a storage table by its explicit namespace coordinate. Returns
@@ -80,19 +92,6 @@ export function tableAt(
80
92
  return ns.entries.table?.[tableName];
81
93
  }
82
94
 
83
- /**
84
- * Default namespace coordinate for an issue that does not carry one
85
- * explicitly. Hand-crafted unit-test issues and `extra_table` issues
86
- * fall back to `__unbound__`, the only namespace any single-namespace
87
- * contract carries — verifier-emitted issues for legacy
88
- * single-namespace contracts already stamp this id explicitly. Typed
89
- * structurally so issue variants without a `namespaceId` slot
90
- * (e.g. `EnumValuesChangedIssue`) flow through to the same fallback.
91
- */
92
- export function resolveNamespaceIdForIssue(issue: { readonly namespaceId?: string }): string {
93
- return issue.namespaceId ?? UNBOUND_NAMESPACE_ID;
94
- }
95
-
96
95
  /**
97
96
  * Resolve the DDL schema name for a namespace coordinate. Postgres-aware
98
97
  * namespaces dispatch to their polymorphic `ddlSchemaName` override —
@@ -111,6 +110,17 @@ export function resolveDdlSchemaForNamespace(ctx: StrategyContext, namespaceId:
111
110
  return namespaceId;
112
111
  }
113
112
 
113
+ /**
114
+ * Recovers the contract namespace id for a DDL schema name embedded in a
115
+ * diff-issue path (`path[1]`). The strategies need the CONTRACT column/table
116
+ * (for codec-hook reads and eligibility probes the retained subsystems still
117
+ * run) even though the issue itself only carries the resolved DDL schema —
118
+ * this is the reverse of `resolveDdlSchemaForNamespace`.
119
+ */
120
+ function namespaceIdForDdlSchema(ctx: StrategyContext, ddlSchemaName: string): string {
121
+ return resolveNamespaceIdForDdlSchema(ctx.toContract, ddlSchemaName);
122
+ }
123
+
114
124
  // ============================================================================
115
125
  // Strategy types
116
126
  // ============================================================================
@@ -139,13 +149,20 @@ export interface StrategyContext {
139
149
  // Call strategies (for issue planner)
140
150
  // ============================================================================
141
151
 
152
+ /**
153
+ * Consumes node-typed diff issues (`SchemaDiffIssue`, from the one differ —
154
+ * `buildPostgresPlanDiff`), reading the issue's node pair for structural
155
+ * decisions and resolving column DDL from the node's `codecRef`. The
156
+ * retained subsystems (codec type-operations, the NOT-NULL temp-default
157
+ * backfill) keep reading the contract via `StrategyContext`.
158
+ */
142
159
  export type CallMigrationStrategy = (
143
- issues: readonly SchemaIssue[],
160
+ issues: readonly SchemaDiffIssue[],
144
161
  context: StrategyContext,
145
162
  ) =>
146
163
  | {
147
164
  kind: 'match';
148
- issues: readonly SchemaIssue[];
165
+ issues: readonly SchemaDiffIssue[];
149
166
  calls: readonly PostgresOpFactoryCall[];
150
167
  /**
151
168
  * `true` for strategies that emit cohesive sequential recipes whose
@@ -158,91 +175,56 @@ export type CallMigrationStrategy = (
158
175
  }
159
176
  | { kind: 'no_match' };
160
177
 
161
- function buildColumnSpec(
162
- namespaceId: string,
163
- table: string,
164
- column: string,
165
- ctx: StrategyContext,
166
- overrides?: { nullable?: boolean },
167
- ): DdlColumn {
168
- const storageCol = tableAt(ctx.toContract.storage, namespaceId, table)?.columns[column];
169
- if (!storageCol)
170
- throw new Error(`Column "${table}"."${column}" not found in destination contract`);
171
- const mutableHooks = ctx.codecHooks as Map<string, CodecControlHooks>;
172
- const mutableTypes = ctx.storageTypes as Record<string, StorageTypeInstance>;
173
- const typeSql = buildColumnTypeSql(storageCol, mutableHooks, mutableTypes);
174
- const ddlDefault = postgresDefaultToDdlColumnDefault(storageCol.default);
175
- const resolved = resolveColumnTypeMetadata(storageCol, mutableTypes);
176
- const typeParams =
177
- resolved.typeParams === undefined
178
- ? undefined
179
- : blindCast<
180
- JsonValue,
181
- 'resolved.typeParams is JsonValue-shaped storage metadata; the narrowed value lands in CodecRef.typeParams which is JsonValue'
182
- >(resolved.typeParams);
183
- const codecRef: CodecRef | undefined = resolved.codecId
184
- ? {
185
- codecId: resolved.codecId,
186
- ...ifDefined('typeParams', typeParams),
187
- }
188
- : undefined;
189
- const nullable = overrides?.nullable ?? storageCol.nullable;
190
- return col(column, typeSql, {
191
- ...(!nullable ? { notNull: true } : {}),
192
- ...ifDefined('default', ddlDefault),
193
- ...ifDefined('codecRef', codecRef),
194
- });
195
- }
196
-
197
- function buildAlterTypeOptions(
198
- namespaceId: string,
199
- table: string,
200
- column: string,
201
- ctx: StrategyContext,
202
- using?: string,
203
- ) {
204
- const col = tableAt(ctx.toContract.storage, namespaceId, table)?.columns[column];
205
- if (!col) throw new Error(`Column "${table}"."${column}" not found in destination contract`);
206
- const mutableHooks = ctx.codecHooks as Map<string, CodecControlHooks>;
207
- const mutableTypes = ctx.storageTypes as Record<string, StorageTypeInstance>;
208
- const qualifiedTargetType = buildColumnTypeSql(col, mutableHooks, mutableTypes, false);
209
- const formatTypeExpected = buildExpectedFormatType(col, mutableHooks, mutableTypes);
178
+ /** A `not-equal` column issue whose node pair is narrowed to `SqlColumnIR`. */
179
+ function columnNodePair(
180
+ issue: SchemaDiffIssue,
181
+ ): { readonly expected: SqlColumnIR; readonly actual: SqlColumnIR } | undefined {
182
+ const node = issueNode(issue);
183
+ if (node === undefined || node.nodeKind !== RelationalSchemaNodeKind.column) return undefined;
184
+ if (issue.expected === undefined || issue.actual === undefined) return undefined;
210
185
  return {
211
- qualifiedTargetType,
212
- formatTypeExpected,
213
- rawTargetTypeForLabel: qualifiedTargetType,
214
- ...(using !== undefined ? { using } : {}),
186
+ expected: blindCast<SqlColumnIR, 'a column diff node is always a SqlColumnIR'>(issue.expected),
187
+ actual: blindCast<SqlColumnIR, 'a column diff node is always a SqlColumnIR'>(issue.actual),
215
188
  };
216
189
  }
217
190
 
218
191
  export const notNullBackfillCallStrategy: CallMigrationStrategy = (issues, ctx) => {
219
192
  // `DataTransformCall` is operation class `'data'`. When the policy excludes
220
193
  // it (`db update` / `db init`), skip so `notNullAddColumnCallStrategy`
221
- // (temp-default backfill) or `mapIssueToCall` can take the issue.
194
+ // (temp-default backfill) or `mapNodeIssueToCall` can take the issue.
222
195
  if (!ctx.policy.allowedOperationClasses.includes('data')) return { kind: 'no_match' };
223
196
 
224
- const matched: SchemaIssue[] = [];
197
+ const matched: SchemaDiffIssue[] = [];
225
198
  const calls: PostgresOpFactoryCall[] = [];
226
199
 
227
200
  for (const issue of issues) {
228
- if (issue.kind !== 'missing_column' || !issue.table || !issue.column) continue;
229
-
230
- const namespaceId = resolveNamespaceIdForIssue(issue);
231
- const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
232
- if (!column) continue;
233
- if (column.nullable === true || column.default !== undefined) continue;
201
+ if (issue.reason !== 'not-found') continue;
202
+ const node = issueNode(issue);
203
+ if (node === undefined || node.nodeKind !== RelationalSchemaNodeKind.column) continue;
204
+ const expected = blindCast<
205
+ SqlColumnIR,
206
+ 'a not-found column issue always carries the expected column node'
207
+ >(issue.expected);
208
+ if (expected.nullable !== false || expected.resolvedDefault !== undefined) continue;
209
+
210
+ const ddlSchemaName = issueSchemaName(issue);
211
+ const tableName = issueTableName(issue);
212
+ if (ddlSchemaName === undefined || tableName === undefined) continue;
213
+ const schemaName = emissionSchemaName(ctx, ddlSchemaName);
234
214
 
235
215
  matched.push(issue);
236
- const spec = buildColumnSpec(namespaceId, issue.table, issue.column, ctx, { nullable: true });
237
- const schemaForTable = resolveDdlSchemaForNamespace(ctx, namespaceId);
216
+ const ddl = renderColumnDdl(expected.name, expected, ctx.codecHooks);
217
+ const nullableSpec = contractFree.col(ddl.name, ddl.type, {
218
+ ...(ddl.codecRef !== undefined ? { codecRef: ddl.codecRef } : {}),
219
+ });
238
220
  calls.push(
239
- new AddColumnCall(schemaForTable, issue.table, spec),
221
+ new AddColumnCall(schemaName, tableName, nullableSpec),
240
222
  new DataTransformCall(
241
- `backfill-${issue.table}-${issue.column}`,
242
- `backfill-${issue.table}-${issue.column}:check`,
243
- `backfill-${issue.table}-${issue.column}:run`,
223
+ `backfill-${tableName}-${expected.name}`,
224
+ `backfill-${tableName}-${expected.name}:check`,
225
+ `backfill-${tableName}-${expected.name}:run`,
244
226
  ),
245
- new SetNotNullCall(schemaForTable, issue.table, issue.column),
227
+ new SetNotNullCall(schemaName, tableName, expected.name),
246
228
  );
247
229
  }
248
230
 
@@ -257,49 +239,87 @@ export const notNullBackfillCallStrategy: CallMigrationStrategy = (issues, ctx)
257
239
 
258
240
  const SAFE_WIDENINGS = new Set(['int2→int4', 'int2→int8', 'int4→int8', 'float4→float8']);
259
241
 
242
+ /**
243
+ * Handles `not-equal` column issues whose TYPE differs. `fromContract` is
244
+ * only supplied by `migration plan` — for reconciliation (`db update` /
245
+ * `db init`, `fromContract === null`) this strategy never fires, mirroring
246
+ * the legacy `typeChangeCallStrategy`'s requirement of a prior contract:
247
+ * `mapNodeIssueToCall`'s in-place ALTER covers reconciliation directly.
248
+ *
249
+ * A single node issue can carry BOTH type and nullability drift (Postgres
250
+ * alters in place, so the differ emits one `not-equal` column issue where
251
+ * the legacy coordinate walk emitted two: `type_mismatch` +
252
+ * `nullability_mismatch`). When this strategy consumes the issue for its
253
+ * type portion, it also emits whatever nullability delta the same node pair
254
+ * carries — using the same construction `nullableTighteningCallStrategy` /
255
+ * the mapper's direct dispatch would have used — so the issue is never
256
+ * partially handled.
257
+ */
260
258
  export const typeChangeCallStrategy: CallMigrationStrategy = (issues, ctx) => {
261
- // For unsafe widenings this strategy emits a `DataTransformCall` placeholder
262
- // (operation class `'data'`); when the policy excludes `'data'`
263
- // (`db update` / `db init`), skip those issues so `mapIssueToCall` can
264
- // emit a direct `ALTER COLUMN TYPE`. Safe widenings still flow through
265
- // here because the resulting `AlterColumnTypeCall` is `widening`-class.
259
+ if (ctx.fromContract === null) return { kind: 'no_match' };
266
260
  const dataAllowed = ctx.policy.allowedOperationClasses.includes('data');
267
261
 
268
- const matched: SchemaIssue[] = [];
262
+ const matched: SchemaDiffIssue[] = [];
269
263
  const calls: PostgresOpFactoryCall[] = [];
270
264
 
271
265
  for (const issue of issues) {
272
- if (issue.kind !== 'type_mismatch') continue;
273
- if (!issue.table || !issue.column) continue;
274
- const namespaceId = resolveNamespaceIdForIssue(issue);
275
- const fromColumn = ctx.fromContract
276
- ? tableAt(ctx.fromContract.storage, namespaceId, issue.table)?.columns[issue.column]
277
- : undefined;
278
- const toColumn = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[
279
- issue.column
280
- ];
281
- if (!fromColumn || !toColumn) continue;
282
- const fromType = fromColumn.nativeType;
283
- const toType = toColumn.nativeType;
284
- if (fromType === toType) continue;
266
+ if (issue.reason !== 'not-equal') continue;
267
+ const pair = columnNodePair(issue);
268
+ if (pair === undefined) continue;
269
+ const { expected, actual } = pair;
270
+ if (!columnTypeChangedNativeOnly(expected, actual)) continue;
271
+
272
+ const fromType = actual.nativeType;
273
+ const toType = expected.nativeType;
285
274
  const isSafeWidening = SAFE_WIDENINGS.has(`${fromType}→${toType}`);
286
275
  if (!isSafeWidening && !dataAllowed) continue;
276
+
277
+ const ddlSchemaName = issueSchemaName(issue);
278
+ const tableName = issueTableName(issue);
279
+ if (ddlSchemaName === undefined || tableName === undefined) continue;
280
+ const schemaName = emissionSchemaName(ctx, ddlSchemaName);
281
+
287
282
  matched.push(issue);
288
- const alterOpts = buildAlterTypeOptions(namespaceId, issue.table, issue.column, ctx);
289
- const schemaForTable = resolveDdlSchemaForNamespace(ctx, namespaceId);
283
+ const { qualifiedTargetType, formatTypeExpected } = renderColumnAlterType(
284
+ expected,
285
+ ctx.codecHooks,
286
+ );
287
+ const alterOpts = {
288
+ qualifiedTargetType,
289
+ formatTypeExpected,
290
+ rawTargetTypeForLabel: qualifiedTargetType,
291
+ };
290
292
  if (isSafeWidening) {
291
- calls.push(new AlterColumnTypeCall(schemaForTable, issue.table, issue.column, alterOpts));
293
+ calls.push(new AlterColumnTypeCall(schemaName, tableName, expected.name, alterOpts));
292
294
  } else {
293
295
  calls.push(
294
296
  new DataTransformCall(
295
- `typechange-${issue.table}-${issue.column}`,
296
- `typechange-${issue.table}-${issue.column}:check`,
297
- `typechange-${issue.table}-${issue.column}:run`,
297
+ `typechange-${tableName}-${expected.name}`,
298
+ `typechange-${tableName}-${expected.name}:check`,
299
+ `typechange-${tableName}-${expected.name}:run`,
298
300
  ),
299
- new AlterColumnTypeCall(schemaForTable, issue.table, issue.column, alterOpts),
301
+ new AlterColumnTypeCall(schemaName, tableName, expected.name, alterOpts),
300
302
  );
301
303
  }
304
+
305
+ if (expected.nullable !== actual.nullable) {
306
+ if (expected.nullable) {
307
+ calls.push(new DropNotNullCall(schemaName, tableName, expected.name));
308
+ } else if (dataAllowed) {
309
+ calls.push(
310
+ new DataTransformCall(
311
+ `handle-nulls-${tableName}-${expected.name}`,
312
+ `handle-nulls-${tableName}-${expected.name}:check`,
313
+ `handle-nulls-${tableName}-${expected.name}:run`,
314
+ ),
315
+ new SetNotNullCall(schemaName, tableName, expected.name),
316
+ );
317
+ } else {
318
+ calls.push(new SetNotNullCall(schemaName, tableName, expected.name));
319
+ }
320
+ }
302
321
  }
322
+
303
323
  if (matched.length === 0) return { kind: 'no_match' };
304
324
  return {
305
325
  kind: 'match',
@@ -309,32 +329,54 @@ export const typeChangeCallStrategy: CallMigrationStrategy = (issues, ctx) => {
309
329
  };
310
330
  };
311
331
 
332
+ /**
333
+ * Whether the raw (unresolved) native type differs — the SAME comparison
334
+ * `typeChangeCallStrategy` always used (`fromColumn.nativeType !==
335
+ * toColumn.nativeType`), which for the widenable numeric/float types
336
+ * `SAFE_WIDENINGS` lists is identical to the resolved comparison
337
+ * `columnTypeChanged` (in `issue-planner.ts`) performs.
338
+ */
339
+ function columnTypeChangedNativeOnly(expected: SqlColumnIR, actual: SqlColumnIR): boolean {
340
+ return expected.nativeType !== actual.nativeType;
341
+ }
342
+
343
+ /**
344
+ * Handles `not-equal` column issues whose type did NOT change but
345
+ * nullability tightened (contract requires NOT NULL, live column is
346
+ * nullable). A type-changed issue's nullability delta (if any) is already
347
+ * handled by `typeChangeCallStrategy`, which runs first — this strategy
348
+ * only ever sees issues that strategy left behind.
349
+ */
312
350
  export const nullableTighteningCallStrategy: CallMigrationStrategy = (issues, ctx) => {
313
351
  // `DataTransformCall` is operation class `'data'`. When the policy excludes
314
- // it (`db update` / `db init`), skip so `mapIssueToCall` emits a direct
352
+ // it (`db update` / `db init`), skip so `mapNodeIssueToCall` emits a direct
315
353
  // `SET NOT NULL` instead.
316
354
  if (!ctx.policy.allowedOperationClasses.includes('data')) return { kind: 'no_match' };
317
355
 
318
- const matched: SchemaIssue[] = [];
356
+ const matched: SchemaDiffIssue[] = [];
319
357
  const calls: PostgresOpFactoryCall[] = [];
320
358
 
321
359
  for (const issue of issues) {
322
- if (issue.kind !== 'nullability_mismatch' || !issue.table || !issue.column) continue;
323
-
324
- const namespaceId = resolveNamespaceIdForIssue(issue);
325
- const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
326
- if (!column) continue;
327
- if (column.nullable === true) continue;
360
+ if (issue.reason !== 'not-equal') continue;
361
+ const pair = columnNodePair(issue);
362
+ if (pair === undefined) continue;
363
+ const { expected, actual } = pair;
364
+ if (columnTypeChangedNativeOnly(expected, actual)) continue; // typeChangeCallStrategy's concern
365
+ if (expected.nullable !== false || actual.nullable !== true) continue;
366
+
367
+ const ddlSchemaName = issueSchemaName(issue);
368
+ const tableName = issueTableName(issue);
369
+ if (ddlSchemaName === undefined || tableName === undefined) continue;
370
+ const schemaName = emissionSchemaName(ctx, ddlSchemaName);
328
371
 
329
372
  matched.push(issue);
330
- const schemaForTable = resolveDdlSchemaForNamespace(ctx, namespaceId);
331
373
  calls.push(
332
374
  new DataTransformCall(
333
- `handle-nulls-${issue.table}-${issue.column}`,
334
- `handle-nulls-${issue.table}-${issue.column}:check`,
335
- `handle-nulls-${issue.table}-${issue.column}:run`,
375
+ `handle-nulls-${tableName}-${expected.name}`,
376
+ `handle-nulls-${tableName}-${expected.name}:check`,
377
+ `handle-nulls-${tableName}-${expected.name}:run`,
336
378
  ),
337
- new SetNotNullCall(schemaForTable, issue.table, issue.column),
379
+ new SetNotNullCall(schemaName, tableName, expected.name),
338
380
  );
339
381
  }
340
382
 
@@ -384,9 +426,13 @@ function checkValueSetsEqual(a: readonly string[], b: readonly string[]): boolea
384
426
  /**
385
427
  * Plans check-constraint migrations for `enumType`-authored columns.
386
428
  *
387
- * Walks every namespace's tables in the target contract. For each table that
388
- * carries `checks`, diffs the contract-expected checks against the live
389
- * schema's checks:
429
+ * Walks every namespace's tables in the target contract (the check nodes'
430
+ * resolved `permittedValues` are ultimately sourced from the same
431
+ * contract-declared value sets, so walking the contract directly is the
432
+ * simplest faithful port — the strategy's decisions never depend on which
433
+ * ISSUES happen to be in the input list, only on the contract + live schema
434
+ * shapes). For each table that carries `checks`, diffs the contract-expected
435
+ * checks against the live schema's checks:
390
436
  *
391
437
  * - Check in contract, absent from live DB → `AddCheckConstraintCall`.
392
438
  * - Check in live DB, absent from contract → `DropCheckConstraintCall`.
@@ -394,7 +440,9 @@ function checkValueSetsEqual(a: readonly string[], b: readonly string[]): boolea
394
440
  * then `AddCheckConstraintCall` (drop + recreate; a check predicate cannot
395
441
  * be altered in place).
396
442
  *
397
- * Consumes `check_missing`, `check_removed`, and `check_mismatch` issues.
443
+ * Consumes every `sql-check-constraint` issue on a table this walk handles
444
+ * (not-found/not-expected/not-equal), leaving check issues on tables with
445
+ * NO contract checks to `mapNodeIssueToCall`'s `not-expected` fallback.
398
446
  */
399
447
  export const checkConstraintPlanCallStrategy: CallMigrationStrategy = (issues, ctx) => {
400
448
  const calls: PostgresOpFactoryCall[] = [];
@@ -411,7 +459,7 @@ export const checkConstraintPlanCallStrategy: CallMigrationStrategy = (issues, c
411
459
 
412
460
  for (const contractCheck of contractChecks) {
413
461
  const liveCheck = liveChecks.find((c) => c.name === contractCheck.name);
414
- const issueKey = `${tableName}${contractCheck.name}`;
462
+ const issueKey = `${tableName} ${contractCheck.name}`;
415
463
  if (!liveCheck) {
416
464
  calls.push(
417
465
  new AddCheckConstraintCall(
@@ -446,7 +494,7 @@ export const checkConstraintPlanCallStrategy: CallMigrationStrategy = (issues, c
446
494
  for (const liveCheck of liveChecks) {
447
495
  const inContract = contractChecks.some((c) => c.name === liveCheck.name);
448
496
  if (!inContract) {
449
- const issueKey = `${tableName}${liveCheck.name}`;
497
+ const issueKey = `${tableName} ${liveCheck.name}`;
450
498
  calls.push(new DropCheckConstraintCall(ddlSchema, tableName, liveCheck.name));
451
499
  handledIssueKeys.add(issueKey);
452
500
  }
@@ -457,16 +505,14 @@ export const checkConstraintPlanCallStrategy: CallMigrationStrategy = (issues, c
457
505
  if (calls.length === 0 && handledIssueKeys.size === 0) return { kind: 'no_match' };
458
506
 
459
507
  const remaining = issues.filter((issue) => {
460
- if (
461
- issue.kind !== 'check_missing' &&
462
- issue.kind !== 'check_removed' &&
463
- issue.kind !== 'check_mismatch'
464
- ) {
465
- return true;
466
- }
467
- if (!issue.table || !issue.indexOrConstraint) return true;
468
- const key = `${issue.table}${issue.indexOrConstraint}`;
469
- return !handledIssueKeys.has(key);
508
+ const node = issueNode(issue);
509
+ if (node === undefined || node.nodeKind !== RelationalSchemaNodeKind.check) return true;
510
+ const tableName = issueTableName(issue);
511
+ if (tableName === undefined) return true;
512
+ const checkName = blindCast<{ readonly name: string }, 'a check node always carries a name'>(
513
+ node,
514
+ ).name;
515
+ return !handledIssueKeys.has(`${tableName} ${checkName}`);
470
516
  });
471
517
 
472
518
  return { kind: 'match', issues: remaining, calls };
@@ -475,14 +521,16 @@ export const checkConstraintPlanCallStrategy: CallMigrationStrategy = (issues, c
475
521
  /**
476
522
  * Dispatches codec-typed storage types through their codec's
477
523
  * `planTypeOperations` hook (the authoritative source for codec-driven DDL
478
- * such as custom type creation).
524
+ * such as custom type creation). Codec extension/type ops are not modeled as
525
+ * diff nodes — this strategy drives entirely off `ctx.toContract.storage.types`
526
+ * + codec hooks, consuming nothing from the node issue list (there is no
527
+ * node-vocabulary equivalent of `type_missing` / `enum_values_changed`).
479
528
  */
480
529
  export const storageTypePlanCallStrategy: CallMigrationStrategy = (issues, ctx) => {
481
530
  const storageTypes = ctx.toContract.storage.types ?? {};
482
531
  if (Object.keys(storageTypes).length === 0) return { kind: 'no_match' };
483
532
 
484
533
  const calls: PostgresOpFactoryCall[] = [];
485
- const handledTypeNames = new Set<string>();
486
534
 
487
535
  for (const [typeName, typeInstance] of Object.entries(storageTypes).sort(([a], [b]) =>
488
536
  a.localeCompare(b),
@@ -499,11 +547,6 @@ export const storageTypePlanCallStrategy: CallMigrationStrategy = (issues, ctx)
499
547
  policy: ctx.policy,
500
548
  });
501
549
  if (!planResult) continue;
502
- if (planResult.operations.length === 0) {
503
- handledTypeNames.add(typeName);
504
- continue;
505
- }
506
- handledTypeNames.add(typeName);
507
550
  for (const op of planResult.operations) {
508
551
  calls.push(
509
552
  new RawSqlCall({
@@ -517,38 +560,28 @@ export const storageTypePlanCallStrategy: CallMigrationStrategy = (issues, ctx)
517
560
  }
518
561
  }
519
562
 
520
- const remaining = issues.filter(
521
- (issue) =>
522
- !(
523
- (issue.kind === 'type_missing' || issue.kind === 'enum_values_changed') &&
524
- issue.typeName &&
525
- handledTypeNames.has(issue.typeName)
526
- ),
527
- );
528
-
529
- if (calls.length === 0 && remaining.length === issues.length) {
530
- return { kind: 'no_match' };
531
- }
532
-
533
- return { kind: 'match', issues: remaining, calls };
563
+ if (calls.length === 0) return { kind: 'no_match' };
564
+ return { kind: 'match', issues, calls };
534
565
  };
535
566
 
536
567
  /**
537
- * Handles `missing_column` issues for NOT NULL columns without a contract
538
- * default. Replaces the walk-schema `buildAddColumnItem` non-default branches.
568
+ * Handles `not-found` column issues for NOT NULL columns without a contract
569
+ * default. Replaces the legacy `buildAddColumnItem` non-default branches.
539
570
  *
540
571
  * Two shapes:
541
572
  * - Shared-temp-default safe: emit a single atomic composite op (add
542
573
  * nullable → backfill identity value → `SET NOT NULL` → `DROP DEFAULT`).
574
+ * The temp-default value is resolved from the column node's `codecRef`
575
+ * (`resolveColumnTemporaryDefault`, wrapping `resolveIdentityValue`).
543
576
  * - Empty-table guarded: emit a hand-built op with a `tableIsEmptyCheck`
544
577
  * precheck so the failure message is "table is not empty" rather than the
545
578
  * raw PG NOT NULL violation.
546
579
  *
547
- * "Normal" missing_column cases (nullable or has a contract default) are left
548
- * for `mapIssueToCall`'s default `AddColumnCall` emission.
580
+ * "Normal" not-found column cases (nullable or has a contract default) are
581
+ * left for `mapNodeIssueToCall`'s default `AddColumnCall` emission.
549
582
  */
550
583
  export const notNullAddColumnCallStrategy: CallMigrationStrategy = (issues, ctx) => {
551
- const matched: SchemaIssue[] = [];
584
+ const matched: SchemaDiffIssue[] = [];
552
585
  const calls: PostgresOpFactoryCall[] = [];
553
586
 
554
587
  const schemaLookups = buildSchemaLookupMap(ctx.schema);
@@ -557,40 +590,47 @@ export const notNullAddColumnCallStrategy: CallMigrationStrategy = (issues, ctx)
557
590
  const mutableStorageTypes = ctx.storageTypes as Record<string, StorageTypeInstance>;
558
591
 
559
592
  for (const issue of issues) {
560
- if (issue.kind !== 'missing_column' || !issue.table || !issue.column) continue;
561
- const namespaceId = resolveNamespaceIdForIssue(issue);
562
- const contractTable = tableAt(ctx.toContract.storage, namespaceId, issue.table);
563
- const column = contractTable?.columns[issue.column];
564
- if (!column) continue;
565
-
566
- const notNull = column.nullable !== true;
567
- const hasDefault = column.default !== undefined;
568
- if (!notNull || hasDefault) continue;
569
-
570
- const schemaTable = ctx.schema.tables[issue.table];
593
+ if (issue.reason !== 'not-found') continue;
594
+ const node = issueNode(issue);
595
+ if (node === undefined || node.nodeKind !== RelationalSchemaNodeKind.column) continue;
596
+ const expected = blindCast<
597
+ SqlColumnIR,
598
+ 'a not-found column issue always carries the expected column node'
599
+ >(issue.expected);
600
+ if (expected.nullable !== false || expected.resolvedDefault !== undefined) continue;
601
+
602
+ const ddlSchemaName = issueSchemaName(issue);
603
+ const tableName = issueTableName(issue);
604
+ if (ddlSchemaName === undefined || tableName === undefined) continue;
605
+
606
+ const namespaceId = namespaceIdForDdlSchema(ctx, ddlSchemaName);
607
+ const schemaName = namespaceId === UNBOUND_NAMESPACE_ID ? UNBOUND_NAMESPACE_ID : ddlSchemaName;
608
+ const contractTable = tableAt(ctx.toContract.storage, namespaceId, tableName);
609
+ const column = contractTable?.columns[expected.name];
610
+ if (!contractTable || !column) continue;
611
+
612
+ const schemaTable = ctx.schema.tables[tableName];
571
613
  if (!schemaTable) continue;
572
614
 
573
- const temporaryDefault = resolveIdentityValue(column, mutableCodecHooks, mutableStorageTypes);
574
- const schemaLookup = schemaLookups.get(issue.table);
615
+ const temporaryDefault = resolveColumnTemporaryDefault(expected, ctx.codecHooks);
616
+ const schemaLookup = schemaLookups.get(tableName);
575
617
  const canUseSharedTempDefault =
576
618
  temporaryDefault !== null &&
577
619
  canUseSharedTemporaryDefaultStrategy({
578
620
  table: contractTable,
579
621
  schemaTable,
580
622
  schemaLookup,
581
- columnName: issue.column,
623
+ columnName: expected.name,
582
624
  });
583
625
 
584
626
  matched.push(issue);
585
627
 
586
- const schemaForTable = resolveDdlSchemaForNamespace(ctx, namespaceId);
587
-
588
628
  if (canUseSharedTempDefault && temporaryDefault !== null) {
589
629
  calls.push(
590
630
  new AddNotNullColumnWithTempDefaultCall({
591
- schemaName: schemaForTable,
592
- tableName: issue.table,
593
- columnName: issue.column,
631
+ schemaName,
632
+ tableName,
633
+ columnName: expected.name,
594
634
  column,
595
635
  codecHooks: mutableCodecHooks,
596
636
  storageTypes: mutableStorageTypes,
@@ -602,10 +642,10 @@ export const notNullAddColumnCallStrategy: CallMigrationStrategy = (issues, ctx)
602
642
 
603
643
  calls.push(
604
644
  new AddNotNullColumnDirectCall(
605
- schemaForTable,
606
- issue.table,
607
- issue.column,
608
- buildColumnSpec(namespaceId, issue.table, issue.column, ctx),
645
+ schemaName,
646
+ tableName,
647
+ expected.name,
648
+ renderColumnDdl(expected.name, expected, ctx.codecHooks),
609
649
  ),
610
650
  );
611
651
  }
@@ -657,7 +697,7 @@ function canUseSharedTemporaryDefaultStrategy(options: {
657
697
  * Ordered list of Postgres planner strategies, shared by `migration plan`
658
698
  * and `db update` / `db init`. The issue planner runs each strategy in
659
699
  * order, letting it consume any issues it handles, and routes whatever's
660
- * left through `mapIssueToCall`. Behavior diverges purely on
700
+ * left through `mapNodeIssueToCall`. Behavior diverges purely on
661
701
  * `policy.allowedOperationClasses`:
662
702
  *
663
703
  * - When `'data'` is allowed (`migration plan`), the data-safe strategies
@@ -667,9 +707,9 @@ function canUseSharedTemporaryDefaultStrategy(options: {
667
707
  *
668
708
  * - When `'data'` is not allowed (`db update` / `db init`), the
669
709
  * placeholder-emitting strategies short-circuit to `no_match`, leaving
670
- * the issue for the downstream walk-schema strategies
671
- * (`storageTypePlanCallStrategy`, `notNullAddColumnCallStrategy`) or the
672
- * `mapIssueToCall` default to handle with direct DDL.
710
+ * the issue for the downstream strategies (`storageTypePlanCallStrategy`,
711
+ * `notNullAddColumnCallStrategy`) or the `mapNodeIssueToCall` default to
712
+ * handle with direct DDL.
673
713
  *
674
714
  * Codec-typed storage type entries are dispatched through
675
715
  * `storageTypePlanCallStrategy`.