@prisma-next/target-postgres 0.9.0 → 0.10.0-dev.10

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
@@ -22,6 +22,7 @@ import type {
22
22
  PostgresEnumStorageEntry,
23
23
  SqlStorage,
24
24
  StorageColumn,
25
+ StorageTable,
25
26
  StorageTypeInstance,
26
27
  } from '@prisma-next/sql-contract/types';
27
28
  import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
@@ -36,6 +37,7 @@ import {
36
37
  AlterColumnTypeCall,
37
38
  CreateEnumTypeCall,
38
39
  CreateIndexCall,
40
+ CreateSchemaCall,
39
41
  CreateTableCall,
40
42
  DropColumnCall,
41
43
  DropConstraintCall,
@@ -53,17 +55,33 @@ import { buildExpectedFormatType } from './planner-sql-checks';
53
55
  import {
54
56
  type CallMigrationStrategy,
55
57
  postgresPlannerStrategies,
58
+ resolveDdlSchemaForNamespace,
59
+ resolveNamespaceIdForIssue,
56
60
  type StrategyContext,
61
+ tableAt,
57
62
  } from './planner-strategies';
58
63
 
59
64
  export type { CallMigrationStrategy, StrategyContext };
60
65
 
66
+ function locateNamespaceTypeInStorage(storage: SqlStorage, typeName: string): unknown {
67
+ for (const ns of Object.values(storage.namespaces)) {
68
+ if (!('types' in ns) || ns.types == null) continue;
69
+ const entry = (ns.types as Record<string, unknown>)[typeName];
70
+ if (entry !== undefined) return entry;
71
+ }
72
+ return undefined;
73
+ }
74
+
61
75
  // ============================================================================
62
76
  // Issue kind ordering (dependency order)
63
77
  // ============================================================================
64
78
 
65
79
  const ISSUE_KIND_ORDER: Record<string, number> = {
66
- // Types first
80
+ // Schemas first — the database container must exist before any DDL
81
+ // that targets it can run.
82
+ missing_schema: 1,
83
+
84
+ // Types next
67
85
  type_missing: 2,
68
86
  type_values_mismatch: 3,
69
87
  enum_values_changed: 3,
@@ -183,22 +201,45 @@ function mapIssueToCall(
183
201
  ctx: StrategyContext,
184
202
  ): Result<readonly PostgresOpFactoryCall[], SqlPlannerConflict> {
185
203
  const { schemaName, codecHooks, storageTypes } = ctx;
204
+ // Per-table effective schema. `extra_table` issues intentionally
205
+ // omit `namespaceId` — the live DB carries a table that
206
+ // is not claimed by any contract namespace, so there is no contract
207
+ // coordinate to project from. Those issues fall back to the planner's
208
+ // global `ctx.schemaName`; every other issue dispatches through the
209
+ // resolved namespace's polymorphic `ddlSchemaName`.
210
+ const tableSchema = (issue: SchemaIssue): string => {
211
+ if (issue.kind === 'extra_table') return schemaName;
212
+ if (!('table' in issue) || !issue.table) return schemaName;
213
+ return resolveDdlSchemaForNamespace(ctx, resolveNamespaceIdForIssue(issue));
214
+ };
186
215
 
187
216
  switch (issue.kind) {
217
+ case 'missing_schema': {
218
+ const namespaceId = issue.namespaceId;
219
+ if (!namespaceId)
220
+ return notOk(
221
+ issueConflict('unsupportedOperation', 'Missing schema issue has no namespaceId'),
222
+ );
223
+ const ddlSchemaName = resolveDdlSchemaForNamespace(ctx, namespaceId);
224
+ return ok([new CreateSchemaCall(ddlSchemaName)]);
225
+ }
226
+
188
227
  case 'missing_table': {
189
228
  if (!issue.table)
190
229
  return notOk(
191
230
  issueConflict('unsupportedOperation', 'Missing table issue has no table name'),
192
231
  );
193
- const contractTable = ctx.toContract.storage.tables[issue.table];
232
+ const namespaceId = resolveNamespaceIdForIssue(issue);
233
+ const contractTable = tableAt(ctx.toContract.storage, namespaceId, issue.table);
194
234
  if (!contractTable) {
195
235
  return notOk(
196
236
  issueConflict(
197
237
  'unsupportedOperation',
198
- `Table "${issue.table}" reported missing but not found in destination contract`,
238
+ `Table "${issue.table}" in namespace "${namespaceId}" reported missing but not found in destination contract`,
199
239
  ),
200
240
  );
201
241
  }
242
+ const schemaForTable = tableSchema(issue);
202
243
  const columns: ColumnSpec[] = Object.entries(contractTable.columns).map(([name, column]) =>
203
244
  toColumnSpec(name, column, codecHooks, storageTypes),
204
245
  );
@@ -206,7 +247,7 @@ function mapIssueToCall(
206
247
  ? { columns: contractTable.primaryKey.columns }
207
248
  : undefined;
208
249
  const calls: PostgresOpFactoryCall[] = [
209
- new CreateTableCall(schemaName, issue.table, columns, primaryKey),
250
+ new CreateTableCall(schemaForTable, issue.table, columns, primaryKey),
210
251
  ];
211
252
  for (const index of contractTable.indexes) {
212
253
  const indexName = index.name ?? `${issue.table}_${index.columns.join('_')}_idx`;
@@ -214,7 +255,7 @@ function mapIssueToCall(
214
255
  if (index.type !== undefined) extras.type = index.type;
215
256
  if (index.options !== undefined) extras.options = index.options;
216
257
  calls.push(
217
- new CreateIndexCall(schemaName, issue.table, indexName, [...index.columns], extras),
258
+ new CreateIndexCall(schemaForTable, issue.table, indexName, [...index.columns], extras),
218
259
  );
219
260
  }
220
261
  const explicitIndexColumnSets = new Set(
@@ -222,24 +263,32 @@ function mapIssueToCall(
222
263
  );
223
264
  for (const fk of contractTable.foreignKeys) {
224
265
  if (fk.constraint) {
225
- const fkName = fk.name ?? `${issue.table}_${fk.columns.join('_')}_fkey`;
266
+ const fkName = fk.name ?? `${issue.table}_${fk.source.columns.join('_')}_fkey`;
226
267
  const fkSpec: ForeignKeySpec = {
227
268
  name: fkName,
228
- columns: fk.columns,
229
- references: { table: fk.references.table, columns: fk.references.columns },
269
+ columns: fk.source.columns,
270
+ references: {
271
+ schema: fk.target.namespaceId,
272
+ table: fk.target.tableName,
273
+ columns: fk.target.columns,
274
+ },
230
275
  ...(fk.onDelete !== undefined && { onDelete: fk.onDelete }),
231
276
  ...(fk.onUpdate !== undefined && { onUpdate: fk.onUpdate }),
232
277
  };
233
- calls.push(new AddForeignKeyCall(schemaName, issue.table, fkSpec));
278
+ calls.push(new AddForeignKeyCall(schemaForTable, issue.table, fkSpec));
234
279
  }
235
- if (fk.index && !explicitIndexColumnSets.has(fk.columns.join(','))) {
236
- const indexName = `${issue.table}_${fk.columns.join('_')}_idx`;
237
- calls.push(new CreateIndexCall(schemaName, issue.table, indexName, [...fk.columns]));
280
+ if (fk.index && !explicitIndexColumnSets.has(fk.source.columns.join(','))) {
281
+ const indexName = `${issue.table}_${fk.source.columns.join('_')}_idx`;
282
+ calls.push(
283
+ new CreateIndexCall(schemaForTable, issue.table, indexName, [...fk.source.columns]),
284
+ );
238
285
  }
239
286
  }
240
287
  for (const unique of contractTable.uniques) {
241
288
  const constraintName = unique.name ?? `${issue.table}_${unique.columns.join('_')}_key`;
242
- calls.push(new AddUniqueCall(schemaName, issue.table, constraintName, [...unique.columns]));
289
+ calls.push(
290
+ new AddUniqueCall(schemaForTable, issue.table, constraintName, [...unique.columns]),
291
+ );
243
292
  }
244
293
  return ok(calls);
245
294
  }
@@ -250,7 +299,10 @@ function mapIssueToCall(
250
299
  issueConflict('unsupportedOperation', 'Missing column issue has no table/column name'),
251
300
  );
252
301
  {
253
- const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
302
+ const namespaceId = resolveNamespaceIdForIssue(issue);
303
+ const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[
304
+ issue.column
305
+ ];
254
306
  if (!column)
255
307
  return notOk(
256
308
  issueConflict(
@@ -260,7 +312,7 @@ function mapIssueToCall(
260
312
  );
261
313
  return ok([
262
314
  new AddColumnCall(
263
- schemaName,
315
+ tableSchema(issue),
264
316
  issue.table,
265
317
  toColumnSpec(issue.column, column, codecHooks, storageTypes),
266
318
  ),
@@ -273,7 +325,10 @@ function mapIssueToCall(
273
325
  issueConflict('unsupportedOperation', 'Default missing issue has no table/column name'),
274
326
  );
275
327
  {
276
- const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
328
+ const namespaceId = resolveNamespaceIdForIssue(issue);
329
+ const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[
330
+ issue.column
331
+ ];
277
332
  if (!column?.default) {
278
333
  return notOk(
279
334
  issueConflict(
@@ -284,27 +339,27 @@ function mapIssueToCall(
284
339
  }
285
340
  const defaultSql = buildColumnDefaultSql(column.default, column);
286
341
  if (!defaultSql) return ok([]);
287
- return ok([new SetDefaultCall(schemaName, issue.table, issue.column, defaultSql)]);
342
+ return ok([new SetDefaultCall(tableSchema(issue), issue.table, issue.column, defaultSql)]);
288
343
  }
289
344
 
290
345
  case 'extra_table':
291
346
  if (!issue.table)
292
347
  return notOk(issueConflict('unsupportedOperation', 'Extra table issue has no table name'));
293
- return ok([new DropTableCall(schemaName, issue.table)]);
348
+ return ok([new DropTableCall(tableSchema(issue), issue.table)]);
294
349
 
295
350
  case 'extra_column':
296
351
  if (!issue.table || !issue.column)
297
352
  return notOk(
298
353
  issueConflict('unsupportedOperation', 'Extra column issue has no table/column name'),
299
354
  );
300
- return ok([new DropColumnCall(schemaName, issue.table, issue.column)]);
355
+ return ok([new DropColumnCall(tableSchema(issue), issue.table, issue.column)]);
301
356
 
302
357
  case 'extra_index':
303
358
  if (!issue.table || !issue.indexOrConstraint)
304
359
  return notOk(
305
360
  issueConflict('unsupportedOperation', 'Extra index issue has no table/index name'),
306
361
  );
307
- return ok([new DropIndexCall(schemaName, issue.table, issue.indexOrConstraint)]);
362
+ return ok([new DropIndexCall(tableSchema(issue), issue.table, issue.indexOrConstraint)]);
308
363
 
309
364
  case 'extra_unique_constraint':
310
365
  case 'extra_foreign_key':
@@ -336,7 +391,12 @@ function mapIssueToCall(
336
391
  extra_primary_key: 'primaryKey' as const,
337
392
  };
338
393
  return ok([
339
- new DropConstraintCall(schemaName, issue.table, constraintName, kindMap[issue.kind]),
394
+ new DropConstraintCall(
395
+ tableSchema(issue),
396
+ issue.table,
397
+ constraintName,
398
+ kindMap[issue.kind],
399
+ ),
340
400
  ]);
341
401
  }
342
402
 
@@ -345,14 +405,17 @@ function mapIssueToCall(
345
405
  return notOk(
346
406
  issueConflict('unsupportedOperation', 'Extra default issue has no table/column name'),
347
407
  );
348
- return ok([new DropDefaultCall(schemaName, issue.table, issue.column)]);
408
+ return ok([new DropDefaultCall(tableSchema(issue), issue.table, issue.column)]);
349
409
 
350
410
  case 'nullability_mismatch': {
351
411
  if (!issue.table || !issue.column)
352
412
  return notOk(
353
413
  issueConflict('nullabilityConflict', 'Nullability mismatch has no table/column name'),
354
414
  );
355
- const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
415
+ const namespaceId = resolveNamespaceIdForIssue(issue);
416
+ const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[
417
+ issue.column
418
+ ];
356
419
  if (!column)
357
420
  return notOk(
358
421
  issueConflict(
@@ -360,10 +423,11 @@ function mapIssueToCall(
360
423
  `Column "${issue.table}"."${issue.column}" not found in destination contract`,
361
424
  ),
362
425
  );
426
+ const schemaForTable = tableSchema(issue);
363
427
  return ok(
364
428
  column.nullable
365
- ? [new DropNotNullCall(schemaName, issue.table, issue.column)]
366
- : [new SetNotNullCall(schemaName, issue.table, issue.column)],
429
+ ? [new DropNotNullCall(schemaForTable, issue.table, issue.column)]
430
+ : [new SetNotNullCall(schemaForTable, issue.table, issue.column)],
367
431
  );
368
432
  }
369
433
 
@@ -371,7 +435,10 @@ function mapIssueToCall(
371
435
  if (!issue.table || !issue.column)
372
436
  return notOk(issueConflict('typeMismatch', 'Type mismatch has no table/column name'));
373
437
  {
374
- const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
438
+ const namespaceId = resolveNamespaceIdForIssue(issue);
439
+ const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[
440
+ issue.column
441
+ ];
375
442
  if (!column)
376
443
  return notOk(
377
444
  issueConflict(
@@ -387,7 +454,7 @@ function mapIssueToCall(
387
454
  const qualifiedTargetType = buildColumnTypeSql(column, hooksMap, typesMap, false);
388
455
  const formatTypeExpected = buildExpectedFormatType(column, hooksMap, typesMap);
389
456
  return ok([
390
- new AlterColumnTypeCall(schemaName, issue.table, issue.column, {
457
+ new AlterColumnTypeCall(tableSchema(issue), issue.table, issue.column, {
391
458
  qualifiedTargetType,
392
459
  formatTypeExpected,
393
460
  rawTargetTypeForLabel: qualifiedTargetType,
@@ -401,12 +468,15 @@ function mapIssueToCall(
401
468
  issueConflict('unsupportedOperation', 'Default mismatch has no table/column name'),
402
469
  );
403
470
  {
404
- const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
471
+ const namespaceId = resolveNamespaceIdForIssue(issue);
472
+ const column = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.columns[
473
+ issue.column
474
+ ];
405
475
  if (!column?.default) return ok([]);
406
476
  const defaultSql = buildColumnDefaultSql(column.default, column);
407
477
  if (!defaultSql) return ok([]);
408
478
  return ok([
409
- new SetDefaultCall(schemaName, issue.table, issue.column, defaultSql, 'widening'),
479
+ new SetDefaultCall(tableSchema(issue), issue.table, issue.column, defaultSql, 'widening'),
410
480
  ]);
411
481
  }
412
482
 
@@ -414,13 +484,16 @@ function mapIssueToCall(
414
484
  if (!issue.table)
415
485
  return notOk(issueConflict('indexIncompatible', 'Primary key issue has no table name'));
416
486
  if (isMissing(issue)) {
417
- const pk = ctx.toContract.storage.tables[issue.table]?.primaryKey;
487
+ const namespaceId = resolveNamespaceIdForIssue(issue);
488
+ const pk = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.primaryKey;
418
489
  if (!pk)
419
490
  return notOk(
420
491
  issueConflict('indexIncompatible', `No primary key in contract for "${issue.table}"`),
421
492
  );
422
493
  const constraintName = pk.name ?? `${issue.table}_pkey`;
423
- return ok([new AddPrimaryKeyCall(schemaName, issue.table, constraintName, pk.columns)]);
494
+ return ok([
495
+ new AddPrimaryKeyCall(tableSchema(issue), issue.table, constraintName, pk.columns),
496
+ ]);
424
497
  }
425
498
  return notOk(
426
499
  issueConflict(
@@ -438,7 +511,7 @@ function mapIssueToCall(
438
511
  if (isMissing(issue) && issue.expected) {
439
512
  const columns = issue.expected.split(', ');
440
513
  const constraintName = `${issue.table}_${columns.join('_')}_key`;
441
- return ok([new AddUniqueCall(schemaName, issue.table, constraintName, columns)]);
514
+ return ok([new AddUniqueCall(tableSchema(issue), issue.table, constraintName, columns)]);
442
515
  }
443
516
  return notOk(
444
517
  issueConflict(
@@ -452,15 +525,22 @@ function mapIssueToCall(
452
525
  if (!issue.table)
453
526
  return notOk(issueConflict('indexIncompatible', 'Index issue has no table name'));
454
527
  if (isMissing(issue) && issue.expected) {
528
+ const namespaceId = resolveNamespaceIdForIssue(issue);
455
529
  const columns = issue.expected.split(', ');
456
- const contractIndex = ctx.toContract.storage.tables[issue.table]?.indexes.find((idx) =>
530
+ const contractIndex = tableAt(
531
+ ctx.toContract.storage,
532
+ namespaceId,
533
+ issue.table,
534
+ )?.indexes.find((idx: StorageTable['indexes'][number]) =>
457
535
  arraysEqual(idx.columns, columns),
458
536
  );
459
537
  const indexName = contractIndex?.name ?? `${issue.table}_${columns.join('_')}_idx`;
460
538
  const extras: { type?: string; options?: Record<string, unknown> } = {};
461
539
  if (contractIndex?.type !== undefined) extras.type = contractIndex.type;
462
540
  if (contractIndex?.options !== undefined) extras.options = contractIndex.options;
463
- return ok([new CreateIndexCall(schemaName, issue.table, indexName, columns, extras)]);
541
+ return ok([
542
+ new CreateIndexCall(tableSchema(issue), issue.table, indexName, columns, extras),
543
+ ]);
464
544
  }
465
545
  return notOk(
466
546
  issueConflict(
@@ -476,20 +556,25 @@ function mapIssueToCall(
476
556
  if (isMissing(issue) && issue.expected) {
477
557
  const arrowIdx = issue.expected.indexOf(' -> ');
478
558
  if (arrowIdx >= 0) {
559
+ const namespaceId = resolveNamespaceIdForIssue(issue);
479
560
  const columns = issue.expected.slice(0, arrowIdx).split(', ');
480
561
  const fkName = `${issue.table}_${columns.join('_')}_fkey`;
481
- const fk = ctx.toContract.storage.tables[issue.table]?.foreignKeys.find(
482
- (k) => k.columns.join(', ') === columns.join(', '),
562
+ const fk = tableAt(ctx.toContract.storage, namespaceId, issue.table)?.foreignKeys.find(
563
+ (k) => k.source.columns.join(', ') === columns.join(', '),
483
564
  );
484
565
  if (fk) {
485
566
  const fkSpec: ForeignKeySpec = {
486
567
  name: fkName,
487
- columns: fk.columns,
488
- references: { table: fk.references.table, columns: fk.references.columns },
568
+ columns: fk.source.columns,
569
+ references: {
570
+ schema: fk.target.namespaceId,
571
+ table: fk.target.tableName,
572
+ columns: fk.target.columns,
573
+ },
489
574
  ...(fk.onDelete !== undefined && { onDelete: fk.onDelete }),
490
575
  ...(fk.onUpdate !== undefined && { onUpdate: fk.onUpdate }),
491
576
  };
492
- return ok([new AddForeignKeyCall(schemaName, issue.table, fkSpec)]);
577
+ return ok([new AddForeignKeyCall(tableSchema(issue), issue.table, fkSpec)]);
493
578
  }
494
579
  return notOk(
495
580
  issueConflict(
@@ -511,7 +596,11 @@ function mapIssueToCall(
511
596
  case 'type_missing': {
512
597
  if (!issue.typeName)
513
598
  return notOk(issueConflict('unsupportedOperation', 'Type missing issue has no typeName'));
514
- const typeInstance = ctx.toContract.storage.types?.[issue.typeName];
599
+ // Enum types live in namespace.types; codec aliases live in storage.types.
600
+ // Check both so the planner handles whichever slot the type is in.
601
+ const typeInstance: unknown =
602
+ ctx.toContract.storage.types?.[issue.typeName] ??
603
+ locateNamespaceTypeInStorage(ctx.toContract.storage, issue.typeName);
515
604
  if (!typeInstance) {
516
605
  return notOk(
517
606
  issueConflict(
@@ -530,10 +619,11 @@ function mapIssueToCall(
530
619
  ),
531
620
  ]);
532
621
  }
622
+ const codecInstance = typeInstance as StorageTypeInstance;
533
623
  return notOk(
534
624
  issueConflict(
535
625
  'unsupportedOperation',
536
- `Type "${issue.typeName}" uses codec "${typeInstance.codecId}" — only enum types are supported`,
626
+ `Type "${issue.typeName}" uses codec "${codecInstance.codecId}" — only enum types are supported`,
537
627
  ),
538
628
  );
539
629
  }
@@ -15,7 +15,7 @@ function renderForeignKeySql(schemaName: string, tableName: string, fk: ForeignK
15
15
  let sql = `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
16
16
  ADD CONSTRAINT ${quoteIdentifier(fk.name)}
17
17
  FOREIGN KEY (${fk.columns.map(quoteIdentifier).join(', ')})
18
- REFERENCES ${qualifyTableName(schemaName, fk.references.table)} (${fk.references.columns
18
+ REFERENCES ${qualifyTableName(fk.references.schema, fk.references.table)} (${fk.references.columns
19
19
  .map(quoteIdentifier)
20
20
  .join(', ')})`;
21
21
 
@@ -26,13 +26,15 @@ export interface ColumnSpec {
26
26
  }
27
27
 
28
28
  /**
29
- * Literal-args shape for a foreign key definition. The referenced table is
30
- * assumed to live in the same schema as the constrained table.
29
+ * Literal-args shape for a foreign key definition. `references.schema`
30
+ * carries the target table's namespace (schema) coordinate so the rendered
31
+ * DDL qualifies the REFERENCES clause correctly for cross-schema FKs.
31
32
  */
32
33
  export interface ForeignKeySpec {
33
34
  readonly name: string;
34
35
  readonly columns: readonly string[];
35
36
  readonly references: {
37
+ readonly schema: string;
36
38
  readonly table: string;
37
39
  readonly columns: readonly string[];
38
40
  };
@@ -233,8 +233,8 @@ export function buildForeignKeySql(
233
233
  ): string {
234
234
  let sql = `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
235
235
  ADD CONSTRAINT ${quoteIdentifier(fkName)}
236
- FOREIGN KEY (${foreignKey.columns.map(quoteIdentifier).join(', ')})
237
- REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${foreignKey.references.columns
236
+ FOREIGN KEY (${foreignKey.source.columns.map(quoteIdentifier).join(', ')})
237
+ REFERENCES ${qualifyTableName(schemaName, foreignKey.target.tableName)} (${foreignKey.target.columns
238
238
  .map(quoteIdentifier)
239
239
  .join(', ')})`;
240
240
 
@@ -1,3 +1,4 @@
1
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
1
2
  import type { ForeignKey } from '@prisma-next/sql-contract/types';
2
3
  import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
3
4
 
@@ -26,11 +27,21 @@ function buildSchemaTableLookup(table: SqlSchemaIR['tables'][string]): SchemaTab
26
27
  const uniqueIndexKeys = new Set(
27
28
  table.indexes.filter((i) => i.unique).map((i) => i.columns.join(',')),
28
29
  );
29
- const fkKeys = new Set(
30
- table.foreignKeys.map(
31
- (fk) => `${fk.columns.join(',')}|${fk.referencedTable}|${fk.referencedColumns.join(',')}`,
32
- ),
33
- );
30
+ const fkKeys = new Set<string>();
31
+ for (const fk of table.foreignKeys) {
32
+ // Keys are JSON-encoded tuples so identifiers containing any character
33
+ // (including the column-list comma or pipe characters) cannot collide
34
+ // across structurally-distinct FKs. Unqualified keys are 3-tuples
35
+ // (cols, table, refCols); qualified keys are 4-tuples
36
+ // (cols, schema, table, refCols) — the arity difference makes the two
37
+ // key shapes fundamentally non-collidable.
38
+ fkKeys.add(JSON.stringify([fk.columns, fk.referencedTable, fk.referencedColumns]));
39
+ if (fk.referencedSchema !== undefined) {
40
+ fkKeys.add(
41
+ JSON.stringify([fk.columns, fk.referencedSchema, fk.referencedTable, fk.referencedColumns]),
42
+ );
43
+ }
44
+ }
34
45
  return { uniqueKeys, indexKeys, uniqueIndexKeys, fkKeys };
35
46
  }
36
47
 
@@ -48,7 +59,20 @@ export function hasIndex(lookup: SchemaTableLookup, columns: readonly string[]):
48
59
  }
49
60
 
50
61
  export function hasForeignKey(lookup: SchemaTableLookup, fk: ForeignKey): boolean {
62
+ // Mirror the encoding produced by buildSchemaTableLookup exactly:
63
+ // unqualified 3-tuple for unbound-namespace FKs, qualified 4-tuple for
64
+ // bound-namespace FKs.
65
+ if (fk.target.namespaceId === UNBOUND_NAMESPACE_ID) {
66
+ return lookup.fkKeys.has(
67
+ JSON.stringify([fk.source.columns, fk.target.tableName, fk.target.columns]),
68
+ );
69
+ }
51
70
  return lookup.fkKeys.has(
52
- `${fk.columns.join(',')}|${fk.references.table}|${fk.references.columns.join(',')}`,
71
+ JSON.stringify([
72
+ fk.source.columns,
73
+ fk.target.namespaceId,
74
+ fk.target.tableName,
75
+ fk.target.columns,
76
+ ]),
53
77
  );
54
78
  }
@@ -4,16 +4,27 @@ import type {
4
4
  StorageColumn,
5
5
  StorageTypeInstance,
6
6
  } from '@prisma-next/sql-contract/types';
7
+ import { postgresCreateNamespace } from '../postgres-schema';
7
8
  import { escapeLiteral, quoteIdentifier } from '../sql-utils';
8
9
  import { resolveColumnTypeMetadata } from './planner-type-resolution';
9
10
 
11
+ /**
12
+ * String-keyed entry points the migration ops use to render
13
+ * schema-qualified DDL and catalog checks. The `schema` argument is
14
+ * interpreted as a namespace coordinate: the framework `__unbound__`
15
+ * sentinel resolves to the late-bound `PostgresUnboundSchema` singleton
16
+ * (which elides the qualifier so `search_path` decides at runtime); any
17
+ * other id materialises a `PostgresSchema(id)` whose qualifier is the
18
+ * named schema. Helpers route through these `Namespace` concretions so
19
+ * the unbound branch lives in the polymorphic override, not the call
20
+ * site.
21
+ */
10
22
  export function qualifyTableName(schema: string, table: string): string {
11
- return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
23
+ return postgresCreateNamespace({ id: schema }).qualifyTable(table);
12
24
  }
13
25
 
14
26
  export function toRegclassLiteral(schema: string, name: string): string {
15
- const regclass = `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
16
- return `'${escapeLiteral(regclass)}'`;
27
+ return postgresCreateNamespace({ id: schema }).regclassLiteral(name);
17
28
  }
18
29
 
19
30
  /**
@@ -32,15 +43,16 @@ export function constraintExistsCheck({
32
43
  table?: string;
33
44
  exists?: boolean;
34
45
  }): string {
46
+ const namespace = postgresCreateNamespace({ id: schema });
35
47
  const existsClause = exists ? 'EXISTS' : 'NOT EXISTS';
36
48
  const tableFilter = table
37
- ? `AND c.conrelid = to_regclass(${toRegclassLiteral(schema, table)})`
49
+ ? `AND c.conrelid = to_regclass(${namespace.regclassLiteral(table)})`
38
50
  : '';
39
51
  return `SELECT ${existsClause} (
40
52
  SELECT 1 FROM pg_constraint c
41
53
  JOIN pg_namespace n ON c.connamespace = n.oid
42
54
  WHERE c.conname = '${escapeLiteral(constraintName)}'
43
- AND n.nspname = '${escapeLiteral(schema)}'
55
+ AND n.nspname = ${namespace.schemaSqlExpression()}
44
56
  ${tableFilter}
45
57
  )`;
46
58
  }
@@ -56,11 +68,12 @@ export function columnExistsCheck({
56
68
  column: string;
57
69
  exists?: boolean;
58
70
  }): string {
71
+ const namespace = postgresCreateNamespace({ id: schema });
59
72
  const existsClause = exists ? '' : 'NOT ';
60
73
  return `SELECT ${existsClause}EXISTS (
61
74
  SELECT 1
62
75
  FROM information_schema.columns
63
- WHERE table_schema = '${escapeLiteral(schema)}'
76
+ WHERE table_schema = ${namespace.schemaSqlExpression()}
64
77
  AND table_name = '${escapeLiteral(table)}'
65
78
  AND column_name = '${escapeLiteral(column)}'
66
79
  )`;
@@ -77,11 +90,12 @@ export function columnNullabilityCheck({
77
90
  column: string;
78
91
  nullable: boolean;
79
92
  }): string {
93
+ const namespace = postgresCreateNamespace({ id: schema });
80
94
  const expected = nullable ? 'YES' : 'NO';
81
95
  return `SELECT EXISTS (
82
96
  SELECT 1
83
97
  FROM information_schema.columns
84
- WHERE table_schema = '${escapeLiteral(schema)}'
98
+ WHERE table_schema = ${namespace.schemaSqlExpression()}
85
99
  AND table_name = '${escapeLiteral(table)}'
86
100
  AND column_name = '${escapeLiteral(column)}'
87
101
  AND is_nullable = '${expected}'
@@ -97,10 +111,11 @@ export function columnHasNoDefaultCheck(opts: {
97
111
  table: string;
98
112
  column: string;
99
113
  }): string {
114
+ const namespace = postgresCreateNamespace({ id: opts.schema });
100
115
  return `SELECT NOT EXISTS (
101
116
  SELECT 1
102
117
  FROM information_schema.columns
103
- WHERE table_schema = '${escapeLiteral(opts.schema)}'
118
+ WHERE table_schema = ${namespace.schemaSqlExpression()}
104
119
  AND table_name = '${escapeLiteral(opts.table)}'
105
120
  AND column_name = '${escapeLiteral(opts.column)}'
106
121
  AND column_default IS NOT NULL
@@ -267,12 +282,13 @@ export function columnTypeCheck({
267
282
  column: string;
268
283
  expectedType: string;
269
284
  }): string {
285
+ const namespace = postgresCreateNamespace({ id: schema });
270
286
  return `SELECT EXISTS (
271
287
  SELECT 1
272
288
  FROM pg_attribute a
273
289
  JOIN pg_class c ON c.oid = a.attrelid
274
290
  JOIN pg_namespace n ON n.oid = c.relnamespace
275
- WHERE n.nspname = '${escapeLiteral(schema)}'
291
+ WHERE n.nspname = ${namespace.schemaSqlExpression()}
276
292
  AND c.relname = '${escapeLiteral(table)}'
277
293
  AND a.attname = '${escapeLiteral(column)}'
278
294
  AND format_type(a.atttypid, a.atttypmod) = '${escapeLiteral(expectedType)}'
@@ -291,11 +307,12 @@ export function columnDefaultExistsCheck({
291
307
  column: string;
292
308
  exists?: boolean;
293
309
  }): string {
310
+ const namespace = postgresCreateNamespace({ id: schema });
294
311
  const nullCheck = exists ? 'IS NOT NULL' : 'IS NULL';
295
312
  return `SELECT EXISTS (
296
313
  SELECT 1
297
314
  FROM information_schema.columns
298
- WHERE table_schema = '${escapeLiteral(schema)}'
315
+ WHERE table_schema = ${namespace.schemaSqlExpression()}
299
316
  AND table_name = '${escapeLiteral(table)}'
300
317
  AND column_name = '${escapeLiteral(column)}'
301
318
  AND column_default ${nullCheck}
@@ -308,6 +325,7 @@ export function tableHasPrimaryKeyCheck(
308
325
  exists: boolean,
309
326
  constraintName?: string,
310
327
  ): string {
328
+ const namespace = postgresCreateNamespace({ id: schema });
311
329
  const comparison = exists ? '' : 'NOT ';
312
330
  const constraintFilter = constraintName
313
331
  ? `AND c2.relname = '${escapeLiteral(constraintName)}'`
@@ -318,7 +336,7 @@ export function tableHasPrimaryKeyCheck(
318
336
  JOIN pg_class c ON c.oid = i.indrelid
319
337
  JOIN pg_namespace n ON n.oid = c.relnamespace
320
338
  LEFT JOIN pg_class c2 ON c2.oid = i.indexrelid
321
- WHERE n.nspname = '${escapeLiteral(schema)}'
339
+ WHERE n.nspname = ${namespace.schemaSqlExpression()}
322
340
  AND c.relname = '${escapeLiteral(table)}'
323
341
  AND i.indisprimary
324
342
  ${constraintFilter}