@prisma-next/target-postgres 0.11.0-dev.9 → 0.12.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 (112) hide show
  1. package/dist/codec-types-CRlHq7Cz.d.mts.map +1 -1
  2. package/dist/codecs-Dud5KDNk.d.mts.map +1 -1
  3. package/dist/codecs.d.mts.map +1 -1
  4. package/dist/codecs.mjs.map +1 -1
  5. package/dist/control.d.mts.map +1 -1
  6. package/dist/control.mjs +16 -39
  7. package/dist/control.mjs.map +1 -1
  8. package/dist/data-transform-CdtGUWp2.mjs.map +1 -1
  9. package/dist/data-transform-bmOKkygi.d.mts.map +1 -1
  10. package/dist/{default-normalizer-DHCsbfjc.mjs → default-normalizer-CRscvhS5.mjs} +1 -1
  11. package/dist/{default-normalizer-DHCsbfjc.mjs.map → default-normalizer-CRscvhS5.mjs.map} +1 -1
  12. package/dist/default-normalizer.d.mts.map +1 -1
  13. package/dist/default-normalizer.mjs +1 -1
  14. package/dist/descriptor-meta-DLA2xV6B.mjs.map +1 -1
  15. package/dist/enum-planning-Dz0Ye3Lb.mjs +0 -0
  16. package/dist/enum-planning-Dz0Ye3Lb.mjs.map +1 -0
  17. package/dist/enum-planning.d.mts +41 -3
  18. package/dist/enum-planning.d.mts.map +1 -1
  19. package/dist/enum-planning.mjs +2 -2
  20. package/dist/errors--zafB5_n.mjs.map +1 -1
  21. package/dist/errors.d.mts.map +1 -1
  22. package/dist/{issue-planner-Ct9xNqbr.mjs → issue-planner-ByQhUzS4.mjs} +111 -54
  23. package/dist/issue-planner-ByQhUzS4.mjs.map +1 -0
  24. package/dist/issue-planner.d.mts.map +1 -1
  25. package/dist/issue-planner.mjs +1 -1
  26. package/dist/migration.d.mts.map +1 -1
  27. package/dist/migration.mjs +1 -1
  28. package/dist/migration.mjs.map +1 -1
  29. package/dist/{native-type-normalizer-DMikJJ1V.mjs → native-type-normalizer-ClNPq__-.mjs} +1 -1
  30. package/dist/{native-type-normalizer-DMikJJ1V.mjs.map → native-type-normalizer-ClNPq__-.mjs.map} +1 -1
  31. package/dist/native-type-normalizer.d.mts.map +1 -1
  32. package/dist/native-type-normalizer.mjs +1 -1
  33. package/dist/{op-factory-call-CB6tPJ3f.mjs → op-factory-call-B0WNg30h.mjs} +2 -2
  34. package/dist/{op-factory-call-CB6tPJ3f.mjs.map → op-factory-call-B0WNg30h.mjs.map} +1 -1
  35. package/dist/op-factory-call-Drccm_JD.d.mts.map +1 -1
  36. package/dist/op-factory-call.mjs +1 -1
  37. package/dist/pack.d.mts.map +1 -1
  38. package/dist/{planner-DlhK35aV.mjs → planner-ClF0y0YR.mjs} +17 -25
  39. package/dist/planner-ClF0y0YR.mjs.map +1 -0
  40. package/dist/{planner-ddl-builders--0TmW6Ey.mjs → planner-ddl-builders-BxRCSn_b.mjs} +3 -3
  41. package/dist/{planner-ddl-builders--0TmW6Ey.mjs.map → planner-ddl-builders-BxRCSn_b.mjs.map} +1 -1
  42. package/dist/planner-ddl-builders.d.mts.map +1 -1
  43. package/dist/planner-ddl-builders.mjs +1 -1
  44. package/dist/planner-identity-values-ojX-6cPV.mjs.map +1 -1
  45. package/dist/planner-identity-values.d.mts.map +1 -1
  46. package/dist/{planner-produced-postgres-migration-TJWH2m_x.mjs → planner-produced-postgres-migration-N1yqYg20.mjs} +3 -5
  47. package/dist/planner-produced-postgres-migration-N1yqYg20.mjs.map +1 -0
  48. package/dist/planner-produced-postgres-migration-p-VKkCia.d.mts.map +1 -1
  49. package/dist/planner-produced-postgres-migration.mjs +1 -1
  50. package/dist/planner-schema-lookup-BGyukuzG.mjs.map +1 -1
  51. package/dist/planner-schema-lookup.d.mts.map +1 -1
  52. package/dist/{planner-sql-checks-BDtJQ9WD.mjs → planner-sql-checks-D3H-xOO1.mjs} +3 -3
  53. package/dist/{planner-sql-checks-BDtJQ9WD.mjs.map → planner-sql-checks-D3H-xOO1.mjs.map} +1 -1
  54. package/dist/planner-sql-checks.d.mts.map +1 -1
  55. package/dist/planner-sql-checks.mjs +1 -1
  56. package/dist/planner-target-details-CIj61DUj.d.mts.map +1 -1
  57. package/dist/planner.d.mts +1 -6
  58. package/dist/planner.d.mts.map +1 -1
  59. package/dist/planner.mjs +1 -1
  60. package/dist/{postgres-contract-serializer-CYct4Y7O.mjs → postgres-contract-serializer-YJvjKrmo.mjs} +2 -2
  61. package/dist/postgres-contract-serializer-YJvjKrmo.mjs.map +1 -0
  62. package/dist/postgres-enum-type-CNhPTDhy.d.mts.map +1 -1
  63. package/dist/postgres-enum-type-DS-KLVRH.mjs.map +1 -1
  64. package/dist/postgres-migration-uADmx0dW.mjs.map +1 -1
  65. package/dist/{postgres-schema-BL0RAvew.mjs → postgres-schema-Bm7vjlOv.mjs} +12 -19
  66. package/dist/postgres-schema-Bm7vjlOv.mjs.map +1 -0
  67. package/dist/render-ops-BC2PtCkj.mjs.map +1 -1
  68. package/dist/render-ops.d.mts.map +1 -1
  69. package/dist/{render-typescript-CI1wbgUc.mjs → render-typescript-CPk7hhWH.mjs} +2 -3
  70. package/dist/render-typescript-CPk7hhWH.mjs.map +1 -0
  71. package/dist/render-typescript.d.mts +0 -1
  72. package/dist/render-typescript.d.mts.map +1 -1
  73. package/dist/render-typescript.mjs +1 -1
  74. package/dist/runtime.d.mts.map +1 -1
  75. package/dist/runtime.mjs +1 -1
  76. package/dist/runtime.mjs.map +1 -1
  77. package/dist/shared-ByhSooBS.d.mts.map +1 -1
  78. package/dist/{sql-utils-BewXAnsG.mjs → sql-utils-B_ruBD-M.mjs} +1 -1
  79. package/dist/{sql-utils-BewXAnsG.mjs.map → sql-utils-B_ruBD-M.mjs.map} +1 -1
  80. package/dist/sql-utils.d.mts.map +1 -1
  81. package/dist/sql-utils.mjs +1 -1
  82. package/dist/statement-builders-vImtdfmM.mjs.map +1 -1
  83. package/dist/statement-builders.d.mts +1 -1
  84. package/dist/statement-builders.d.mts.map +1 -1
  85. package/dist/{tables-Vhh4k5yz.mjs → tables-Dcb2q9zV.mjs} +3 -3
  86. package/dist/{tables-Vhh4k5yz.mjs.map → tables-Dcb2q9zV.mjs.map} +1 -1
  87. package/dist/types-D-XIpzHA.d.mts.map +1 -1
  88. package/dist/types.d.mts +10 -14
  89. package/dist/types.d.mts.map +1 -1
  90. package/dist/types.mjs +1 -1
  91. package/package.json +29 -18
  92. package/src/core/migrations/enum-planning.ts +134 -14
  93. package/src/core/migrations/issue-planner.ts +20 -11
  94. package/src/core/migrations/planner-produced-postgres-migration.ts +0 -2
  95. package/src/core/migrations/planner-strategies.ts +138 -40
  96. package/src/core/migrations/planner.ts +9 -21
  97. package/src/core/migrations/render-typescript.ts +1 -5
  98. package/src/core/migrations/runner.ts +20 -61
  99. package/src/core/migrations/statement-builders.ts +1 -1
  100. package/src/core/migrations/verify-postgres-namespaces.ts +6 -6
  101. package/src/core/postgres-contract-serializer.ts +4 -4
  102. package/src/core/postgres-schema.ts +10 -20
  103. package/src/exports/control.ts +16 -0
  104. package/src/exports/enum-planning.ts +5 -0
  105. package/dist/enum-planning-Bqp96iIw.mjs +0 -63
  106. package/dist/enum-planning-Bqp96iIw.mjs.map +0 -1
  107. package/dist/issue-planner-Ct9xNqbr.mjs.map +0 -1
  108. package/dist/planner-DlhK35aV.mjs.map +0 -1
  109. package/dist/planner-produced-postgres-migration-TJWH2m_x.mjs.map +0 -1
  110. package/dist/postgres-contract-serializer-CYct4Y7O.mjs.map +0 -1
  111. package/dist/postgres-schema-BL0RAvew.mjs.map +0 -1
  112. package/dist/render-typescript-CI1wbgUc.mjs.map +0 -1
@@ -37,7 +37,11 @@ import {
37
37
  import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
38
38
  import { PostgresEnumType } from '../postgres-enum-type';
39
39
  import { isPostgresSchema } from '../postgres-schema';
40
- import { determineEnumDiff, readExistingEnumValues } from './enum-planning';
40
+ import {
41
+ determineEnumDiff,
42
+ readExistingEnumValues,
43
+ resolveDdlSchemaForNamespaceStorage,
44
+ } from './enum-planning';
41
45
  import {
42
46
  AddColumnCall,
43
47
  AddEnumValuesCall,
@@ -108,12 +112,12 @@ export function resolveNamespaceIdForIssue(issue: { readonly namespaceId?: strin
108
112
  /**
109
113
  * Resolve the DDL schema name for a namespace coordinate. Postgres-aware
110
114
  * 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.
115
+ * named schemas return their own id; the unbound singleton returns
116
+ * `UNBOUND_NAMESPACE_ID`. Legacy single-namespace contracts whose
117
+ * `__unbound__` slot is the framework-default `SqlUnboundNamespace`
118
+ * (rather than the Postgres-aware `PostgresUnboundSchema`) flow the
119
+ * coordinate through unchanged so downstream `qualifyTableName`
120
+ * resolves polymorphically.
117
121
  */
118
122
  export function resolveDdlSchemaForNamespace(ctx: StrategyContext, namespaceId: string): string {
119
123
  const namespace = ctx.toContract.storage.namespaces[namespaceId];
@@ -123,21 +127,57 @@ export function resolveDdlSchemaForNamespace(ctx: StrategyContext, namespaceId:
123
127
  return namespaceId;
124
128
  }
125
129
 
130
+ /** Default Postgres enum landing namespace — where contract-level (`types:`)
131
+ * enums are placed by the authoring builder when no explicit namespace is
132
+ * given. Mirrors `POSTGRES_ENUM_NAMESPACE_ID` in the contract-ts builder. */
133
+ const DEFAULT_ENUM_NAMESPACE_ID = 'public';
134
+
135
+ function namespaceHasEnum(storage: SqlStorage, namespaceId: string, typeName: string): boolean {
136
+ const ns = storage.namespaces[namespaceId];
137
+ if (!ns || !('enum' in ns) || ns.enum == null) return false;
138
+ return (ns.enum as Record<string, PostgresEnumStorageEntry>)[typeName] !== undefined;
139
+ }
140
+
126
141
  /**
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].enum`, not under `storage.types`.
142
+ * Resolves which namespace's enum a column's bare `typeRef` binds to.
143
+ *
144
+ * Columns carry a bare (non-namespace-qualified) `typeRef`; the enum it names
145
+ * may live in a different namespace than the column's own (the authoring
146
+ * builder places contract-level `types:` enums in the default `public`
147
+ * namespace while a model's table may sit in the unbound namespace). The
148
+ * binding rule: an enum declared in the column's *own* namespace shadows
149
+ * everything; otherwise the column references the ambient enum — the sole
150
+ * namespace that defines `typeName`, preferring the default `public`
151
+ * namespace when several do. Returns `undefined` when no namespace defines it.
152
+ */
153
+ function resolveColumnEnumNamespace(
154
+ storage: SqlStorage,
155
+ columnNamespaceId: string,
156
+ typeName: string,
157
+ ): string | undefined {
158
+ if (namespaceHasEnum(storage, columnNamespaceId, typeName)) return columnNamespaceId;
159
+ const owners = Object.keys(storage.namespaces).filter((nsId) =>
160
+ namespaceHasEnum(storage, nsId, typeName),
161
+ );
162
+ if (owners.length === 1) return owners[0];
163
+ if (owners.includes(DEFAULT_ENUM_NAMESPACE_ID)) return DEFAULT_ENUM_NAMESPACE_ID;
164
+ return owners[0];
165
+ }
166
+
167
+ /**
168
+ * Finds a type entry by explicit namespace coordinate. Namespace types (e.g.
169
+ * Postgres enums) live under `storage.namespaces[nsId].enum`. Returns the
170
+ * entry from the named namespace only — never scans other namespaces, so two
171
+ * namespaces that hold an enum with the same name resolve independently.
130
172
  */
131
173
  function locateNamespaceType(
132
174
  storage: SqlStorage,
175
+ namespaceId: string,
133
176
  typeName: string,
134
177
  ): PostgresEnumStorageEntry | undefined {
135
- for (const ns of Object.values(storage.namespaces)) {
136
- if (!('enum' in ns) || ns.enum == null) continue;
137
- const entry = (ns.enum as Record<string, PostgresEnumStorageEntry>)[typeName];
138
- if (entry !== undefined) return entry;
139
- }
140
- return undefined;
178
+ const ns = storage.namespaces[namespaceId];
179
+ if (!ns || !('enum' in ns) || ns.enum == null) return undefined;
180
+ return (ns.enum as Record<string, PostgresEnumStorageEntry>)[typeName];
141
181
  }
142
182
 
143
183
  // ============================================================================
@@ -369,10 +409,11 @@ export const nullableTighteningCallStrategy: CallMigrationStrategy = (issues, ct
369
409
  };
370
410
 
371
411
  function enumRebuildCallRecipe(
412
+ namespaceId: string,
372
413
  typeName: string,
373
414
  ctx: StrategyContext,
374
415
  ): readonly PostgresOpFactoryCall[] {
375
- const toType = locateNamespaceType(ctx.toContract.storage, typeName);
416
+ const toType = locateNamespaceType(ctx.toContract.storage, namespaceId, typeName);
376
417
  if (!toType) return [];
377
418
  const isEnum = isPostgresEnumStorageEntry(toType);
378
419
  const nativeType = toType.nativeType;
@@ -380,13 +421,28 @@ function enumRebuildCallRecipe(
380
421
  ? toType.values
381
422
  : (((toType as StorageTypeInstance).typeParams['values'] ?? []) as readonly string[]);
382
423
  const tempName = `${nativeType}${REBUILD_SUFFIX}`;
424
+ // Type DDL targets the enum's real schema — the unbound coordinate resolves
425
+ // to the introspected `current_schema()`, never the `__unbound__` sentinel.
426
+ const ddlSchema = resolveDdlSchemaForNamespaceStorage(
427
+ ctx.toContract.storage,
428
+ namespaceId,
429
+ ctx.schema,
430
+ );
383
431
 
432
+ // Migrate every column whose `typeRef` binds to *this* enum. The column's
433
+ // bare `typeRef` resolves to an enum namespace (own-namespace shadows;
434
+ // otherwise the ambient/default `public` enum), so a column in the unbound
435
+ // namespace correctly binds to a `public`-namespace enum, while two
436
+ // same-named enums in distinct namespaces keep their columns disjoint.
384
437
  const columnRefs: { namespaceId: string; table: string; column: string }[] = [];
385
438
  for (const [nsId, ns] of Object.entries(ctx.toContract.storage.namespaces)) {
386
439
  for (const [tableName, tableNode] of Object.entries(ns.tables)) {
387
440
  const table = tableNode as StorageTable;
388
441
  for (const [columnName, column] of Object.entries(table.columns)) {
389
- if (column.typeRef === typeName) {
442
+ if (
443
+ column.typeRef === typeName &&
444
+ resolveColumnEnumNamespace(ctx.toContract.storage, nsId, typeName) === namespaceId
445
+ ) {
390
446
  columnRefs.push({ namespaceId: nsId, table: tableName, column: columnName });
391
447
  }
392
448
  }
@@ -394,7 +450,7 @@ function enumRebuildCallRecipe(
394
450
  }
395
451
 
396
452
  return [
397
- new CreateEnumTypeCall(ctx.schemaName, tempName, desiredValues),
453
+ new CreateEnumTypeCall(ddlSchema, tempName, desiredValues),
398
454
  ...columnRefs.map((ref) => {
399
455
  const using = `${ref.column}::text::${tempName}`;
400
456
  return new AlterColumnTypeCall(
@@ -409,8 +465,8 @@ function enumRebuildCallRecipe(
409
465
  },
410
466
  );
411
467
  }),
412
- new DropEnumTypeCall(ctx.schemaName, nativeType),
413
- new RenameTypeCall(ctx.schemaName, tempName, nativeType),
468
+ new DropEnumTypeCall(ddlSchema, nativeType),
469
+ new RenameTypeCall(ddlSchema, tempName, nativeType),
414
470
  ];
415
471
  }
416
472
 
@@ -450,6 +506,18 @@ function enumRebuildCallRecipe(
450
506
  * into the `dep` bucket — i.e. `CREATE TYPE` runs before any
451
507
  * `CreateTableCall` that references the new enum.
452
508
  */
509
+ /**
510
+ * Separator character for compound enum map keys (`namespaceId\u0000typeName`).
511
+ * NUL (`\u0000`) is invalid in both Postgres identifiers and TypeScript symbol
512
+ * names so it cannot appear in either component — unambiguous separator.
513
+ */
514
+ const COMPOUND_KEY_SEP = '\u0000';
515
+
516
+ /** Builds the compound map key for a namespace-qualified enum entry. */
517
+ function enumCompoundKey(namespaceId: string, typeName: string): string {
518
+ return `${namespaceId}${COMPOUND_KEY_SEP}${typeName}`;
519
+ }
520
+
453
521
  export const nativeEnumPlanCallStrategy: CallMigrationStrategy = (issues, ctx) => {
454
522
  const enumTypes = collectPostgresEnumTypes(ctx.toContract.storage);
455
523
  if (enumTypes.size === 0) return { kind: 'no_match' };
@@ -457,28 +525,42 @@ export const nativeEnumPlanCallStrategy: CallMigrationStrategy = (issues, ctx) =
457
525
  const dataAllowed = ctx.policy.allowedOperationClasses.includes('data');
458
526
 
459
527
  const calls: PostgresOpFactoryCall[] = [];
460
- const handledTypeNames = new Set<string>();
461
- const introducedTypeNames = new Set<string>();
462
- const rebuiltTypeNames = new Set<string>();
528
+ const handledKeys = new Set<string>();
529
+ const introducedKeys = new Set<string>();
530
+ const rebuiltKeys = new Set<string>();
463
531
  let emittedRebuildRecipe = false;
464
532
 
465
- for (const [typeName, enumType] of enumTypes) {
533
+ for (const [key, enumType] of enumTypes) {
534
+ const sepIdx = key.indexOf(COMPOUND_KEY_SEP);
535
+ const enumNamespaceId = key.slice(0, sepIdx);
536
+ const typeName = key.slice(sepIdx + 1);
537
+
466
538
  const desired = enumType.values;
467
- const existing = readExistingEnumValues(ctx.schema, enumType.nativeType);
539
+ // The enum's live schema: for the unbound coordinate this resolves to the
540
+ // introspected `current_schema()` (e.g. `public`), never the `__unbound__`
541
+ // DDL-emit sentinel — so both the existing-values lookup key and the
542
+ // emitted `CREATE TYPE` / `ALTER TYPE` target the real schema the type
543
+ // lives in. Named namespaces resolve to their own DDL schema.
544
+ const ddlSchema = resolveDdlSchemaForNamespaceStorage(
545
+ ctx.toContract.storage,
546
+ enumNamespaceId,
547
+ ctx.schema,
548
+ );
549
+ const existing = readExistingEnumValues(ctx.schema, ddlSchema, enumType.nativeType);
468
550
  if (!existing) {
469
- calls.push(new CreateEnumTypeCall(ctx.schemaName, typeName, desired, enumType.nativeType));
470
- handledTypeNames.add(typeName);
471
- introducedTypeNames.add(typeName);
551
+ calls.push(new CreateEnumTypeCall(ddlSchema, typeName, desired, enumType.nativeType));
552
+ handledKeys.add(key);
553
+ introducedKeys.add(key);
472
554
  continue;
473
555
  }
474
556
  const diff = determineEnumDiff(existing, desired);
475
557
  if (diff.kind === 'unchanged') {
476
- handledTypeNames.add(typeName);
558
+ handledKeys.add(key);
477
559
  continue;
478
560
  }
479
561
  if (diff.kind === 'add_values') {
480
- calls.push(new AddEnumValuesCall(ctx.schemaName, typeName, enumType.nativeType, diff.values));
481
- handledTypeNames.add(typeName);
562
+ calls.push(new AddEnumValuesCall(ddlSchema, typeName, enumType.nativeType, diff.values));
563
+ handledKeys.add(key);
482
564
  continue;
483
565
  }
484
566
  if (dataAllowed && diff.removedValues.length > 0) {
@@ -490,10 +572,10 @@ export const nativeEnumPlanCallStrategy: CallMigrationStrategy = (issues, ctx) =
490
572
  ),
491
573
  );
492
574
  }
493
- calls.push(...enumRebuildCallRecipe(typeName, ctx));
575
+ calls.push(...enumRebuildCallRecipe(enumNamespaceId, typeName, ctx));
494
576
  emittedRebuildRecipe = true;
495
- handledTypeNames.add(typeName);
496
- rebuiltTypeNames.add(typeName);
577
+ handledKeys.add(key);
578
+ rebuiltKeys.add(key);
497
579
  }
498
580
 
499
581
  // The strategy emits a single `recipe` flag for the entire pass,
@@ -505,9 +587,17 @@ export const nativeEnumPlanCallStrategy: CallMigrationStrategy = (issues, ctx) =
505
587
  // enum would fail at runtime with a confusing `type "X" does not
506
588
  // exist` error. Surface the unrepresentable case here as a
507
589
  // planner-time error so the failure mode is loud, not silent.
508
- if (introducedTypeNames.size > 0 && rebuiltTypeNames.size > 0) {
590
+ if (introducedKeys.size > 0 && rebuiltKeys.size > 0) {
591
+ const introducedDisplay = [...introducedKeys]
592
+ .sort()
593
+ .map((k) => k.replace(COMPOUND_KEY_SEP, '.'))
594
+ .join(', ');
595
+ const rebuiltDisplay = [...rebuiltKeys]
596
+ .sort()
597
+ .map((k) => k.replace(COMPOUND_KEY_SEP, '.'))
598
+ .join(', ');
509
599
  throw new Error(
510
- `nativeEnumPlanCallStrategy: cannot emit both a brand-new enum and a rebuild on a different enum in the same plan; the single recipe flag cannot route them to different buckets. Introduced: [${[...introducedTypeNames].sort().join(', ')}]; rebuilt: [${[...rebuiltTypeNames].sort().join(', ')}]. Split the strategy or grow the \`match\` return type before this case lands.`,
600
+ `nativeEnumPlanCallStrategy: cannot emit both a brand-new enum and a rebuild on a different enum in the same plan; the single recipe flag cannot route them to different buckets. Introduced: [${introducedDisplay}]; rebuilt: [${rebuiltDisplay}]. Split the strategy or grow the \`match\` return type before this case lands.`,
511
601
  );
512
602
  }
513
603
 
@@ -516,7 +606,7 @@ export const nativeEnumPlanCallStrategy: CallMigrationStrategy = (issues, ctx) =
516
606
  !(
517
607
  (issue.kind === 'type_missing' || issue.kind === 'enum_values_changed') &&
518
608
  issue.typeName &&
519
- handledTypeNames.has(issue.typeName)
609
+ handledKeys.has(enumCompoundKey(resolveNamespaceIdForIssue(issue), issue.typeName))
520
610
  ),
521
611
  );
522
612
 
@@ -538,14 +628,22 @@ export const nativeEnumPlanCallStrategy: CallMigrationStrategy = (issues, ctx) =
538
628
  return { kind: 'match', issues: remaining, calls, recipe: emittedRebuildRecipe };
539
629
  };
540
630
 
631
+ /**
632
+ * Collects every `PostgresEnumType` instance across all declared namespaces,
633
+ * returning a compound-keyed map (`${namespaceId}\u0000${typeName}`). Two
634
+ * namespaces that declare an enum with the same name produce two distinct
635
+ * entries — no name collision, no last-write-wins.
636
+ *
637
+ * Entries within each namespace are sorted by name for deterministic ordering.
638
+ */
541
639
  function collectPostgresEnumTypes(storage: SqlStorage): ReadonlyMap<string, PostgresEnumType> {
542
640
  const result = new Map<string, PostgresEnumType>();
543
- for (const ns of Object.values(storage.namespaces)) {
641
+ for (const [nsId, ns] of Object.entries(storage.namespaces)) {
544
642
  if (!('enum' in ns) || ns.enum == null) continue;
545
643
  const nsEnums = ns.enum as Record<string, unknown>;
546
644
  for (const [name, instance] of Object.entries(nsEnums).sort(([a], [b]) => a.localeCompare(b))) {
547
645
  if (instance instanceof PostgresEnumType) {
548
- result.set(name, instance);
646
+ result.set(enumCompoundKey(nsId, name), instance);
549
647
  }
550
648
  }
551
649
  }
@@ -17,9 +17,10 @@ import type {
17
17
  MigrationScaffoldContext,
18
18
  SchemaIssue,
19
19
  } from '@prisma-next/framework-components/control';
20
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
20
21
  import { parsePostgresDefault } from '../default-normalizer';
21
22
  import { normalizeSchemaNativeType } from '../native-type-normalizer';
22
- import { readExistingEnumValues } from './enum-planning';
23
+ import { createResolveExistingEnumValues } from './enum-planning';
23
24
  import { planIssues } from './issue-planner';
24
25
  import { TypeScriptRenderablePostgresMigration } from './planner-produced-postgres-migration';
25
26
  import { postgresPlannerStrategies } from './planner-strategies';
@@ -39,21 +40,8 @@ type VerifySqlSchemaOptionsWithComponents = Parameters<typeof verifySqlSchema>[0
39
40
  readonly frameworkComponents: PlannerFrameworkComponents;
40
41
  };
41
42
 
42
- interface PlannerConfig {
43
- readonly defaultSchema: string;
44
- }
45
-
46
- const DEFAULT_PLANNER_CONFIG: PlannerConfig = {
47
- defaultSchema: 'public',
48
- };
49
-
50
- export function createPostgresMigrationPlanner(
51
- config: Partial<PlannerConfig> = {},
52
- ): PostgresMigrationPlanner {
53
- return new PostgresMigrationPlanner({
54
- ...DEFAULT_PLANNER_CONFIG,
55
- ...config,
56
- });
43
+ export function createPostgresMigrationPlanner(): PostgresMigrationPlanner {
44
+ return new PostgresMigrationPlanner();
57
45
  }
58
46
 
59
47
  /**
@@ -84,8 +72,6 @@ export type PostgresPlanResult =
84
72
  * authoring surface.
85
73
  */
86
74
  export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgres'> {
87
- constructor(private readonly config: PlannerConfig) {}
88
-
89
75
  plan(options: {
90
76
  readonly contract: unknown;
91
77
  readonly schema: unknown;
@@ -132,7 +118,10 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
132
118
  }
133
119
 
134
120
  private planSql(options: SqlMigrationPlannerPlanOptions): PostgresPlanResult {
135
- const schemaName = options.schemaName ?? this.config.defaultSchema;
121
+ const schemaName =
122
+ options.schemaName ??
123
+ Object.keys(options.contract.storage.namespaces).find((id) => id !== UNBOUND_NAMESPACE_ID) ??
124
+ UNBOUND_NAMESPACE_ID;
136
125
  const policyResult = this.ensureAdditivePolicy(options.policy);
137
126
  if (policyResult) {
138
127
  return policyResult;
@@ -219,8 +208,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
219
208
  frameworkComponents: options.frameworkComponents,
220
209
  normalizeDefault: parsePostgresDefault,
221
210
  normalizeNativeType: normalizeSchemaNativeType,
222
- resolveExistingEnumValues: (schema, enumType) =>
223
- readExistingEnumValues(schema, enumType.nativeType),
211
+ resolveExistingEnumValues: createResolveExistingEnumValues(options.contract.storage),
224
212
  };
225
213
  const verifyResult = verifySqlSchema(verifyOptions);
226
214
  // Schema presence is a Postgres-specific concern (no equivalent in
@@ -11,12 +11,11 @@
11
11
 
12
12
  import type { OpFactoryCall } from '@prisma-next/framework-components/control';
13
13
  import { detectScaffoldRuntime, shebangLineFor } from '@prisma-next/migration-tools/migration-ts';
14
- import { type ImportRequirement, jsonToTsSource, renderImports } from '@prisma-next/ts-render';
14
+ import { type ImportRequirement, renderImports } from '@prisma-next/ts-render';
15
15
 
16
16
  export interface RenderMigrationMeta {
17
17
  readonly from: string | null;
18
18
  readonly to: string;
19
- readonly labels?: readonly string[];
20
19
  }
21
20
 
22
21
  /**
@@ -83,9 +82,6 @@ function buildDescribeMethod(meta: RenderMigrationMeta): string {
83
82
  lines.push(' return {');
84
83
  lines.push(` from: ${JSON.stringify(meta.from)},`);
85
84
  lines.push(` to: ${JSON.stringify(meta.to)},`);
86
- if (meta.labels && meta.labels.length > 0) {
87
- lines.push(` labels: ${jsonToTsSource(meta.labels)},`);
88
- }
89
85
  lines.push(' };');
90
86
  lines.push(' }');
91
87
  lines.push('');
@@ -1,7 +1,6 @@
1
1
  import type { ContractMarkerRecord } from '@prisma-next/contract/types';
2
2
  import type {
3
3
  MigrationOperationPolicy,
4
- MultiSpaceRunnerResult,
5
4
  SqlControlFamilyInstance,
6
5
  SqlMigrationPlanContractInfo,
7
6
  SqlMigrationPlanOperation,
@@ -14,15 +13,19 @@ import type {
14
13
  } from '@prisma-next/family-sql/control';
15
14
  import { runnerFailure, runnerSuccess } from '@prisma-next/family-sql/control';
16
15
  import { verifySqlSchema } from '@prisma-next/family-sql/schema-verify';
17
- import type { ControlDriverInstance } from '@prisma-next/framework-components/control';
16
+ import type {
17
+ ControlDriverInstance,
18
+ MigrationRunnerResult,
19
+ } from '@prisma-next/framework-components/control';
18
20
  import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
21
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
19
22
  import { SqlQueryError } from '@prisma-next/sql-errors';
20
23
  import { ifDefined } from '@prisma-next/utils/defined';
21
24
  import type { Result } from '@prisma-next/utils/result';
22
25
  import { notOk, ok, okVoid } from '@prisma-next/utils/result';
23
26
  import { parsePostgresDefault } from '../default-normalizer';
24
27
  import { normalizeSchemaNativeType } from '../native-type-normalizer';
25
- import { readExistingEnumValues } from './enum-planning';
28
+ import { createResolveExistingEnumValues } from './enum-planning';
26
29
  import type { PostgresPlanTargetDetails } from './planner-target-details';
27
30
  import {
28
31
  buildLedgerInsertStatement,
@@ -33,19 +36,11 @@ import {
33
36
  type SqlStatement,
34
37
  } from './statement-builders';
35
38
 
36
- interface RunnerConfig {
37
- readonly defaultSchema: string;
38
- }
39
-
40
39
  interface ApplyPlanSuccessValue {
41
40
  readonly operationsExecuted: number;
42
41
  readonly executedOperations: readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[];
43
42
  }
44
43
 
45
- const DEFAULT_CONFIG: RunnerConfig = {
46
- defaultSchema: 'public',
47
- };
48
-
49
44
  const LOCK_DOMAIN = 'prisma_next.contract.marker';
50
45
 
51
46
  /**
@@ -58,13 +53,10 @@ function cloneAndFreezeRecord<T extends Record<string, unknown>>(value: T): T {
58
53
  if (val === null || val === undefined) {
59
54
  cloned[key] = val;
60
55
  } else if (Array.isArray(val)) {
61
- // Clone array (shallow clone of array elements)
62
56
  cloned[key] = Object.freeze([...val]);
63
57
  } else if (typeof val === 'object') {
64
- // Recursively clone nested objects
65
58
  cloned[key] = cloneAndFreezeRecord(val as Record<string, unknown>);
66
59
  } else {
67
- // Primitives are copied as-is
68
60
  cloned[key] = val;
69
61
  }
70
62
  }
@@ -73,61 +65,27 @@ function cloneAndFreezeRecord<T extends Record<string, unknown>>(value: T): T {
73
65
 
74
66
  export function createPostgresMigrationRunner(
75
67
  family: SqlControlFamilyInstance,
76
- config: Partial<RunnerConfig> = {},
77
68
  ): SqlMigrationRunner<PostgresPlanTargetDetails> {
78
- return new PostgresMigrationRunner(family, { ...DEFAULT_CONFIG, ...config });
69
+ return new PostgresMigrationRunner(family);
79
70
  }
80
71
 
81
72
  class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDetails> {
82
- constructor(
83
- private readonly family: SqlControlFamilyInstance,
84
- private readonly config: RunnerConfig,
85
- ) {}
86
-
87
- async execute(
88
- options: SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>,
89
- ): Promise<SqlMigrationRunnerResult> {
90
- const driver = options.driver;
91
-
92
- // Static checks fail fast before any transaction work — no point
93
- // burning a BEGIN/ROLLBACK round-trip on a destination-contract
94
- // mismatch the caller can fix locally.
95
- const destinationCheck = this.ensurePlanMatchesDestinationContract(
96
- options.plan.destination,
97
- options.destinationContract,
98
- );
99
- if (!destinationCheck.ok) return destinationCheck;
100
-
101
- const policyCheck = this.enforcePolicyCompatibility(options.policy, options.plan.operations);
102
- if (!policyCheck.ok) return policyCheck;
103
-
104
- await this.beginTransaction(driver);
105
- let committed = false;
106
- try {
107
- const result = await this.executeOnConnection(options);
108
- if (!result.ok) {
109
- return result;
110
- }
111
- await this.commitTransaction(driver);
112
- committed = true;
113
- return result;
114
- } finally {
115
- if (!committed) {
116
- await this.rollbackTransaction(driver);
117
- }
118
- }
119
- }
73
+ constructor(private readonly family: SqlControlFamilyInstance) {}
120
74
 
121
75
  /**
122
76
  * Body of the migration runner without transaction management. The
123
- * caller (single-space `execute(...)` above, or the multi-space
124
- * outer-tx orchestrator at the SQL family level) owns the
77
+ * caller ({@link PostgresMigrationRunner.execute}) owns the
125
78
  * `BEGIN`/`COMMIT`/`ROLLBACK` lifecycle.
126
79
  */
127
80
  async executeOnConnection(
128
81
  options: SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>,
129
82
  ): Promise<SqlMigrationRunnerResult> {
130
- const schema = options.schemaName ?? this.config.defaultSchema;
83
+ const schema =
84
+ options.schemaName ??
85
+ Object.keys(options.destinationContract.storage.namespaces).find(
86
+ (id) => id !== UNBOUND_NAMESPACE_ID,
87
+ ) ??
88
+ UNBOUND_NAMESPACE_ID;
131
89
  const driver = options.driver;
132
90
  if (options.space !== undefined && options.space !== options.plan.spaceId) {
133
91
  throw new Error(
@@ -188,8 +146,9 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
188
146
  frameworkComponents: options.frameworkComponents,
189
147
  normalizeDefault: parsePostgresDefault,
190
148
  normalizeNativeType: normalizeSchemaNativeType,
191
- resolveExistingEnumValues: (schema, enumType) =>
192
- readExistingEnumValues(schema, enumType.nativeType),
149
+ resolveExistingEnumValues: createResolveExistingEnumValues(
150
+ options.destinationContract.storage,
151
+ ),
193
152
  });
194
153
  if (!schemaVerifyResult.ok) {
195
154
  return runnerFailure('SCHEMA_VERIFY_FAILED', schemaVerifyResult.summary, {
@@ -216,12 +175,12 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
216
175
  });
217
176
  }
218
177
 
219
- async executeAcrossSpaces(options: {
178
+ async execute(options: {
220
179
  readonly driver: ControlDriverInstance<'sql', string>;
221
180
  readonly perSpaceOptions: ReadonlyArray<
222
181
  SqlMigrationRunnerExecuteOptions<PostgresPlanTargetDetails>
223
182
  >;
224
- }): Promise<MultiSpaceRunnerResult> {
183
+ }): Promise<MigrationRunnerResult> {
225
184
  const driver = options.driver;
226
185
  const perSpaceOptions = options.perSpaceOptions;
227
186
 
@@ -56,7 +56,7 @@ export interface MergeMarkerInput {
56
56
  * Logical space identifier for this marker row. Required at every
57
57
  * call site so the type system surfaces every place that needs to
58
58
  * thread the value (rather than letting an `?? APP_SPACE_ID`
59
- * fall-through silently collapse multi-space markers onto the
59
+ * fall-through silently collapse per-space markers onto the
60
60
  * `'app'` row). App-plan callers pass {@link APP_SPACE_ID}
61
61
  * (`'app'`); per-extension callers (planner / runner / verifier
62
62
  * extensions over contract spaces) pass the extension's space id.
@@ -52,12 +52,12 @@ function existingSchemasFromSchema(schema: SqlSchemaIR): readonly string[] {
52
52
  *
53
53
  * A namespace's live container is the schema returned by its
54
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.
55
+ * to their own id; the unbound singleton returns `UNBOUND_NAMESPACE_ID`
56
+ * and is skipped explicitly (late-bound namespaces have no fixed DDL
57
+ * schema). Issues are emitted only when the resolved name is a real,
58
+ * creatable schema (not the unbound sentinel) and is missing from the
59
+ * introspected list. `public` is suppressed implicitly because the
60
+ * introspection (or its sensible default) always carries it.
61
61
  *
62
62
  * Each emitted issue stamps `namespaceId` with the contract namespace
63
63
  * coordinate so the downstream `mapIssueToCall` re-resolves the DDL
@@ -117,10 +117,10 @@ export class PostgresContractSerializer extends SqlContractSerializerBase<Contra
117
117
  if (isPostgresSchema(ns)) {
118
118
  namespacesJson[nsId] = this.serializePostgresNamespace(ns, ns.id === UNBOUND_NAMESPACE_ID);
119
119
  } else {
120
- // Family-level SqlNamespacePayload / SqlUnboundNamespace haven't
121
- // been promoted to a PostgresSchema instance yet (e.g. they came
122
- // straight from the TS builder, which uses the family-shared
123
- // SqlStorage constructor). Serialise them as postgres-schema /
120
+ // Family-level SqlUnboundNamespace or other family-built SQL
121
+ // namespaces haven't been promoted to a PostgresSchema instance
122
+ // yet (e.g. they came straight from the TS builder before a target
123
+ // `createNamespace` factory ran). Serialise them as postgres-schema /
124
124
  // postgres-unbound-schema so the round-trip through
125
125
  // deserializeContract hydrates them back into PostgresSchema
126
126
  // instances.
@@ -120,10 +120,11 @@ export class PostgresSchema extends NamespaceBase {
120
120
  * statements that need to identify this namespace in the live
121
121
  * database (e.g. `CREATE TABLE "<ddlSchemaName>"."<table>" …`,
122
122
  * catalog filters, planner conflict lookups). Named schemas resolve
123
- * to their own id; the unbound singleton overrides this to project
124
- * to `'public'` when a sibling public namespace exists in the same
125
- * contract and falls back to the framework sentinel otherwise so
126
- * the planner can detect the missing-projection case explicitly.
123
+ * to their own id. The `PostgresUnboundSchema` singleton inherits
124
+ * this and returns `UNBOUND_NAMESPACE_ID` callers that dispatch
125
+ * through `qualifyTableName` / `toRegclassLiteral` route through the
126
+ * polymorphic `PostgresUnboundSchema` overrides and produce
127
+ * unqualified (search-path-resolved) output automatically.
127
128
  */
128
129
  ddlSchemaName(_storage: SqlStorage): string {
129
130
  return this.id;
@@ -143,6 +144,11 @@ export class PostgresSchema extends NamespaceBase {
143
144
  * sentinel; Postgres decides what late-bound means here (the table
144
145
  * name, naked — the schema is supplied by the live connection's
145
146
  * `search_path`).
147
+ *
148
+ * `ddlSchemaName` is inherited from `PostgresSchema` and returns
149
+ * `UNBOUND_NAMESPACE_ID`. Downstream helpers (`qualifyTableName`,
150
+ * `toRegclassLiteral`) route through the polymorphic factory and
151
+ * produce unqualified output automatically.
146
152
  */
147
153
  export class PostgresUnboundSchema extends PostgresSchema {
148
154
  static readonly instance: PostgresUnboundSchema = new PostgresUnboundSchema();
@@ -162,22 +168,6 @@ export class PostgresUnboundSchema extends PostgresSchema {
162
168
  override schemaSqlExpression(): string {
163
169
  return 'current_schema()';
164
170
  }
165
-
166
- /**
167
- * The unbound slot has no schema name of its own, so DDL emission
168
- * projects it onto a sibling when one is available: if the contract
169
- * carries a `public` namespace, the late-bound slot resolves to
170
- * `'public'` (the default Postgres landing schema); otherwise it
171
- * resolves to the framework sentinel `UNBOUND_NAMESPACE_ID` so the
172
- * planner can recognise the unprojected case and route accordingly
173
- * (e.g. emit a conflict instead of silently picking a schema).
174
- */
175
- override ddlSchemaName(storage: SqlStorage): string {
176
- if (storage.namespaces['public'] !== undefined) {
177
- return 'public';
178
- }
179
- return UNBOUND_NAMESPACE_ID;
180
- }
181
171
  }
182
172
 
183
173
  PostgresSchema.unbound = PostgresUnboundSchema.instance;