@prisma-next/target-postgres 0.14.0-dev.60 → 0.14.0-dev.62

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 (127) hide show
  1. package/dist/{authoring-WkqG_MkU.mjs → authoring-CdIZ0yvD.mjs} +147 -38
  2. package/dist/authoring-CdIZ0yvD.mjs.map +1 -0
  3. package/dist/contract-free.d.mts +1 -1
  4. package/dist/contract-free.d.mts.map +1 -1
  5. package/dist/contract-free.mjs +2 -2
  6. package/dist/{control-policy-Co_13Hxa.mjs → control-policy-DKp0HKpY.mjs} +203 -45
  7. package/dist/control-policy-DKp0HKpY.mjs.map +1 -0
  8. package/dist/control.mjs +22 -25
  9. package/dist/control.mjs.map +1 -1
  10. package/dist/{ddl-B-krEXY1.mjs → ddl-gmxHf68z.mjs} +20 -4
  11. package/dist/{ddl-B-krEXY1.mjs.map → ddl-gmxHf68z.mjs.map} +1 -1
  12. package/dist/ddl.d.mts +2 -2
  13. package/dist/ddl.mjs +2 -2
  14. package/dist/{descriptor-meta-BOcKPaLE.mjs → descriptor-meta-DEuXJfpR.mjs} +3 -3
  15. package/dist/{descriptor-meta-BOcKPaLE.mjs.map → descriptor-meta-DEuXJfpR.mjs.map} +1 -1
  16. package/dist/{diff-database-schema-BOsDnH64.mjs → diff-database-schema-D2sjvMw9.mjs} +55 -19
  17. package/dist/diff-database-schema-D2sjvMw9.mjs.map +1 -0
  18. package/dist/diff-database-schema.d.mts +4 -4
  19. package/dist/diff-database-schema.d.mts.map +1 -1
  20. package/dist/diff-database-schema.mjs +1 -1
  21. package/dist/issue-planner.d.mts +1 -1
  22. package/dist/issue-planner.d.mts.map +1 -1
  23. package/dist/issue-planner.mjs +1 -1
  24. package/dist/migration.d.mts +2 -2
  25. package/dist/migration.mjs +2 -2
  26. package/dist/{nodes-BtQyZha6.d.mts → nodes-Bc2N3tMy.d.mts} +27 -3
  27. package/dist/nodes-Bc2N3tMy.d.mts.map +1 -0
  28. package/dist/{nodes-B815pj_X.mjs → nodes-DJC5B8Id.mjs} +32 -2
  29. package/dist/nodes-DJC5B8Id.mjs.map +1 -0
  30. package/dist/{op-factory-call-DxNNKUeZ.mjs → op-factory-call-C4flTtTG.mjs} +105 -6
  31. package/dist/op-factory-call-C4flTtTG.mjs.map +1 -0
  32. package/dist/{op-factory-call-D6_7-7Vc.d.mts → op-factory-call-CSIg5XDu.d.mts} +26 -3
  33. package/dist/{op-factory-call-D6_7-7Vc.d.mts.map → op-factory-call-CSIg5XDu.d.mts.map} +1 -1
  34. package/dist/op-factory-call.d.mts +2 -2
  35. package/dist/op-factory-call.mjs +2 -2
  36. package/dist/pack.d.mts +146 -3
  37. package/dist/pack.d.mts.map +1 -1
  38. package/dist/pack.mjs +1 -1
  39. package/dist/{planner-CNrc1Odu.mjs → planner-BtEd5LMr.mjs} +32 -64
  40. package/dist/planner-BtEd5LMr.mjs.map +1 -0
  41. package/dist/{planner-ddl-builders-B8Nn6nHN.mjs → planner-ddl-builders-BKlmGDJl.mjs} +7 -3
  42. package/dist/planner-ddl-builders-BKlmGDJl.mjs.map +1 -0
  43. package/dist/planner-ddl-builders.d.mts.map +1 -1
  44. package/dist/planner-ddl-builders.mjs +1 -1
  45. package/dist/{planner-produced-postgres-migration-a-tjfKsA.d.mts → planner-produced-postgres-migration-CXue-6M9.d.mts} +2 -2
  46. package/dist/{planner-produced-postgres-migration-a-tjfKsA.d.mts.map → planner-produced-postgres-migration-CXue-6M9.d.mts.map} +1 -1
  47. package/dist/{planner-produced-postgres-migration-NI-oGKcw.mjs → planner-produced-postgres-migration-Jat3hg4u.mjs} +2 -2
  48. package/dist/{planner-produced-postgres-migration-NI-oGKcw.mjs.map → planner-produced-postgres-migration-Jat3hg4u.mjs.map} +1 -1
  49. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  50. package/dist/planner-produced-postgres-migration.mjs +1 -1
  51. package/dist/{planner-sql-checks-AXoxB1Jl.mjs → planner-sql-checks-Br1c_QsH.mjs} +3 -3
  52. package/dist/{planner-sql-checks-AXoxB1Jl.mjs.map → planner-sql-checks-Br1c_QsH.mjs.map} +1 -1
  53. package/dist/planner-sql-checks.mjs +1 -1
  54. package/dist/planner.d.mts +3 -3
  55. package/dist/planner.d.mts.map +1 -1
  56. package/dist/planner.mjs +2 -2
  57. package/dist/{postgres-contract-view-Dg-N7KA7.mjs → postgres-contract-view-BAPW0ec1.mjs} +3 -3
  58. package/dist/{postgres-contract-view-Dg-N7KA7.mjs.map → postgres-contract-view-BAPW0ec1.mjs.map} +1 -1
  59. package/dist/{postgres-database-schema-node-CfdTY-Qs.d.mts → postgres-database-schema-node-CJv5bfbs.d.mts} +70 -26
  60. package/dist/postgres-database-schema-node-CJv5bfbs.d.mts.map +1 -0
  61. package/dist/{postgres-migration-ChyRsWvE.mjs → postgres-migration-ChLMQRd-.mjs} +19 -3
  62. package/dist/postgres-migration-ChLMQRd-.mjs.map +1 -0
  63. package/dist/{postgres-migration-Bzzi0Asr.d.mts → postgres-migration-DkmgGNcv.d.mts} +21 -2
  64. package/dist/postgres-migration-DkmgGNcv.d.mts.map +1 -0
  65. package/dist/{postgres-role-schema-node-BiSTCTNg.mjs → postgres-role-schema-node-Cm-j0ZGM.mjs} +20 -25
  66. package/dist/postgres-role-schema-node-Cm-j0ZGM.mjs.map +1 -0
  67. package/dist/{postgres-schema-DTLA_aRi.mjs → postgres-schema-DWQ0-CPb.mjs} +32 -4
  68. package/dist/postgres-schema-DWQ0-CPb.mjs.map +1 -0
  69. package/dist/{postgres-schema-CU4gF6QZ.d.mts → postgres-schema-DsrtYp2v.d.mts} +13 -1
  70. package/dist/postgres-schema-DsrtYp2v.d.mts.map +1 -0
  71. package/dist/{postgres-table-schema-node-9FCtCUz2.mjs → postgres-table-schema-node-D6LvInCe.mjs} +71 -8
  72. package/dist/postgres-table-schema-node-D6LvInCe.mjs.map +1 -0
  73. package/dist/{resolve-ddl-schema-Ca88Tg6W.mjs → resolve-ddl-schema-CAza65Iy.mjs} +2 -2
  74. package/dist/{resolve-ddl-schema-Ca88Tg6W.mjs.map → resolve-ddl-schema-CAza65Iy.mjs.map} +1 -1
  75. package/dist/runtime.mjs +1 -1
  76. package/dist/schema-ir-annotations.mjs +1 -1
  77. package/dist/{sql-utils-DcfMz4MQ.mjs → sql-utils-SU4FDvIV.mjs} +12 -3
  78. package/dist/{sql-utils-DcfMz4MQ.mjs.map → sql-utils-SU4FDvIV.mjs.map} +1 -1
  79. package/dist/sql-utils.d.mts +8 -1
  80. package/dist/sql-utils.d.mts.map +1 -1
  81. package/dist/sql-utils.mjs +2 -2
  82. package/dist/types.d.mts +3 -3
  83. package/dist/types.mjs +4 -4
  84. package/package.json +20 -20
  85. package/src/contract-free/ddl.ts +27 -0
  86. package/src/core/authoring.ts +136 -25
  87. package/src/core/ddl/nodes.ts +44 -0
  88. package/src/core/migrations/contract-to-postgres-database-schema-node.ts +12 -1
  89. package/src/core/migrations/control-policy.ts +172 -45
  90. package/src/core/migrations/diff-database-schema.ts +48 -13
  91. package/src/core/migrations/issue-planner.ts +77 -4
  92. package/src/core/migrations/op-factory-call.ts +84 -0
  93. package/src/core/migrations/operations/native-enum-types.ts +62 -0
  94. package/src/core/migrations/planner-ddl-builders.ts +12 -2
  95. package/src/core/migrations/planner.ts +30 -62
  96. package/src/core/migrations/postgres-migration.ts +31 -0
  97. package/src/core/migrations/verify-postgres-namespaces.ts +0 -1
  98. package/src/core/postgres-role.ts +22 -0
  99. package/src/core/postgres-schema.ts +22 -1
  100. package/src/core/postgres-validators.ts +1 -0
  101. package/src/core/psl-infer/infer-psl-contract.ts +7 -16
  102. package/src/core/schema-ir/node-storage-coordinate.ts +26 -0
  103. package/src/core/schema-ir/postgres-database-schema-node.ts +5 -4
  104. package/src/core/schema-ir/postgres-namespace-schema-node.ts +18 -27
  105. package/src/core/schema-ir/postgres-native-enum-schema-node.ts +83 -0
  106. package/src/core/schema-ir/postgres-role-schema-node.ts +4 -2
  107. package/src/core/schema-ir/schema-node-kinds.ts +19 -3
  108. package/src/core/sql-utils.ts +15 -2
  109. package/src/exports/ddl.ts +2 -0
  110. package/src/exports/op-factory-call.ts +2 -0
  111. package/src/exports/sql-utils.ts +1 -0
  112. package/src/exports/types.ts +4 -1
  113. package/dist/authoring-WkqG_MkU.mjs.map +0 -1
  114. package/dist/control-policy-Co_13Hxa.mjs.map +0 -1
  115. package/dist/diff-database-schema-BOsDnH64.mjs.map +0 -1
  116. package/dist/nodes-B815pj_X.mjs.map +0 -1
  117. package/dist/nodes-BtQyZha6.d.mts.map +0 -1
  118. package/dist/op-factory-call-DxNNKUeZ.mjs.map +0 -1
  119. package/dist/planner-CNrc1Odu.mjs.map +0 -1
  120. package/dist/planner-ddl-builders-B8Nn6nHN.mjs.map +0 -1
  121. package/dist/postgres-database-schema-node-CfdTY-Qs.d.mts.map +0 -1
  122. package/dist/postgres-migration-Bzzi0Asr.d.mts.map +0 -1
  123. package/dist/postgres-migration-ChyRsWvE.mjs.map +0 -1
  124. package/dist/postgres-role-schema-node-BiSTCTNg.mjs.map +0 -1
  125. package/dist/postgres-schema-CU4gF6QZ.d.mts.map +0 -1
  126. package/dist/postgres-schema-DTLA_aRi.mjs.map +0 -1
  127. package/dist/postgres-table-schema-node-9FCtCUz2.mjs.map +0 -1
@@ -63,6 +63,7 @@ import {
63
63
  } from './operations/constraints';
64
64
  import { createExtension } from './operations/dependencies';
65
65
  import { createIndex, dropIndex } from './operations/indexes';
66
+ import { createNativeEnumType, dropNativeEnumType } from './operations/native-enum-types';
66
67
  import {
67
68
  createRlsPolicy,
68
69
  disableRowLevelSecurity,
@@ -1318,6 +1319,87 @@ export class CreateSchemaCall extends PostgresOpFactoryCallNode {
1318
1319
  }
1319
1320
  }
1320
1321
 
1322
+ // ============================================================================
1323
+ // Native enum types
1324
+ // ============================================================================
1325
+
1326
+ export class CreateNativeEnumTypeCall extends PostgresOpFactoryCallNode {
1327
+ readonly factoryName = 'createNativeEnumType' as const;
1328
+ readonly operationClass = 'additive' as const;
1329
+ readonly schemaName: string;
1330
+ readonly typeName: string;
1331
+ readonly members: readonly string[];
1332
+ readonly label: string;
1333
+
1334
+ constructor(schemaName: string, typeName: string, members: readonly string[]) {
1335
+ super();
1336
+ this.schemaName = schemaName;
1337
+ this.typeName = typeName;
1338
+ this.members = Object.freeze([...members]);
1339
+ this.label = `Create enum type "${typeName}"`;
1340
+ this.freeze();
1341
+ }
1342
+
1343
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
1344
+ if (lowerer === undefined) {
1345
+ throw new Error(
1346
+ `CreateNativeEnumTypeCall.toOp: a DDL lowerer is required on the Postgres planner path (type "${this.typeName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
1347
+ );
1348
+ }
1349
+ return createNativeEnumType(this.schemaName, this.typeName, this.members, lowerer);
1350
+ }
1351
+
1352
+ renderTypeScript(): string {
1353
+ const opts = [
1354
+ `schema: ${jsonToTsSource(this.schemaName)}`,
1355
+ `typeName: ${jsonToTsSource(this.typeName)}`,
1356
+ `members: ${jsonToTsSource(this.members)}`,
1357
+ ];
1358
+ return `this.createNativeEnumType({ ${opts.join(', ')} })`;
1359
+ }
1360
+
1361
+ override importRequirements(): readonly ImportRequirement[] {
1362
+ return [];
1363
+ }
1364
+ }
1365
+
1366
+ export class DropNativeEnumTypeCall extends PostgresOpFactoryCallNode {
1367
+ readonly factoryName = 'dropNativeEnumType' as const;
1368
+ readonly operationClass = 'destructive' as const;
1369
+ readonly schemaName: string;
1370
+ readonly typeName: string;
1371
+ readonly label: string;
1372
+
1373
+ constructor(schemaName: string, typeName: string) {
1374
+ super();
1375
+ this.schemaName = schemaName;
1376
+ this.typeName = typeName;
1377
+ this.label = `Drop enum type "${typeName}"`;
1378
+ this.freeze();
1379
+ }
1380
+
1381
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
1382
+ if (lowerer === undefined) {
1383
+ throw new Error(
1384
+ `DropNativeEnumTypeCall.toOp: a DDL lowerer is required on the Postgres planner path (type "${this.typeName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
1385
+ );
1386
+ }
1387
+ return dropNativeEnumType(this.schemaName, this.typeName, lowerer);
1388
+ }
1389
+
1390
+ renderTypeScript(): string {
1391
+ const opts = [
1392
+ `schema: ${jsonToTsSource(this.schemaName)}`,
1393
+ `typeName: ${jsonToTsSource(this.typeName)}`,
1394
+ ];
1395
+ return `this.dropNativeEnumType({ ${opts.join(', ')} })`;
1396
+ }
1397
+
1398
+ override importRequirements(): readonly ImportRequirement[] {
1399
+ return [];
1400
+ }
1401
+ }
1402
+
1321
1403
  // ============================================================================
1322
1404
  // Data transform
1323
1405
  // ============================================================================
@@ -1562,6 +1644,8 @@ export type PostgresOpFactoryCall =
1562
1644
  | RawSqlCall
1563
1645
  | CreateExtensionCall
1564
1646
  | CreateSchemaCall
1647
+ | CreateNativeEnumTypeCall
1648
+ | DropNativeEnumTypeCall
1565
1649
  | CreatePostgresRlsPolicyCall
1566
1650
  | DropPostgresRlsPolicyCall
1567
1651
  | EnableRowLevelSecurityCall
@@ -0,0 +1,62 @@
1
+ import type { ExecuteRequestLowerer } from '@prisma-next/family-sql/control-adapter';
2
+ import { ifDefined } from '@prisma-next/utils/defined';
3
+ import { createType, dropType } from '../../../contract-free/ddl';
4
+ import { validateEnumValueLength } from '../../sql-utils';
5
+ import { boundSchema } from '../bound-schema';
6
+ import { type Op, step, targetDetails } from './shared';
7
+
8
+ /**
9
+ * `CREATE TYPE <qualified> AS ENUM (…)` for a managed native enum, built as a
10
+ * typed DDL node and lowered by the control adapter (the type name is quoted,
11
+ * member values render as escaped string literals in declaration order —
12
+ * Postgres enum sort order is semantic). `boundSchema` maps the unbound sentinel
13
+ * to an absent schema so the type name renders unqualified and the connection's
14
+ * `search_path` resolves the schema at runtime.
15
+ */
16
+ export async function createNativeEnumType(
17
+ schemaName: string,
18
+ typeName: string,
19
+ members: readonly string[],
20
+ lowerer: ExecuteRequestLowerer,
21
+ ): Promise<Op> {
22
+ for (const member of members) {
23
+ validateEnumValueLength(member, typeName);
24
+ }
25
+ const ddlNode = createType({
26
+ ...ifDefined('schema', boundSchema(schemaName)),
27
+ name: typeName,
28
+ values: members,
29
+ });
30
+ const statement = await lowerer.lowerToExecuteRequest(ddlNode);
31
+ return {
32
+ id: `createNativeEnumType.${typeName}`,
33
+ label: `Create enum type "${typeName}"`,
34
+ operationClass: 'additive',
35
+ target: targetDetails('type', typeName, schemaName),
36
+ precheck: [],
37
+ execute: [step(`create enum type "${typeName}"`, statement.sql, statement.params)],
38
+ postcheck: [],
39
+ };
40
+ }
41
+
42
+ /** `DROP TYPE <qualified>` for an unclaimed managed native enum, via typed DDL. */
43
+ export async function dropNativeEnumType(
44
+ schemaName: string,
45
+ typeName: string,
46
+ lowerer: ExecuteRequestLowerer,
47
+ ): Promise<Op> {
48
+ const ddlNode = dropType({
49
+ ...ifDefined('schema', boundSchema(schemaName)),
50
+ name: typeName,
51
+ });
52
+ const statement = await lowerer.lowerToExecuteRequest(ddlNode);
53
+ return {
54
+ id: `dropNativeEnumType.${typeName}`,
55
+ label: `Drop enum type "${typeName}"`,
56
+ operationClass: 'destructive',
57
+ target: targetDetails('type', typeName, schemaName),
58
+ precheck: [],
59
+ execute: [step(`drop enum type "${typeName}"`, statement.sql, statement.params)],
60
+ postcheck: [],
61
+ };
62
+ }
@@ -1,7 +1,7 @@
1
1
  import type { CodecControlHooks } from '@prisma-next/family-sql/control';
2
2
  import type { StorageColumn, StorageTypeInstance } from '@prisma-next/sql-contract/types';
3
3
  import { ifDefined } from '@prisma-next/utils/defined';
4
- import { escapeLiteral, quoteIdentifier } from '../sql-utils';
4
+ import { escapeLiteral, quoteIdentifier, quoteQualifiedName } from '../sql-utils';
5
5
  import type { PostgresColumnDefault } from '../types';
6
6
  import { resolveColumnTypeMetadata } from './planner-type-resolution';
7
7
 
@@ -65,13 +65,23 @@ export function buildColumnTypeSql(
65
65
  }
66
66
  }
67
67
 
68
+ // A column whose codec supplied a `typeParams.typeName` references a named
69
+ // database type (e.g. a native enum), not a parameterized builtin: render it
70
+ // as its quoted, schema-qualified type-name identifier. DDL-render only — the
71
+ // verify comparison value (`resolvedNativeType`) stays the bare name the
72
+ // family expander produces, matching introspection.
73
+ if (typeof resolved.typeParams?.['typeName'] === 'string') {
74
+ const quoted = quoteQualifiedName(resolved.nativeType);
75
+ return column.many ? `${quoted}[]` : quoted;
76
+ }
77
+
68
78
  const expanded = expandParameterizedTypeSql(resolved, codecHooks);
69
79
  if (expanded !== null) {
70
80
  return column.many ? `${expanded}[]` : expanded;
71
81
  }
72
82
 
73
83
  if (column.typeRef) {
74
- const base = quoteIdentifier(resolved.nativeType);
84
+ const base = quoteQualifiedName(resolved.nativeType);
75
85
  return column.many ? `${base}[]` : base;
76
86
  }
77
87
 
@@ -28,24 +28,19 @@ import { blindCast } from '@prisma-next/utils/casts';
28
28
  import { ifDefined } from '@prisma-next/utils/defined';
29
29
  import { PostgresRlsPolicy } from '../postgres-rls-policy';
30
30
  import { parseRlsPolicyWireName } from '../rls/wire-name';
31
+ import { postgresNodeStorageCoordinate } from '../schema-ir/node-storage-coordinate';
31
32
  import { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
32
33
  import { PostgresPolicySchemaNode } from '../schema-ir/postgres-policy-schema-node';
33
- import { PostgresSchemaNodeKind, type SqlSchemaDiffNode } from '../schema-ir/schema-node-kinds';
34
+ import type { SqlSchemaDiffNode } from '../schema-ir/schema-node-kinds';
34
35
  import {
35
- formatPostgresControlPolicySubjectLabel,
36
+ renderPostgresSuppression,
36
37
  resolveNamespaceIdForDdlSchema,
37
38
  resolvePostgresCallControlPolicySubject,
38
39
  resolvePostgresNodeIssueControlPolicySubject,
39
40
  resolvePostgresNodeIssueCreationFactoryName,
40
41
  } from './control-policy';
41
42
  import { buildPostgresPlanDiff } from './diff-database-schema';
42
- import {
43
- coalesceSubtreeIssues,
44
- issueNode,
45
- issueSchemaName,
46
- issueTableName,
47
- planIssues,
48
- } from './issue-planner';
43
+ import { coalesceSubtreeIssues, issueNode, issueSchemaName, planIssues } from './issue-planner';
49
44
  import type { PostgresOpFactoryCall } from './op-factory-call';
50
45
  import {
51
46
  CreatePostgresRlsPolicyCall,
@@ -186,6 +181,11 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
186
181
  frameworkComponents: options.frameworkComponents,
187
182
  });
188
183
  const policyDiffIssues = rawIssues.filter((issue) => isPolicyDiffIssue(issue));
184
+ // Role diff issues resolve to the `external` control policy (see
185
+ // `resolvePostgresNodeIssueControlPolicySubject`'s role branch), so the
186
+ // control-policy partition below suppresses them to zero ops on its own,
187
+ // before `mapNodeIssueToCall` ever sees them — no separate exclusion
188
+ // needed here.
189
189
  const relationalDiffIssues = rawIssues.filter((issue) => !isPolicyDiffIssue(issue));
190
190
 
191
191
  // The generic differ is total and un-gated: strict-mode extras filtering
@@ -250,8 +250,6 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
250
250
  resolveControlPolicySubject: (issue) =>
251
251
  resolvePostgresNodeIssueControlPolicySubject(issue, options.contract),
252
252
  resolveCreationFactoryName: resolvePostgresNodeIssueCreationFactoryName,
253
- formatSubjectLabel: (factoryName, subject) =>
254
- formatPostgresControlPolicySubjectLabel(factoryName, subject, options.contract),
255
253
  });
256
254
 
257
255
  const result = planIssues({
@@ -283,8 +281,6 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
283
281
  resolveControlPolicySubject: (call) =>
284
282
  resolvePostgresCallControlPolicySubject(call, options.contract),
285
283
  resolveFactoryName: (call) => call.factoryName,
286
- formatSubjectLabel: (factoryName, subject) =>
287
- formatPostgresControlPolicySubjectLabel(factoryName, subject, options.contract),
288
284
  });
289
285
 
290
286
  // Inline `onFieldEvent`-emitted ops after structural DDL. The fixed
@@ -309,15 +305,13 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
309
305
  resolveControlPolicySubject: (call) =>
310
306
  resolvePostgresCallControlPolicySubject(call, options.contract),
311
307
  resolveFactoryName: (call) => call.factoryName,
312
- formatSubjectLabel: (factoryName, subject) =>
313
- formatPostgresControlPolicySubjectLabel(factoryName, subject, options.contract),
314
308
  });
315
309
  const calls = [...result.value.calls, ...schemaDiffPartition.kept, ...fieldEventPartition.kept];
316
310
  const warnings: SqlPlannerConflict[] = [
317
- ...issuePartition.warnings,
318
- ...schemaDiffPartition.warnings,
319
- ...fieldEventPartition.warnings,
320
- ];
311
+ ...issuePartition.suppressions,
312
+ ...schemaDiffPartition.suppressions,
313
+ ...fieldEventPartition.suppressions,
314
+ ].map((record) => renderPostgresSuppression(record, options.contract));
321
315
 
322
316
  return Object.freeze({
323
317
  kind: 'success' as const,
@@ -495,41 +489,14 @@ function isPolicyDiffIssue(issue: SchemaDiffIssue<SqlSchemaDiffNode>): boolean {
495
489
  }
496
490
 
497
491
  /**
498
- * Drops a `not-expected` issue when it is a whole extra TABLE that some
499
- * contract space owns, asking the ownership oracle per node.
500
- *
501
- * The consultation applies ONLY to table-level extras a live table this
502
- * space's contract lacks identified by asking the issue's own node
503
- * (`nodeKind === table`), never by counting path segments. `declaresEntity`
504
- * answers over the whole composition (self included), keyed on the schema-IR
505
- * entity coordinate so a genuine orphan in one namespace is never conflated
506
- * with a same-named table a sibling space declares in another, or with a
507
- * same-named non-table entity (an enum, an RLS policy) a sibling declares in
508
- * the SAME namespace: a positive answer means another space owns THIS table
509
- * in THIS namespace (a table this space owned would be in its expected tree,
510
- * never an extra) — leave it. A negative answer means no space owns it — a
511
- * genuine orphan to drop. `entityKind` is the literal `'table'` here: this
512
- * function only ever asks about a node already confirmed to be
513
- * `PostgresSchemaNodeKind.table` (checked just above), and the diff tree's
514
- * `nodeKind` vocabulary (`'postgres-table'`) is distinct from the storage
515
- * `entries` vocabulary `elementCoordinates` walks (`'table'`) — the literal
516
- * is that storage-entries spelling, not the node kind.
517
- *
518
- * The issue path carries the *resolved DDL schema* (e.g. `public`), but a
519
- * contract space's own declared namespace id can be the unbound sentinel
520
- * (`__unbound__`) that resolves to that schema — the two spellings would
521
- * never string-match. `resolveNamespaceIdForDdlSchema` recovers the raw
522
- * namespace id THIS space's own contract would use for that DDL schema, so
523
- * a table this space's own unbound namespace declares (and any sibling
524
- * whose own unbound namespace resolves the same way) is compared on the
525
- * same coordinate the aggregate's `elementCoordinates` walk yields.
526
- *
527
- * A DEEPER extra (an extra column/constraint on a table this space DOES own —
528
- * only the child drifted, so the table is in the expected tree) is this
529
- * space's own drift and is always kept for dropping; asking the oracle there
530
- * would wrongly suppress it, because the owned table answers `true`. Shallower
531
- * issues (namespace/root) are never ownership-scoped here. No oracle ⇒ every
532
- * extra is kept (single-space plan).
492
+ * Drops a `not-expected` issue when it is a whole extra storage entity (a table
493
+ * or a native enum) that some space in the composition owns. Each such node
494
+ * yields its own storage coordinate (see {@link postgresNodeStorageCoordinate}),
495
+ * so `declaresEntity` answers over the whole composition on one uniform
496
+ * coordinate: a positive answer means a sibling owns this entity here — leave
497
+ * it; a negative answer means a genuine orphan — drop it. A node with no storage
498
+ * coordinate (a namespace, or a deeper column/constraint drift on an owned
499
+ * table) is this space's own and is always kept. No oracle ⇒ keep everything.
533
500
  */
534
501
  function retainUnownedExtras(
535
502
  issues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[],
@@ -540,12 +507,13 @@ function retainUnownedExtras(
540
507
  return issues.filter((issue) => {
541
508
  if (issue.reason !== 'not-expected') return true;
542
509
  const node = issueNode(issue);
543
- if (node === undefined || node.nodeKind !== PostgresSchemaNodeKind.table) return true;
510
+ if (node === undefined) return true;
511
+ const coordinate = postgresNodeStorageCoordinate(node);
512
+ if (coordinate === undefined) return true;
544
513
  const ddlSchemaName = issueSchemaName(issue);
545
- const tableName = issueTableName(issue);
546
- if (ddlSchemaName === undefined || tableName === undefined) return true;
514
+ if (ddlSchemaName === undefined) return true;
547
515
  const namespaceId = resolveNamespaceIdForDdlSchema(contract, ddlSchemaName);
548
- return !ownership.declaresEntity({ namespaceId, entityKind: 'table', entityName: tableName });
516
+ return !ownership.declaresEntity({ namespaceId, ...coordinate });
549
517
  });
550
518
  }
551
519
 
@@ -562,9 +530,9 @@ function retainUnownedExtras(
562
530
  * Returns the real `PostgresNamespaceSchemaNode` reference rather than a
563
531
  * projection: `storageTypePlanCallStrategy` hands this same value to codec
564
532
  * `planTypeOperations` hooks as `schema`, and hooks read the Postgres-specific
565
- * `nativeEnumTypeNames` field off it (via `PostgresNamespaceSchemaNode.is`) to
566
- * decide whether a native enum type already exists — a projection that only
567
- * copies `tables` would silently drop that signal. `StrategyContext.schema`'s
533
+ * `nativeEnums` field off it (via `PostgresNamespaceSchemaNode.is`) to decide
534
+ * whether a native enum type already exists — a projection that only copies
535
+ * `tables` would silently drop that signal. `StrategyContext.schema`'s
568
536
  * declared type (`SqlSchemaIR`, shared with SQLite's flat shape) doesn't
569
537
  * capture this, so the return is `blindCast` the same way `namespaceSchemaNodes`
570
538
  * in the family's relational walk already treats a namespace node as a
@@ -580,7 +548,7 @@ function relationalNamespaceNode(
580
548
  if (namespaceNode === undefined) return undefined;
581
549
  return blindCast<
582
550
  SqlSchemaIR,
583
- 'PostgresNamespaceSchemaNode carries tables (+ nativeEnumTypeNames, read by codec hooks via PostgresNamespaceSchemaNode.is) structurally compatible with the SqlSchemaIR shape the strategy layer declares'
551
+ 'PostgresNamespaceSchemaNode carries tables (+ nativeEnums, read by codec hooks via PostgresNamespaceSchemaNode.is) structurally compatible with the SqlSchemaIR shape the strategy layer declares'
584
552
  >(namespaceNode);
585
553
  }
586
554
 
@@ -17,6 +17,7 @@ import {
17
17
  AlterColumnTypeCall,
18
18
  type AlterColumnTypeOptions,
19
19
  CreateIndexCall,
20
+ CreateNativeEnumTypeCall,
20
21
  CreateSchemaCall,
21
22
  CreateTableCall,
22
23
  DropCheckConstraintCall,
@@ -24,6 +25,7 @@ import {
24
25
  DropConstraintCall,
25
26
  DropDefaultCall,
26
27
  DropIndexCall,
28
+ DropNativeEnumTypeCall,
27
29
  DropNotNullCall,
28
30
  DropTableCall,
29
31
  SetDefaultCall,
@@ -171,6 +173,35 @@ export abstract class PostgresMigration<
171
173
  return new CreateSchemaCall(options.schema).toOp(this.controlAdapterFor('createSchema'));
172
174
  }
173
175
 
176
+ /**
177
+ * Emit a `CREATE TYPE ... AS ENUM (...)` migration operation for a managed
178
+ * native enum. Builds a typed DDL node and lowers it through the stored
179
+ * control adapter (members render in declaration order). Throws if no adapter
180
+ * is present.
181
+ */
182
+ protected createNativeEnumType(options: {
183
+ readonly schema: string;
184
+ readonly typeName: string;
185
+ readonly members: readonly string[];
186
+ }): Promise<SqlMigrationPlanOperation<PostgresPlanTargetDetails>> {
187
+ return new CreateNativeEnumTypeCall(options.schema, options.typeName, options.members).toOp(
188
+ this.controlAdapterFor('createNativeEnumType'),
189
+ );
190
+ }
191
+
192
+ /**
193
+ * Emit a `DROP TYPE` migration operation for a managed native enum, lowered
194
+ * through the stored control adapter. Throws if no adapter is present.
195
+ */
196
+ protected dropNativeEnumType(options: {
197
+ readonly schema: string;
198
+ readonly typeName: string;
199
+ }): Promise<SqlMigrationPlanOperation<PostgresPlanTargetDetails>> {
200
+ return new DropNativeEnumTypeCall(options.schema, options.typeName).toOp(
201
+ this.controlAdapterFor('dropNativeEnumType'),
202
+ );
203
+ }
204
+
174
205
  protected addColumn(options: {
175
206
  readonly schema: string;
176
207
  readonly table: string;
@@ -91,7 +91,6 @@ export function verifyPostgresNamespacePresence(input: {
91
91
  const namespace = new PostgresNamespaceSchemaNode({
92
92
  schemaName: ddlName,
93
93
  tables: {},
94
- nativeEnumTypeNames: [],
95
94
  });
96
95
  issues.push({
97
96
  path: ['database', ddlName],
@@ -1,6 +1,15 @@
1
1
  import { freezeNode } from '@prisma-next/framework-components/ir';
2
2
  import { SqlNode } from '@prisma-next/sql-contract/types';
3
3
 
4
+ /**
5
+ * Roles are referenced by the contract but never owned — the framework
6
+ * issues no `CREATE`/`DROP ROLE`, so a role's effective governance is always
7
+ * `external`: a missing declared role fails verify, an extra live role is
8
+ * tolerated, and the planner emits zero role DDL. `external` is the only
9
+ * value a role's control can ever carry.
10
+ */
11
+ export const ROLE_DEFAULT_CONTROL_POLICY = 'external';
12
+
4
13
  export interface PostgresRoleInput {
5
14
  readonly name: string;
6
15
  /**
@@ -8,6 +17,11 @@ export interface PostgresRoleInput {
8
17
  * from `@prisma-next/framework-components/ir`.
9
18
  */
10
19
  readonly namespaceId: string;
20
+ /**
21
+ * Defaults to {@link ROLE_DEFAULT_CONTROL_POLICY} when omitted. Roles are
22
+ * referenced but never owned, so `'external'` is the only allowed value.
23
+ */
24
+ readonly control?: 'external';
11
25
  }
12
26
 
13
27
  /**
@@ -22,16 +36,24 @@ export interface PostgresRoleInput {
22
36
  * Extends `SqlNode` directly, frozen at construction via `freezeNode(this)`.
23
37
  * The `kind: 'role'` discriminant is enumerable so it survives JSON.
24
38
  * Matches the entries key (one-string rule).
39
+ *
40
+ * `control` always carries a value (never omitted): it defaults to
41
+ * {@link ROLE_DEFAULT_CONTROL_POLICY} in the constructor, so both a
42
+ * freshly-authored role and one hydrated from an older `contract.json`
43
+ * lacking the field resolve the same way — the control-policy resolver
44
+ * reads it directly off the entity instead of special-casing role issues.
25
45
  */
26
46
  export class PostgresRole extends SqlNode {
27
47
  override readonly kind = 'role' as const;
28
48
  readonly name: string;
29
49
  readonly namespaceId: string;
50
+ readonly control: 'external';
30
51
 
31
52
  constructor(input: PostgresRoleInput) {
32
53
  super();
33
54
  this.name = input.name;
34
55
  this.namespaceId = input.namespaceId;
56
+ this.control = input.control ?? ROLE_DEFAULT_CONTROL_POLICY;
35
57
  freezeNode(this);
36
58
  }
37
59
  }
@@ -15,6 +15,7 @@ import {
15
15
  } from '@prisma-next/sql-contract/types';
16
16
 
17
17
  import { type CfExpr, cfExpr } from '@prisma-next/sql-relational-core/contract-free';
18
+ import { invariant } from '@prisma-next/utils/assertions';
18
19
  import { blindCast } from '@prisma-next/utils/casts';
19
20
  import { ifDefined } from '@prisma-next/utils/defined';
20
21
  import { PostgresTableSource } from './ast/table-source';
@@ -25,7 +26,7 @@ import {
25
26
  rlsEnablementEntityKind,
26
27
  roleEntityKind,
27
28
  } from './entity-kinds';
28
- import type { PostgresNativeEnum } from './postgres-native-enum';
29
+ import { PostgresNativeEnum } from './postgres-native-enum';
29
30
  import type { PostgresRlsEnablement } from './postgres-rls-enablement';
30
31
  import type { PostgresRlsPolicy } from './postgres-rls-policy';
31
32
  import type { PostgresRole } from './postgres-role';
@@ -88,6 +89,26 @@ export class PostgresSchema extends SqlNamespaceBase {
88
89
  'carry',
89
90
  );
90
91
 
92
+ // A native enum keys under its PHYSICAL type name in storage `entries`
93
+ // (ADR 221 coordinate `entityName`). The family/authoring layer keys every
94
+ // block by its author handle, so re-key the `native_enum` slot by each
95
+ // entity's `typeName` here — target-local Postgres code that already knows
96
+ // this entity kind. This runs at both authoring and hydration; a serialized
97
+ // contract's key already equals the `typeName`, so the re-key is idempotent.
98
+ const nativeEnumSlot = dispatched['native_enum'];
99
+ if (nativeEnumSlot !== undefined) {
100
+ const rekeyed: Record<string, unknown> = {};
101
+ for (const [handle, entity] of Object.entries(nativeEnumSlot)) {
102
+ const physicalName = PostgresNativeEnum.is(entity) ? entity.typeName : handle;
103
+ invariant(
104
+ !Object.hasOwn(rekeyed, physicalName),
105
+ `PostgresSchema "${input.id}": two native_enum entities resolve to the same physical type name "${physicalName}". Give them distinct @@map type names.`,
106
+ );
107
+ rekeyed[physicalName] = entity;
108
+ }
109
+ dispatched['native_enum'] = Object.freeze(rekeyed);
110
+ }
111
+
91
112
  // Drop an empty valueSet so presence signals non-emptiness.
92
113
  const valueSetRaw = dispatched['valueSet'];
93
114
  const withPresence =
@@ -4,6 +4,7 @@ export const PostgresRoleSchema = type({
4
4
  kind: "'role'",
5
5
  name: 'string',
6
6
  namespaceId: 'string',
7
+ 'control?': "'external'",
7
8
  });
8
9
 
9
10
  export const PostgresRlsPolicySchema = type({
@@ -300,10 +300,10 @@ export function inferPostgresPslContract(
300
300
  const owners = describedContractOwners(describedContracts ?? []);
301
301
  const enumOwners = describedNativeEnumOwnersByTypeName(describedContracts ?? []);
302
302
 
303
- // Native enum adoption: each namespace's introspected `{ typeName, values }`
304
- // entries become `native_enum` blocks + `pg.enum(<Name>)` columns, minus the
305
- // types a described pack contract already declares (matched by TYPE NAME —
306
- // see describedNativeEnumOwnersByTypeName). Transitional grade gap: an
303
+ // Native enum adoption: each namespace's introspected `enums` nodes become
304
+ // `native_enum` blocks + `pg.enum(<Name>)` columns, minus the types a
305
+ // described pack contract already declares (matched by TYPE NAME — see
306
+ // describedNativeEnumOwnersByTypeName). Transitional grade gap: an
307
307
  // inferred block carries no explicit `control` and inherits the contract's
308
308
  // `defaultControl`; under `defaultControl: 'managed'` the planner does not
309
309
  // yet enforce the native enum lifecycle (CREATE TYPE / DROP TYPE / ADD
@@ -313,17 +313,7 @@ export function inferPostgresPslContract(
313
313
  const packOwnedEnumTypesByNamespace = new Map<string, Map<string, string>>();
314
314
  const enumNamespaceNames = new Set<string>();
315
315
  for (const namespace of namespaces) {
316
- const definitionsForNamespace = new Set(namespace.nativeEnums.map((e) => e.typeName));
317
- for (const typeName of namespace.nativeEnumTypeNames) {
318
- if (!definitionsForNamespace.has(typeName)) {
319
- throw new Error(
320
- `contract infer: introspection reported native enum type "${typeName}" in schema ` +
321
- `"${namespace.schemaName}" without member values — the introspected tree's ` +
322
- '`nativeEnumTypeNames` and `nativeEnums` are out of sync (internal invariant).',
323
- );
324
- }
325
- }
326
- for (const { typeName, values } of namespace.nativeEnums) {
316
+ for (const { typeName, members } of namespace.nativeEnums) {
327
317
  const owner = enumOwners.get(
328
318
  coordinateKey({
329
319
  namespaceId: namespace.schemaName,
@@ -341,7 +331,7 @@ export function inferPostgresPslContract(
341
331
  packOwnedEnumTypesByNamespace.set(namespace.schemaName, owned);
342
332
  continue;
343
333
  }
344
- enumDefinitions.set(typeName, values);
334
+ enumDefinitions.set(typeName, members);
345
335
  enumNamespaceNames.add(namespace.schemaName);
346
336
  }
347
337
  }
@@ -663,6 +653,7 @@ function buildNativeEnumBlock(
663
653
 
664
654
  return {
665
655
  kind: 'native_enum',
656
+ keyword: 'native_enum',
666
657
  name,
667
658
  parameters,
668
659
  blockAttributes:
@@ -0,0 +1,26 @@
1
+ import type { EntityCoordinate } from '@prisma-next/framework-components/ir';
2
+ import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
3
+ import { PostgresNativeEnumSchemaNode } from './postgres-native-enum-schema-node';
4
+ import { PostgresTableSchemaNode } from './postgres-table-schema-node';
5
+ import { postgresNodeEntityKind } from './schema-node-kinds';
6
+
7
+ /**
8
+ * The storage-`entries` coordinate `(entityKind, entityName)` a Postgres diff
9
+ * node addresses — a table by its name, a native enum by its physical type
10
+ * name. `undefined` for a node that is not a whole storage entity (a namespace,
11
+ * a column, a policy). Lets ownership/subject resolution treat every entity kind
12
+ * uniformly, the node self-describing its storage identity.
13
+ */
14
+ export function postgresNodeStorageCoordinate(
15
+ node: SqlSchemaIRNode,
16
+ ): Pick<EntityCoordinate, 'entityKind' | 'entityName'> | undefined {
17
+ if (PostgresTableSchemaNode.is(node)) {
18
+ const entityKind = postgresNodeEntityKind(node.nodeKind);
19
+ return entityKind === undefined ? undefined : { entityKind, entityName: node.name };
20
+ }
21
+ if (PostgresNativeEnumSchemaNode.is(node)) {
22
+ const entityKind = postgresNodeEntityKind(node.nodeKind);
23
+ return entityKind === undefined ? undefined : { entityKind, entityName: node.typeName };
24
+ }
25
+ return undefined;
26
+ }
@@ -17,9 +17,10 @@ export interface PostgresDatabaseSchemaNodeInput {
17
17
  *
18
18
  * `id` is the fixed sentinel `'database'` — the root has no siblings and
19
19
  * the value is never emitted into migration paths. `isEqualTo` is identity
20
- * (roots always share the `'database'` id). `children()` returns namespace
21
- * nodes only; roles are held on the root but not yielded (role diffing is a
22
- * later slice).
20
+ * (roots always share the `'database'` id). `children()` yields the namespace
21
+ * nodes followed by the role nodes both are root-level diff subjects. The
22
+ * differ pairs siblings by `(nodeKind, id)`, so a role and a namespace may
23
+ * share a name without colliding.
23
24
  */
24
25
  export class PostgresDatabaseSchemaNode extends SqlSchemaIRNode implements DiffableNode {
25
26
  override readonly nodeKind = PostgresSchemaNodeKind.database;
@@ -47,7 +48,7 @@ export class PostgresDatabaseSchemaNode extends SqlSchemaIRNode implements Diffa
47
48
  }
48
49
 
49
50
  children(): readonly DiffableNode[] {
50
- return Object.values(this.namespaces);
51
+ return [...Object.values(this.namespaces), ...this.roles];
51
52
  }
52
53
 
53
54
  static is(node: SqlSchemaIRNode): node is PostgresDatabaseSchemaNode {