@prisma-next/target-postgres 0.9.0 → 0.10.0

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 (124) hide show
  1. package/dist/{codec-ids-RvYfmUmi.d.mts → codec-ids-D9fJ4HP5.d.mts} +1 -1
  2. package/dist/{codec-ids-RvYfmUmi.d.mts.map → codec-ids-D9fJ4HP5.d.mts.map} +1 -1
  3. package/dist/codec-ids.d.mts +1 -1
  4. package/dist/{codec-types-667FxIW8.d.mts → codec-types-CRlHq7Cz.d.mts} +2 -2
  5. package/dist/{codec-types-667FxIW8.d.mts.map → codec-types-CRlHq7Cz.d.mts.map} +1 -1
  6. package/dist/codec-types.d.mts +1 -1
  7. package/dist/{codecs-DXeDABSO.d.mts → codecs-Dud5KDNk.d.mts} +2 -2
  8. package/dist/{codecs-DXeDABSO.d.mts.map → codecs-Dud5KDNk.d.mts.map} +1 -1
  9. package/dist/codecs.d.mts +1 -1
  10. package/dist/control.d.mts +1 -1
  11. package/dist/control.mjs +5 -5
  12. package/dist/{data-transform-COkGR6Ns.mjs → data-transform-CdtGUWp2.mjs} +1 -1
  13. package/dist/{data-transform-COkGR6Ns.mjs.map → data-transform-CdtGUWp2.mjs.map} +1 -1
  14. package/dist/{data-transform-B6p02mFJ.d.mts → data-transform-bmOKkygi.d.mts} +2 -2
  15. package/dist/{data-transform-B6p02mFJ.d.mts.map → data-transform-bmOKkygi.d.mts.map} +1 -1
  16. package/dist/data-transform.d.mts +1 -1
  17. package/dist/data-transform.mjs +1 -1
  18. package/dist/descriptor-meta-zrZzWmJF.mjs +91 -0
  19. package/dist/{descriptor-meta-DFUCClk_.mjs.map → descriptor-meta-zrZzWmJF.mjs.map} +1 -1
  20. package/dist/enum-planning.d.mts +1 -1
  21. package/dist/{errors-BiOloWUh.mjs → errors--zafB5_n.mjs} +1 -1
  22. package/dist/{errors-BiOloWUh.mjs.map → errors--zafB5_n.mjs.map} +1 -1
  23. package/dist/errors.mjs +1 -1
  24. package/dist/{issue-planner-BhWVYyE1.mjs → issue-planner-qalHRCI2.mjs} +179 -77
  25. package/dist/issue-planner-qalHRCI2.mjs.map +1 -0
  26. package/dist/issue-planner.d.mts +2 -2
  27. package/dist/issue-planner.d.mts.map +1 -1
  28. package/dist/issue-planner.mjs +1 -1
  29. package/dist/migration.d.mts +3 -3
  30. package/dist/migration.mjs +3 -3
  31. package/dist/{op-factory-call-c1zELk3U.d.mts → op-factory-call-Drccm_JD.d.mts} +3 -3
  32. package/dist/{op-factory-call-c1zELk3U.d.mts.map → op-factory-call-Drccm_JD.d.mts.map} +1 -1
  33. package/dist/{op-factory-call-DerP9BoT.mjs → op-factory-call-Zsrdty3k.mjs} +2 -2
  34. package/dist/{op-factory-call-DerP9BoT.mjs.map → op-factory-call-Zsrdty3k.mjs.map} +1 -1
  35. package/dist/op-factory-call.d.mts +1 -1
  36. package/dist/op-factory-call.mjs +1 -1
  37. package/dist/pack.d.mts +2 -2
  38. package/dist/pack.mjs +1 -1
  39. package/dist/{planner-DnzPpv1j.mjs → planner-C8yhbXOq.mjs} +84 -5
  40. package/dist/planner-C8yhbXOq.mjs.map +1 -0
  41. package/dist/{planner-ddl-builders-5QIyhBUF.mjs → planner-ddl-builders-DINYrbJ3.mjs} +4 -4
  42. package/dist/{planner-ddl-builders-5QIyhBUF.mjs.map → planner-ddl-builders-DINYrbJ3.mjs.map} +1 -1
  43. package/dist/planner-ddl-builders.d.mts +1 -1
  44. package/dist/planner-ddl-builders.mjs +1 -1
  45. package/dist/{planner-identity-values-BUYNOCwb.mjs → planner-identity-values-ojX-6cPV.mjs} +1 -1
  46. package/dist/{planner-identity-values-BUYNOCwb.mjs.map → planner-identity-values-ojX-6cPV.mjs.map} +1 -1
  47. package/dist/planner-identity-values.mjs +1 -1
  48. package/dist/{planner-produced-postgres-migration-FYsKh26I.mjs → planner-produced-postgres-migration-BqGLw7VT.mjs} +4 -4
  49. package/dist/{planner-produced-postgres-migration-FYsKh26I.mjs.map → planner-produced-postgres-migration-BqGLw7VT.mjs.map} +1 -1
  50. package/dist/{planner-produced-postgres-migration-D34ftfEK.d.mts → planner-produced-postgres-migration-c9lpjPv1.d.mts} +3 -3
  51. package/dist/{planner-produced-postgres-migration-D34ftfEK.d.mts.map → planner-produced-postgres-migration-c9lpjPv1.d.mts.map} +1 -1
  52. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  53. package/dist/planner-produced-postgres-migration.mjs +1 -1
  54. package/dist/planner-schema-lookup-BGyukuzG.mjs +57 -0
  55. package/dist/planner-schema-lookup-BGyukuzG.mjs.map +1 -0
  56. package/dist/planner-schema-lookup.d.mts.map +1 -1
  57. package/dist/planner-schema-lookup.mjs +1 -1
  58. package/dist/{planner-sql-checks-Cd016Ycs.mjs → planner-sql-checks-BM4sD6Xc.mjs} +28 -11
  59. package/dist/planner-sql-checks-BM4sD6Xc.mjs.map +1 -0
  60. package/dist/planner-sql-checks.d.mts +11 -0
  61. package/dist/planner-sql-checks.d.mts.map +1 -1
  62. package/dist/planner-sql-checks.mjs +1 -1
  63. package/dist/{planner-target-details-iYJwzFHP.d.mts → planner-target-details-CIj61DUj.d.mts} +1 -1
  64. package/dist/{planner-target-details-iYJwzFHP.d.mts.map → planner-target-details-CIj61DUj.d.mts.map} +1 -1
  65. package/dist/planner-target-details.d.mts +1 -1
  66. package/dist/planner.d.mts +1 -1
  67. package/dist/planner.d.mts.map +1 -1
  68. package/dist/planner.mjs +1 -1
  69. package/dist/postgres-contract-serializer-CcZO9ukP.mjs +83 -0
  70. package/dist/postgres-contract-serializer-CcZO9ukP.mjs.map +1 -0
  71. package/dist/{postgres-enum-type-CrKq8au9.d.mts → postgres-enum-type-CNhPTDhy.d.mts} +1 -1
  72. package/dist/{postgres-enum-type-CrKq8au9.d.mts.map → postgres-enum-type-CNhPTDhy.d.mts.map} +1 -1
  73. package/dist/{postgres-migration-q4oWyNJE.mjs → postgres-migration-C5os-tkl.mjs} +3 -3
  74. package/dist/{postgres-migration-q4oWyNJE.mjs.map → postgres-migration-C5os-tkl.mjs.map} +1 -1
  75. package/dist/{postgres-migration-CiQzhcMe.d.mts → postgres-migration-jvsKgUDM.d.mts} +3 -3
  76. package/dist/{postgres-migration-CiQzhcMe.d.mts.map → postgres-migration-jvsKgUDM.d.mts.map} +1 -1
  77. package/dist/postgres-schema-BosNxhWq.mjs +163 -0
  78. package/dist/postgres-schema-BosNxhWq.mjs.map +1 -0
  79. package/dist/{render-ops-CkiuHSNj.mjs → render-ops-BC2PtCkj.mjs} +1 -1
  80. package/dist/{render-ops-CkiuHSNj.mjs.map → render-ops-BC2PtCkj.mjs.map} +1 -1
  81. package/dist/render-ops.d.mts +1 -1
  82. package/dist/render-ops.mjs +1 -1
  83. package/dist/{render-typescript-C9XWI8Ld.mjs → render-typescript-nRHbqLbI.mjs} +1 -1
  84. package/dist/{render-typescript-C9XWI8Ld.mjs.map → render-typescript-nRHbqLbI.mjs.map} +1 -1
  85. package/dist/render-typescript.mjs +1 -1
  86. package/dist/runtime.d.mts +7 -17
  87. package/dist/runtime.d.mts.map +1 -1
  88. package/dist/runtime.mjs +2 -2
  89. package/dist/{shared-DLYdmYo-.d.mts → shared-ByhSooBS.d.mts} +6 -4
  90. package/dist/shared-ByhSooBS.d.mts.map +1 -0
  91. package/dist/{statement-builders-BSIQMClE.mjs → statement-builders-vImtdfmM.mjs} +1 -1
  92. package/dist/{statement-builders-BSIQMClE.mjs.map → statement-builders-vImtdfmM.mjs.map} +1 -1
  93. package/dist/statement-builders.mjs +1 -1
  94. package/dist/{tables-Ce_Q0I8B.mjs → tables-DZ-5Yxi0.mjs} +3 -3
  95. package/dist/tables-DZ-5Yxi0.mjs.map +1 -0
  96. package/dist/{types-Dq74Z3eu.d.mts → types-D-XIpzHA.d.mts} +1 -1
  97. package/dist/types-D-XIpzHA.d.mts.map +1 -0
  98. package/dist/types.d.mts +125 -3
  99. package/dist/types.d.mts.map +1 -0
  100. package/dist/types.mjs +2 -1
  101. package/package.json +17 -17
  102. package/src/core/migrations/issue-planner.ts +131 -41
  103. package/src/core/migrations/operations/constraints.ts +1 -1
  104. package/src/core/migrations/operations/shared.ts +4 -2
  105. package/src/core/migrations/planner-ddl-builders.ts +2 -2
  106. package/src/core/migrations/planner-schema-lookup.ts +30 -6
  107. package/src/core/migrations/planner-sql-checks.ts +29 -11
  108. package/src/core/migrations/planner-strategies.ts +138 -43
  109. package/src/core/migrations/planner.ts +14 -1
  110. package/src/core/migrations/verify-postgres-namespaces.ts +87 -0
  111. package/src/core/postgres-contract-serializer.ts +139 -60
  112. package/src/core/postgres-schema.ts +208 -0
  113. package/src/exports/types.ts +5 -0
  114. package/dist/descriptor-meta-DFUCClk_.mjs +0 -124
  115. package/dist/issue-planner-BhWVYyE1.mjs.map +0 -1
  116. package/dist/planner-DnzPpv1j.mjs.map +0 -1
  117. package/dist/planner-schema-lookup--u9whY_Y.mjs +0 -29
  118. package/dist/planner-schema-lookup--u9whY_Y.mjs.map +0 -1
  119. package/dist/planner-sql-checks-Cd016Ycs.mjs.map +0 -1
  120. package/dist/postgres-contract-serializer-D5VJk6lo.mjs +0 -61
  121. package/dist/postgres-contract-serializer-D5VJk6lo.mjs.map +0 -1
  122. package/dist/shared-DLYdmYo-.d.mts.map +0 -1
  123. package/dist/tables-Ce_Q0I8B.mjs.map +0 -1
  124. package/dist/types-Dq74Z3eu.d.mts.map +0 -1
@@ -26,14 +26,17 @@ import type {
26
26
  } from '@prisma-next/family-sql/control';
27
27
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
28
28
  import type { SchemaIssue } from '@prisma-next/framework-components/control';
29
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
29
30
  import {
30
31
  isPostgresEnumStorageEntry,
31
32
  type PostgresEnumStorageEntry,
32
33
  type SqlStorage,
34
+ type StorageTable,
33
35
  type StorageTypeInstance,
34
36
  } from '@prisma-next/sql-contract/types';
35
37
  import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
36
38
  import { PostgresEnumType } from '../postgres-enum-type';
39
+ import { isPostgresSchema } from '../postgres-schema';
37
40
  import { determineEnumDiff, readExistingEnumValues } from './enum-planning';
38
41
  import {
39
42
  AddColumnCall,
@@ -69,6 +72,74 @@ import { buildTargetDetails, type PostgresPlanTargetDetails } from './planner-ta
69
72
 
70
73
  const REBUILD_SUFFIX = '__prisma_next_new';
71
74
 
75
+ /**
76
+ * Look up a storage table by its explicit namespace coordinate. Returns
77
+ * `undefined` when the namespace has no table by that name (or no such
78
+ * namespace exists). Callers that get `undefined` MUST treat it as an
79
+ * explicit conflict — never silently fall back to a global default
80
+ * schema or a name-only walk, because that footgun would resolve a
81
+ * stale or duplicate table name to whichever namespace the iteration
82
+ * order surfaced first (a real data-loss hazard in multi-namespace
83
+ * contracts where two namespaces can carry the same table name).
84
+ */
85
+ export function tableAt(
86
+ storage: SqlStorage,
87
+ namespaceId: string,
88
+ tableName: string,
89
+ ): StorageTable | undefined {
90
+ // Namespace.tables is typed as Record<string, IRNode> at the interface level;
91
+ // SQL family namespaces always hold StorageTable instances.
92
+ return storage.namespaces[namespaceId]?.tables[tableName] as StorageTable | undefined;
93
+ }
94
+
95
+ /**
96
+ * Default namespace coordinate for an issue that does not carry one
97
+ * explicitly. Hand-crafted unit-test issues and `extra_table` issues
98
+ * fall back to `__unbound__`, the only namespace any single-namespace
99
+ * contract carries — verifier-emitted issues for legacy
100
+ * single-namespace contracts already stamp this id explicitly. Typed
101
+ * structurally so issue variants without a `namespaceId` slot
102
+ * (e.g. `EnumValuesChangedIssue`) flow through to the same fallback.
103
+ */
104
+ export function resolveNamespaceIdForIssue(issue: { readonly namespaceId?: string }): string {
105
+ return issue.namespaceId ?? UNBOUND_NAMESPACE_ID;
106
+ }
107
+
108
+ /**
109
+ * Resolve the DDL schema name for a namespace coordinate. Postgres-aware
110
+ * namespaces dispatch to their polymorphic `ddlSchemaName` override —
111
+ * named schemas return their own id and the unbound singleton projects
112
+ * to `'public'` (sibling-present) or the framework sentinel
113
+ * (sibling-absent). Legacy single-namespace contracts whose `__unbound__`
114
+ * slot is the framework-default `SqlUnboundNamespace` (rather than the
115
+ * Postgres-aware `PostgresUnboundSchema`) flow the coordinate through
116
+ * unchanged so downstream `qualifyTableName` resolves polymorphically.
117
+ */
118
+ export function resolveDdlSchemaForNamespace(ctx: StrategyContext, namespaceId: string): string {
119
+ const namespace = ctx.toContract.storage.namespaces[namespaceId];
120
+ if (isPostgresSchema(namespace)) {
121
+ return namespace.ddlSchemaName(ctx.toContract.storage);
122
+ }
123
+ return namespaceId;
124
+ }
125
+
126
+ /**
127
+ * Finds a type entry by name across all namespace type registries.
128
+ * Namespace types (e.g. Postgres enums) live under
129
+ * `storage.namespaces[nsId].types`, not under `storage.types`.
130
+ */
131
+ function locateNamespaceType(
132
+ storage: SqlStorage,
133
+ typeName: string,
134
+ ): PostgresEnumStorageEntry | undefined {
135
+ for (const ns of Object.values(storage.namespaces)) {
136
+ if (!('types' in ns) || ns.types == null) continue;
137
+ const entry = (ns.types as Record<string, PostgresEnumStorageEntry>)[typeName];
138
+ if (entry !== undefined) return entry;
139
+ }
140
+ return undefined;
141
+ }
142
+
72
143
  // ============================================================================
73
144
  // Strategy types
74
145
  // ============================================================================
@@ -120,12 +191,13 @@ export type CallMigrationStrategy = (
120
191
  | { kind: 'no_match' };
121
192
 
122
193
  function buildColumnSpec(
194
+ namespaceId: string,
123
195
  table: string,
124
196
  column: string,
125
197
  ctx: StrategyContext,
126
198
  overrides?: { nullable?: boolean },
127
199
  ) {
128
- const col = ctx.toContract.storage.tables[table]?.columns[column];
200
+ const col = tableAt(ctx.toContract.storage, namespaceId, table)?.columns[column];
129
201
  if (!col) throw new Error(`Column "${table}"."${column}" not found in destination contract`);
130
202
  const mutableHooks = ctx.codecHooks as Map<string, CodecControlHooks>;
131
203
  const mutableTypes = ctx.storageTypes as Record<
@@ -141,12 +213,13 @@ function buildColumnSpec(
141
213
  }
142
214
 
143
215
  function buildAlterTypeOptions(
216
+ namespaceId: string,
144
217
  table: string,
145
218
  column: string,
146
219
  ctx: StrategyContext,
147
220
  using?: string,
148
221
  ) {
149
- const col = ctx.toContract.storage.tables[table]?.columns[column];
222
+ const col = tableAt(ctx.toContract.storage, namespaceId, table)?.columns[column];
150
223
  if (!col) throw new Error(`Column "${table}"."${column}" not found in destination contract`);
151
224
  const mutableHooks = ctx.codecHooks as Map<string, CodecControlHooks>;
152
225
  const mutableTypes = ctx.storageTypes as Record<
@@ -175,20 +248,22 @@ export const notNullBackfillCallStrategy: CallMigrationStrategy = (issues, ctx)
175
248
  for (const issue of issues) {
176
249
  if (issue.kind !== 'missing_column' || !issue.table || !issue.column) continue;
177
250
 
178
- const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
251
+ const namespaceId = resolveNamespaceIdForIssue(issue);
252
+ const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
179
253
  if (!column) continue;
180
254
  if (column.nullable === true || column.default !== undefined) continue;
181
255
 
182
256
  matched.push(issue);
183
- const spec = buildColumnSpec(issue.table, issue.column, ctx, { nullable: true });
257
+ const spec = buildColumnSpec(namespaceId, issue.table, issue.column, ctx, { nullable: true });
258
+ const schemaForTable = resolveDdlSchemaForNamespace(ctx, namespaceId);
184
259
  calls.push(
185
- new AddColumnCall(ctx.schemaName, issue.table, spec),
260
+ new AddColumnCall(schemaForTable, issue.table, spec),
186
261
  new DataTransformCall(
187
262
  `backfill-${issue.table}-${issue.column}`,
188
263
  `backfill-${issue.table}-${issue.column}:check`,
189
264
  `backfill-${issue.table}-${issue.column}:run`,
190
265
  ),
191
- new SetNotNullCall(ctx.schemaName, issue.table, issue.column),
266
+ new SetNotNullCall(schemaForTable, issue.table, issue.column),
192
267
  );
193
268
  }
194
269
 
@@ -217,8 +292,13 @@ export const typeChangeCallStrategy: CallMigrationStrategy = (issues, ctx) => {
217
292
  for (const issue of issues) {
218
293
  if (issue.kind !== 'type_mismatch') continue;
219
294
  if (!issue.table || !issue.column) continue;
220
- const fromColumn = ctx.fromContract?.storage.tables[issue.table]?.columns[issue.column];
221
- const toColumn = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
295
+ const namespaceId = resolveNamespaceIdForIssue(issue);
296
+ const fromColumn = ctx.fromContract
297
+ ? tableAt(ctx.fromContract.storage, namespaceId, issue.table)?.columns[issue.column]
298
+ : undefined;
299
+ const toColumn = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[
300
+ issue.column
301
+ ];
222
302
  if (!fromColumn || !toColumn) continue;
223
303
  const fromType = fromColumn.nativeType;
224
304
  const toType = toColumn.nativeType;
@@ -226,9 +306,10 @@ export const typeChangeCallStrategy: CallMigrationStrategy = (issues, ctx) => {
226
306
  const isSafeWidening = SAFE_WIDENINGS.has(`${fromType}→${toType}`);
227
307
  if (!isSafeWidening && !dataAllowed) continue;
228
308
  matched.push(issue);
229
- const alterOpts = buildAlterTypeOptions(issue.table, issue.column, ctx);
309
+ const alterOpts = buildAlterTypeOptions(namespaceId, issue.table, issue.column, ctx);
310
+ const schemaForTable = resolveDdlSchemaForNamespace(ctx, namespaceId);
230
311
  if (isSafeWidening) {
231
- calls.push(new AlterColumnTypeCall(ctx.schemaName, issue.table, issue.column, alterOpts));
312
+ calls.push(new AlterColumnTypeCall(schemaForTable, issue.table, issue.column, alterOpts));
232
313
  } else {
233
314
  calls.push(
234
315
  new DataTransformCall(
@@ -236,7 +317,7 @@ export const typeChangeCallStrategy: CallMigrationStrategy = (issues, ctx) => {
236
317
  `typechange-${issue.table}-${issue.column}:check`,
237
318
  `typechange-${issue.table}-${issue.column}:run`,
238
319
  ),
239
- new AlterColumnTypeCall(ctx.schemaName, issue.table, issue.column, alterOpts),
320
+ new AlterColumnTypeCall(schemaForTable, issue.table, issue.column, alterOpts),
240
321
  );
241
322
  }
242
323
  }
@@ -261,18 +342,20 @@ export const nullableTighteningCallStrategy: CallMigrationStrategy = (issues, ct
261
342
  for (const issue of issues) {
262
343
  if (issue.kind !== 'nullability_mismatch' || !issue.table || !issue.column) continue;
263
344
 
264
- const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
345
+ const namespaceId = resolveNamespaceIdForIssue(issue);
346
+ const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[issue.column];
265
347
  if (!column) continue;
266
348
  if (column.nullable === true) continue;
267
349
 
268
350
  matched.push(issue);
351
+ const schemaForTable = resolveDdlSchemaForNamespace(ctx, namespaceId);
269
352
  calls.push(
270
353
  new DataTransformCall(
271
354
  `handle-nulls-${issue.table}-${issue.column}`,
272
355
  `handle-nulls-${issue.table}-${issue.column}:check`,
273
356
  `handle-nulls-${issue.table}-${issue.column}:run`,
274
357
  ),
275
- new SetNotNullCall(ctx.schemaName, issue.table, issue.column),
358
+ new SetNotNullCall(schemaForTable, issue.table, issue.column),
276
359
  );
277
360
  }
278
361
 
@@ -289,7 +372,7 @@ function enumRebuildCallRecipe(
289
372
  typeName: string,
290
373
  ctx: StrategyContext,
291
374
  ): readonly PostgresOpFactoryCall[] {
292
- const toType = ctx.toContract.storage.types?.[typeName];
375
+ const toType = locateNamespaceType(ctx.toContract.storage, typeName);
293
376
  if (!toType) return [];
294
377
  const isEnum = isPostgresEnumStorageEntry(toType);
295
378
  const nativeType = toType.nativeType;
@@ -298,11 +381,14 @@ function enumRebuildCallRecipe(
298
381
  : (((toType as StorageTypeInstance).typeParams['values'] ?? []) as readonly string[]);
299
382
  const tempName = `${nativeType}${REBUILD_SUFFIX}`;
300
383
 
301
- const columnRefs: { table: string; column: string }[] = [];
302
- for (const [tableName, table] of Object.entries(ctx.toContract.storage.tables)) {
303
- for (const [columnName, column] of Object.entries(table.columns)) {
304
- if (column.typeRef === typeName) {
305
- columnRefs.push({ table: tableName, column: columnName });
384
+ const columnRefs: { namespaceId: string; table: string; column: string }[] = [];
385
+ for (const [nsId, ns] of Object.entries(ctx.toContract.storage.namespaces)) {
386
+ for (const [tableName, tableNode] of Object.entries(ns.tables)) {
387
+ const table = tableNode as StorageTable;
388
+ for (const [columnName, column] of Object.entries(table.columns)) {
389
+ if (column.typeRef === typeName) {
390
+ columnRefs.push({ namespaceId: nsId, table: tableName, column: columnName });
391
+ }
306
392
  }
307
393
  }
308
394
  }
@@ -311,12 +397,17 @@ function enumRebuildCallRecipe(
311
397
  new CreateEnumTypeCall(ctx.schemaName, tempName, desiredValues),
312
398
  ...columnRefs.map((ref) => {
313
399
  const using = `${ref.column}::text::${tempName}`;
314
- return new AlterColumnTypeCall(ctx.schemaName, ref.table, ref.column, {
315
- qualifiedTargetType: tempName,
316
- formatTypeExpected: tempName,
317
- rawTargetTypeForLabel: tempName,
318
- using,
319
- });
400
+ return new AlterColumnTypeCall(
401
+ resolveDdlSchemaForNamespace(ctx, ref.namespaceId),
402
+ ref.table,
403
+ ref.column,
404
+ {
405
+ qualifiedTargetType: tempName,
406
+ formatTypeExpected: tempName,
407
+ rawTargetTypeForLabel: tempName,
408
+ using,
409
+ },
410
+ );
320
411
  }),
321
412
  new DropEnumTypeCall(ctx.schemaName, nativeType),
322
413
  new RenameTypeCall(ctx.schemaName, tempName, nativeType),
@@ -360,7 +451,7 @@ function enumRebuildCallRecipe(
360
451
  * `CreateTableCall` that references the new enum.
361
452
  */
362
453
  export const nativeEnumPlanCallStrategy: CallMigrationStrategy = (issues, ctx) => {
363
- const enumTypes = collectPostgresEnumTypes(ctx.toContract.storage.types);
454
+ const enumTypes = collectPostgresEnumTypes(ctx.toContract.storage);
364
455
  if (enumTypes.size === 0) return { kind: 'no_match' };
365
456
 
366
457
  const dataAllowed = ctx.policy.allowedOperationClasses.includes('data');
@@ -447,15 +538,15 @@ export const nativeEnumPlanCallStrategy: CallMigrationStrategy = (issues, ctx) =
447
538
  return { kind: 'match', issues: remaining, calls, recipe: emittedRebuildRecipe };
448
539
  };
449
540
 
450
- function collectPostgresEnumTypes(
451
- storageTypes: SqlStorage['types'],
452
- ): ReadonlyMap<string, PostgresEnumType> {
541
+ function collectPostgresEnumTypes(storage: SqlStorage): ReadonlyMap<string, PostgresEnumType> {
453
542
  const result = new Map<string, PostgresEnumType>();
454
- for (const [name, instance] of Object.entries(storageTypes ?? {}).sort(([a], [b]) =>
455
- a.localeCompare(b),
456
- )) {
457
- if (instance instanceof PostgresEnumType) {
458
- result.set(name, instance);
543
+ for (const ns of Object.values(storage.namespaces)) {
544
+ if (!('types' in ns) || ns.types == null) continue;
545
+ const nsTypes = ns.types as Record<string, unknown>;
546
+ for (const [name, instance] of Object.entries(nsTypes).sort(([a], [b]) => a.localeCompare(b))) {
547
+ if (instance instanceof PostgresEnumType) {
548
+ result.set(name, instance);
549
+ }
459
550
  }
460
551
  }
461
552
  return result;
@@ -555,7 +646,8 @@ export const notNullAddColumnCallStrategy: CallMigrationStrategy = (issues, ctx)
555
646
 
556
647
  for (const issue of issues) {
557
648
  if (issue.kind !== 'missing_column' || !issue.table || !issue.column) continue;
558
- const contractTable = ctx.toContract.storage.tables[issue.table];
649
+ const namespaceId = resolveNamespaceIdForIssue(issue);
650
+ const contractTable = tableAt(ctx.toContract.storage, namespaceId, issue.table);
559
651
  const column = contractTable?.columns[issue.column];
560
652
  if (!column) continue;
561
653
 
@@ -579,11 +671,13 @@ export const notNullAddColumnCallStrategy: CallMigrationStrategy = (issues, ctx)
579
671
 
580
672
  matched.push(issue);
581
673
 
674
+ const schemaForTable = resolveDdlSchemaForNamespace(ctx, namespaceId);
675
+
582
676
  if (canUseSharedTempDefault && temporaryDefault !== null) {
583
677
  calls.push(
584
678
  new RawSqlCall(
585
679
  buildAddNotNullColumnWithTemporaryDefaultOperation({
586
- schema: ctx.schemaName,
680
+ schema: schemaForTable,
587
681
  tableName: issue.table,
588
682
  columnName: issue.column,
589
683
  column,
@@ -596,16 +690,16 @@ export const notNullAddColumnCallStrategy: CallMigrationStrategy = (issues, ctx)
596
690
  continue;
597
691
  }
598
692
 
599
- const qualified = qualifyTableName(ctx.schemaName, issue.table);
693
+ const qualified = qualifyTableName(schemaForTable, issue.table);
600
694
  calls.push(
601
695
  new RawSqlCall({
602
- ...buildAddColumnOperationIdentity(ctx.schemaName, issue.table, issue.column),
696
+ ...buildAddColumnOperationIdentity(schemaForTable, issue.table, issue.column),
603
697
  operationClass: 'additive',
604
698
  precheck: [
605
699
  {
606
700
  description: `ensure column "${issue.column}" is missing`,
607
701
  sql: columnExistsCheck({
608
- schema: ctx.schemaName,
702
+ schema: schemaForTable,
609
703
  table: issue.table,
610
704
  column: issue.column,
611
705
  exists: false,
@@ -633,7 +727,7 @@ export const notNullAddColumnCallStrategy: CallMigrationStrategy = (issues, ctx)
633
727
  {
634
728
  description: `verify column "${issue.column}" exists`,
635
729
  sql: columnExistsCheck({
636
- schema: ctx.schemaName,
730
+ schema: schemaForTable,
637
731
  table: issue.table,
638
732
  column: issue.column,
639
733
  }),
@@ -641,7 +735,7 @@ export const notNullAddColumnCallStrategy: CallMigrationStrategy = (issues, ctx)
641
735
  {
642
736
  description: `verify column "${issue.column}" is NOT NULL`,
643
737
  sql: columnNullabilityCheck({
644
- schema: ctx.schemaName,
738
+ schema: schemaForTable,
645
739
  table: issue.table,
646
740
  column: issue.column,
647
741
  nullable: false,
@@ -665,7 +759,7 @@ export const notNullAddColumnCallStrategy: CallMigrationStrategy = (issues, ctx)
665
759
  // ============================================================================
666
760
 
667
761
  function canUseSharedTemporaryDefaultStrategy(options: {
668
- readonly table: NonNullable<Contract<SqlStorage>['storage']['tables'][string]>;
762
+ readonly table: StorageTable;
669
763
  readonly schemaTable: SqlSchemaIR['tables'][string];
670
764
  readonly schemaLookup: ReturnType<typeof buildSchemaLookupMap> extends ReadonlyMap<
671
765
  string,
@@ -687,7 +781,8 @@ function canUseSharedTemporaryDefaultStrategy(options: {
687
781
  }
688
782
 
689
783
  for (const foreignKey of table.foreignKeys) {
690
- if (foreignKey.constraint === false || !foreignKey.columns.includes(columnName)) continue;
784
+ if (foreignKey.constraint === false || !foreignKey.source.columns.includes(columnName))
785
+ continue;
691
786
  if (!schemaLookup || !hasForeignKey(schemaLookup, foreignKey)) return false;
692
787
  }
693
788
 
@@ -23,6 +23,7 @@ import { readExistingEnumValues } from './enum-planning';
23
23
  import { planIssues } from './issue-planner';
24
24
  import { TypeScriptRenderablePostgresMigration } from './planner-produced-postgres-migration';
25
25
  import { postgresPlannerStrategies } from './planner-strategies';
26
+ import { verifyPostgresNamespacePresence } from './verify-postgres-namespaces';
26
27
 
27
28
  type PlannerFrameworkComponents = SqlMigrationPlannerPlanOptions extends {
28
29
  readonly frameworkComponents: infer T;
@@ -222,6 +223,18 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
222
223
  readExistingEnumValues(schema, enumType.nativeType),
223
224
  };
224
225
  const verifyResult = verifySqlSchema(verifyOptions);
225
- return verifyResult.schema.issues;
226
+ // Schema presence is a Postgres-specific concern (no equivalent in
227
+ // SQLite / Mongo), so the issue emission lives in the target layer
228
+ // rather than in the family verifier. Stitch it in here so a single
229
+ // `SchemaIssue[]` flows through `planIssues` and the planner emits
230
+ // CREATE SCHEMA in the dep bucket before any CreateTableCall.
231
+ const namespaceIssues = verifyPostgresNamespacePresence({
232
+ contract: options.contract,
233
+ schema: options.schema,
234
+ });
235
+ if (namespaceIssues.length === 0) {
236
+ return verifyResult.schema.issues;
237
+ }
238
+ return [...namespaceIssues, ...verifyResult.schema.issues];
226
239
  }
227
240
  }
@@ -0,0 +1,87 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+ import type { SchemaIssue } from '@prisma-next/framework-components/control';
3
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
4
+ import type { SqlStorage } from '@prisma-next/sql-contract/types';
5
+ import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
6
+ import { isPostgresSchema } from '../postgres-schema';
7
+
8
+ /**
9
+ * Resolves the live-database schema name for a given namespace
10
+ * coordinate. Mirrors `resolveDdlSchemaForNamespace` in
11
+ * `planner-strategies.ts` so the verifier's projection and the
12
+ * planner's projection always agree — Postgres-aware namespaces (the
13
+ * production path) dispatch to `ddlSchemaName(storage)`, and bare
14
+ * object payloads (used by some tests) fall back to the coordinate
15
+ * itself.
16
+ */
17
+ function resolveDdlSchemaName(storage: SqlStorage, namespaceId: string): string {
18
+ const namespace = storage.namespaces[namespaceId];
19
+ if (isPostgresSchema(namespace)) {
20
+ return namespace.ddlSchemaName(storage);
21
+ }
22
+ return namespaceId;
23
+ }
24
+
25
+ /**
26
+ * Reads the introspected list of schema names from the Postgres-flavoured
27
+ * annotations slot on the schema IR. Defaults to the always-present
28
+ * `public` schema when introspection did not populate the slot — a fresh
29
+ * Postgres database always carries `public` (unless an operator dropped
30
+ * it manually), so any verifier path that runs without an enriched
31
+ * introspection still suppresses the redundant `CREATE SCHEMA "public"`.
32
+ *
33
+ * Production introspection (`PostgresControlAdapter.introspect`) is the
34
+ * authoritative source: it queries `pg_namespace` and writes every
35
+ * non-system schema into `annotations.pg.existingSchemas`. Tests that
36
+ * want to assert against a richer initial state pass the slot
37
+ * explicitly via the schema IR.
38
+ */
39
+ function existingSchemasFromSchema(schema: SqlSchemaIR): readonly string[] {
40
+ const annotations = (schema as { annotations?: { pg?: { existingSchemas?: unknown } } })
41
+ .annotations;
42
+ const slot = annotations?.pg?.existingSchemas;
43
+ if (Array.isArray(slot)) {
44
+ return slot.filter((s): s is string => typeof s === 'string');
45
+ }
46
+ return ['public'];
47
+ }
48
+
49
+ /**
50
+ * Emits a `missing_schema` issue for every contract-declared Postgres
51
+ * namespace whose live container does not yet exist.
52
+ *
53
+ * A namespace's live container is the schema returned by its
54
+ * polymorphic `ddlSchemaName(storage)` method — named schemas resolve
55
+ * to their own id, the unbound singleton projects to `public` (sibling
56
+ * present) or the framework sentinel (sibling absent). Issues are
57
+ * emitted only when the resolved name is a real, creatable schema
58
+ * (not the unbound sentinel) and is missing from the introspected
59
+ * list. `public` is suppressed implicitly because the introspection
60
+ * (or its sensible default) always carries it.
61
+ *
62
+ * Each emitted issue stamps `namespaceId` with the contract namespace
63
+ * coordinate so the downstream `mapIssueToCall` re-resolves the DDL
64
+ * schema name through the same polymorphic path — keeping the
65
+ * coordinate, not the resolved name, as the issue's stable identity.
66
+ */
67
+ export function verifyPostgresNamespacePresence(input: {
68
+ readonly contract: Contract<SqlStorage>;
69
+ readonly schema: SqlSchemaIR;
70
+ }): readonly SchemaIssue[] {
71
+ const { contract, schema } = input;
72
+ const existing = new Set(existingSchemasFromSchema(schema));
73
+ const issues: SchemaIssue[] = [];
74
+ const namespaceIds = Object.keys(contract.storage.namespaces).sort();
75
+ for (const namespaceId of namespaceIds) {
76
+ if (namespaceId === UNBOUND_NAMESPACE_ID) continue;
77
+ const ddlName = resolveDdlSchemaName(contract.storage, namespaceId);
78
+ if (ddlName === UNBOUND_NAMESPACE_ID) continue;
79
+ if (existing.has(ddlName)) continue;
80
+ issues.push({
81
+ kind: 'missing_schema',
82
+ namespaceId,
83
+ message: `Schema "${ddlName}" is missing from database`,
84
+ });
85
+ }
86
+ return issues;
87
+ }