@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
@@ -1,11 +1,17 @@
1
- import type { Contract } from '@prisma-next/contract/types';
2
- import type { ControlPolicySubject } from '@prisma-next/family-sql/control';
1
+ import type { Contract, ControlPolicy } from '@prisma-next/contract/types';
2
+ import type {
3
+ ControlPolicySubject,
4
+ SqlPlannerConflict,
5
+ SuppressionRecord,
6
+ } from '@prisma-next/family-sql/control';
3
7
  import type { SchemaDiffIssue } from '@prisma-next/framework-components/control';
4
8
  import { entityAt, UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
5
9
  import type { SqlStorage, StorageTable } from '@prisma-next/sql-contract/types';
6
10
  import { blindCast } from '@prisma-next/utils/casts';
7
11
  import { ifDefined } from '@prisma-next/utils/defined';
12
+ import { type PostgresRole, ROLE_DEFAULT_CONTROL_POLICY } from '../postgres-role';
8
13
  import { isPostgresSchema } from '../postgres-schema';
14
+ import { postgresNodeStorageCoordinate } from '../schema-ir/node-storage-coordinate';
9
15
  import type { PostgresNamespaceSchemaNode } from '../schema-ir/postgres-namespace-schema-node';
10
16
  import { PostgresTableSchemaNode } from '../schema-ir/postgres-table-schema-node';
11
17
  import type { SqlSchemaDiffNode } from '../schema-ir/schema-node-kinds';
@@ -23,13 +29,16 @@ import type { PostgresOpFactoryCall } from './op-factory-call';
23
29
  * and is fail-closed. Any call not listed here — including future or
24
30
  * extension-contributed factories — is treated as NOT object-creation, so it
25
31
  * is suppressed under `tolerated` rather than permissively emitted.
32
+ *
33
+ * Lists the creation factories that actually reach the call-side resolver
34
+ * (`resolvePostgresCallControlPolicySubject`) — today only RLS policy
35
+ * creation, from `planPostgresSchemaDiff`. Every other whole-object create
36
+ * (table, schema, native enum, and RLS enablement) is constructed inside
37
+ * `planIssues` and graded on the node side by
38
+ * `resolvePostgresNodeIssueCreationFactoryName`, so none of them are listed
39
+ * here.
26
40
  */
27
- const OBJECT_CREATION_FACTORIES: ReadonlySet<string> = new Set<string>([
28
- 'createTable',
29
- 'createSchema',
30
- 'createRlsPolicy',
31
- 'enableRowLevelSecurity',
32
- ]);
41
+ const OBJECT_CREATION_FACTORIES: ReadonlySet<string> = new Set<string>(['createRlsPolicy']);
33
42
 
34
43
  function createsNewTopLevelObject(call: PostgresOpFactoryCall): boolean {
35
44
  return OBJECT_CREATION_FACTORIES.has(call.factoryName);
@@ -40,18 +49,25 @@ function ddlSchemaNameForNamespace(contract: Contract<SqlStorage>, namespaceId:
40
49
  return isPostgresSchema(namespace) ? namespace.ddlSchemaName(contract.storage) : namespaceId;
41
50
  }
42
51
 
43
- function resolveNamespaceIdForTable(
52
+ /**
53
+ * Resolve the namespace a declared storage entity lives in by walking every
54
+ * namespace for one whose `entries` map actually contains the coordinate —
55
+ * a table by its name, a native enum by its physical type name (see
56
+ * {@link postgresNodeStorageCoordinate}). When `ddlSchemaName` is given, the
57
+ * match must also land in that DDL schema (disambiguates same-named entities
58
+ * declared under different namespaces); when omitted, the first namespace
59
+ * that declares the entity wins. Falls back to {@link UNBOUND_NAMESPACE_ID}
60
+ * when no namespace declares it — e.g. an extra/dropped entity the contract
61
+ * doesn't claim at all.
62
+ */
63
+ function resolveNamespaceIdForEntity(
44
64
  contract: Contract<SqlStorage>,
45
- tableName: string,
65
+ coordinate: { readonly entityKind: string; readonly entityName: string },
46
66
  ddlSchemaName: string | undefined,
47
67
  ): string {
48
68
  for (const namespaceId of Object.keys(contract.storage.namespaces)) {
49
- const table = entityAt<StorageTable>(contract.storage, {
50
- namespaceId,
51
- entityKind: 'table',
52
- entityName: tableName,
53
- });
54
- if (!table) continue;
69
+ const entity = entityAt(contract.storage, { namespaceId, ...coordinate });
70
+ if (!entity) continue;
55
71
  if (
56
72
  ddlSchemaName === undefined ||
57
73
  ddlSchemaNameForNamespace(contract, namespaceId) === ddlSchemaName
@@ -62,6 +78,18 @@ function resolveNamespaceIdForTable(
62
78
  return UNBOUND_NAMESPACE_ID;
63
79
  }
64
80
 
81
+ function resolveNamespaceIdForTable(
82
+ contract: Contract<SqlStorage>,
83
+ tableName: string,
84
+ ddlSchemaName: string | undefined,
85
+ ): string {
86
+ return resolveNamespaceIdForEntity(
87
+ contract,
88
+ { entityKind: 'table', entityName: tableName },
89
+ ddlSchemaName,
90
+ );
91
+ }
92
+
65
93
  export function resolveNamespaceIdForDdlSchema(
66
94
  contract: Contract<SqlStorage>,
67
95
  ddlSchemaName: string,
@@ -92,16 +120,60 @@ function postgresCallFields(call: PostgresOpFactoryCall): PostgresCallFields {
92
120
  };
93
121
  }
94
122
 
95
- export function formatPostgresControlPolicySubjectLabel(
96
- factoryName: string,
123
+ function formatSuppressionSubjectLabel(
97
124
  subject: ControlPolicySubject | undefined,
98
125
  contract: Contract<SqlStorage>,
99
126
  ): string {
100
- if (subject?.table) {
101
- const ddlSchema = ddlSchemaNameForNamespace(contract, subject.namespaceId);
102
- return `${factoryName}(${ddlSchema}.${subject.table})`;
127
+ if (subject === undefined) return 'unknown';
128
+ const ddlSchema = ddlSchemaNameForNamespace(contract, subject.namespaceId);
129
+ if (subject.entityKind !== undefined && subject.entityName !== undefined) {
130
+ return `${subject.entityKind} "${ddlSchema}.${subject.entityName}"`;
131
+ }
132
+ return `namespace "${ddlSchema}"`;
133
+ }
134
+
135
+ function postgresSuppressionSummary(
136
+ subjectLabel: string,
137
+ subject: ControlPolicySubject | undefined,
138
+ policy: string,
139
+ ): string {
140
+ const namespace = subject?.namespaceId ?? 'unknown';
141
+ const declared = subject?.explicitNodeControlPolicy;
142
+ if (policy === 'external' && declared === 'managed') {
143
+ return `control policy suppressed: ${subjectLabel} — namespace '${namespace}' has effective control 'external' but declared 'managed'`;
103
144
  }
104
- return factoryName;
145
+ const declaredSuffix = declared ? ` but declared '${declared}'` : '';
146
+ return `control policy suppressed: ${subjectLabel} — namespace '${namespace}' has effective control '${policy}'${declaredSuffix}`;
147
+ }
148
+
149
+ /**
150
+ * Render one family {@link SuppressionRecord} into a target `SqlPlannerConflict`.
151
+ * The family decides *that* a subject is suppressed and hands over the raw
152
+ * coordinate + policy; the label, message, and location are rendered here,
153
+ * driven entirely by the subject's own `(entityKind, entityName)` coordinate
154
+ * — no target-owned table-vs-enum vocabulary.
155
+ */
156
+ export function renderPostgresSuppression(
157
+ record: SuppressionRecord,
158
+ contract: Contract<SqlStorage>,
159
+ ): SqlPlannerConflict {
160
+ const subject = record.subject;
161
+ const subjectLabel = formatSuppressionSubjectLabel(subject, contract);
162
+ return {
163
+ kind: 'controlPolicySuppressedCall',
164
+ summary: postgresSuppressionSummary(subjectLabel, subject, record.policy),
165
+ location: {
166
+ ...ifDefined('namespaceId', subject?.namespaceId),
167
+ ...ifDefined('entityKind', subject?.entityKind),
168
+ ...ifDefined('entityName', subject?.entityName),
169
+ ...ifDefined('column', subject?.column),
170
+ },
171
+ meta: {
172
+ controlPolicy: record.policy,
173
+ ...ifDefined('factoryName', record.factoryName),
174
+ ...ifDefined('declaredControlPolicy', subject?.explicitNodeControlPolicy),
175
+ },
176
+ };
105
177
  }
106
178
 
107
179
  export function resolvePostgresCallControlPolicySubject(
@@ -131,9 +203,10 @@ export function resolvePostgresCallControlPolicySubject(
131
203
  })?.control;
132
204
  return {
133
205
  namespaceId,
134
- ...ifDefined('explicitNodeControlPolicy', tableControlPolicy),
135
- table: callFields.tableName,
206
+ entityKind: 'table',
207
+ entityName: callFields.tableName,
136
208
  ...ifDefined('column', callFields.columnName),
209
+ ...ifDefined('explicitNodeControlPolicy', tableControlPolicy),
137
210
  createsNewObject,
138
211
  };
139
212
  }
@@ -149,10 +222,10 @@ export function resolvePostgresCallControlPolicySubject(
149
222
  }
150
223
 
151
224
  /**
152
- * Node kinds that describe the absence of a whole, top-level Postgres
153
- * object the same objects `createsNewTopLevelObject` recognises for calls.
154
- * Used by {@link resolvePostgresNodeIssueCreationFactoryName} to decide
155
- * whether a `tolerated` subject permits the issue to flow into the planner
225
+ * Node kinds whose *absence* is the creation of a whole, top-level Postgres
226
+ * object: a namespace, a table, or a native enum. Used by
227
+ * {@link resolvePostgresNodeIssueCreationFactoryName} to decide whether a
228
+ * `tolerated` subject permits the issue to flow into the planner
156
229
  * (create-if-absent) and to seed the suppressed-subject warning's
157
230
  * `factoryName` when the planner is skipped. RLS policy creation is not
158
231
  * listed here — policy issues never reach this issue-based partition (they
@@ -162,13 +235,15 @@ export function resolvePostgresCallControlPolicySubject(
162
235
  const POSTGRES_NODE_CREATION_FACTORY: Readonly<Record<string, string>> = Object.freeze({
163
236
  [PostgresSchemaNodeKind.namespace]: 'createSchema',
164
237
  [PostgresSchemaNodeKind.table]: 'createTable',
238
+ [PostgresSchemaNodeKind.nativeEnum]: 'createNativeEnumType',
165
239
  });
166
240
 
167
241
  /**
168
242
  * A table `not-equal` issue whose `rlsEnabled` flips OFF→ON (expected on,
169
- * actual off) is enablement toward `ENABLE ROW LEVEL SECURITY`
170
- * creation-class per `OBJECT_CREATION_FACTORIES` ('enableRowLevelSecurity'
171
- * is a member): enabling RLS establishes the fail-closed guard the declared
243
+ * actual off) is enablement toward `ENABLE ROW LEVEL SECURITY`. It is
244
+ * creation-class on the node side because `isEnablementCreationIssue` and
245
+ * {@link resolvePostgresNodeIssueCreationFactoryName} treat this OFF→ON delta
246
+ * as a creation: enabling RLS establishes the fail-closed guard the declared
172
247
  * policy set attaches to, the same grant `tolerated` extends to creating the
173
248
  * policies themselves. The opposite direction (`DISABLE`) is a modification
174
249
  * and stays managed-only. Keying on the actual delta (not just the expected
@@ -206,15 +281,36 @@ export function resolvePostgresNodeIssueCreationFactoryName(
206
281
  * {@link SchemaDiffIssue}. Mirrors the resolution
207
282
  * `resolvePostgresCallControlPolicySubject` performs for a generated DDL
208
283
  * call, but works *off the issue* — so the planner can partition issues by
209
- * effective policy before the diff engine runs. `createsNewObject` is
210
- * derived from the node kind + reason: a `not-found` schema/table describes
211
- * a brand-new top-level object; everything else touches an existing object.
284
+ * effective policy before the diff engine runs.
212
285
  *
213
- * A `not-expected` table carries no contract namespace coordinate (the live
214
- * table isn't claimed by any contract namespace), so the subject's
215
- * `namespaceId` falls back to {@link UNBOUND_NAMESPACE_ID} via
216
- * `resolveNamespaceIdForTable`'s own fallback the call-side resolver does
217
- * the same for the `DropTableCall` it produces.
286
+ * A whole storage entity (table or native enum) resolves generically off
287
+ * {@link postgresNodeStorageCoordinate} the node self-describes its
288
+ * `(entityKind, entityName)`, so table and enum issues share one code path
289
+ * with no per-kind branch. A sub-entity issue (a column/constraint drift
290
+ * inside an owned table) carries no such coordinate on its own node, so it
291
+ * falls back to reading the enclosing table name off the issue path.
292
+ *
293
+ * `createsNewObject` is delegated to {@link resolvePostgresNodeIssueCreationFactoryName}
294
+ * in every branch below — that function already encodes exactly which
295
+ * node-kind/reason combinations describe a brand-new top-level object
296
+ * (a `not-found` table/enum, or an RLS-enablement `not-equal`).
297
+ *
298
+ * A `not-expected` (extra/dropped) entity carries no contract namespace
299
+ * coordinate — the live object isn't claimed by any contract namespace — so
300
+ * the subject's `namespaceId` falls back to {@link UNBOUND_NAMESPACE_ID} via
301
+ * `resolveNamespaceIdForEntity`'s own fallback, matching how the call-side
302
+ * resolver treats the `DropTableCall`/`DropNativeEnumTypeCall` it produces.
303
+ *
304
+ * A role node resolves against the `PostgresRole` contract entity at that
305
+ * role's name (roles are root-level diff subjects, so the name is the
306
+ * second path segment after the tree root). A `not-found` role is declared,
307
+ * so the entity is always found and its `control` read directly — which is
308
+ * always {@link ROLE_DEFAULT_CONTROL_POLICY} today, since no authoring
309
+ * surface sets it otherwise. A `not-expected` role has no contract entity
310
+ * (an undeclared live role, by definition), so the subject falls back to
311
+ * the same default — roles are referenced but never owned, so an
312
+ * undeclared role gets exactly the same governance as a declared one.
313
+ * Never creates a new object: role provisioning is a non-goal.
218
314
  */
219
315
  export function resolvePostgresNodeIssueControlPolicySubject(
220
316
  issue: SchemaDiffIssue<SqlSchemaDiffNode>,
@@ -234,6 +330,39 @@ export function resolvePostgresNodeIssueControlPolicySubject(
234
330
  };
235
331
  }
236
332
 
333
+ if (node.nodeKind === PostgresSchemaNodeKind.role) {
334
+ const roleName = issue.path[1];
335
+ const roleEntity =
336
+ roleName === undefined
337
+ ? undefined
338
+ : entityAt<PostgresRole>(contract.storage, {
339
+ namespaceId: UNBOUND_NAMESPACE_ID,
340
+ entityKind: 'role',
341
+ entityName: roleName,
342
+ });
343
+ return {
344
+ namespaceId: UNBOUND_NAMESPACE_ID,
345
+ explicitNodeControlPolicy: roleEntity?.control ?? ROLE_DEFAULT_CONTROL_POLICY,
346
+ createsNewObject: false,
347
+ };
348
+ }
349
+
350
+ const coordinate = postgresNodeStorageCoordinate(node);
351
+ if (coordinate !== undefined) {
352
+ const namespaceId = resolveNamespaceIdForEntity(contract, coordinate, issue.path[1]);
353
+ const entityControl = entityAt<{ readonly control?: ControlPolicy }>(contract.storage, {
354
+ namespaceId,
355
+ ...coordinate,
356
+ })?.control;
357
+ return {
358
+ namespaceId,
359
+ ...coordinate,
360
+ ...ifDefined('column', issueColumnName(issue)),
361
+ ...ifDefined('explicitNodeControlPolicy', entityControl),
362
+ createsNewObject: resolvePostgresNodeIssueCreationFactoryName(issue) !== undefined,
363
+ };
364
+ }
365
+
237
366
  const tableName = issue.path[2];
238
367
  if (tableName === undefined) return undefined;
239
368
  const ddlSchemaName = issue.path[1];
@@ -243,15 +372,13 @@ export function resolvePostgresNodeIssueControlPolicySubject(
243
372
  entityKind: 'table',
244
373
  entityName: tableName,
245
374
  });
246
- const createsNewObject =
247
- (node.nodeKind === PostgresSchemaNodeKind.table && issue.reason === 'not-found') ||
248
- isEnablementCreationIssue(issue);
249
375
 
250
376
  return {
251
377
  namespaceId,
252
- ...ifDefined('explicitNodeControlPolicy', table?.control),
253
- table: tableName,
378
+ entityKind: 'table',
379
+ entityName: tableName,
254
380
  ...ifDefined('column', issueColumnName(issue)),
255
- createsNewObject,
381
+ ...ifDefined('explicitNodeControlPolicy', table?.control),
382
+ createsNewObject: resolvePostgresNodeIssueCreationFactoryName(issue) !== undefined,
256
383
  };
257
384
  }
@@ -3,8 +3,9 @@ import type { SqlSchemaDiffResult } from '@prisma-next/family-sql/control';
3
3
  import { buildNativeTypeExpander } from '@prisma-next/family-sql/control';
4
4
  import { classifyDiffSubjectGranularity } from '@prisma-next/family-sql/diff';
5
5
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
6
- import type { SchemaDiffIssue } from '@prisma-next/framework-components/control';
6
+ import type { DiffableNode, SchemaDiffIssue } from '@prisma-next/framework-components/control';
7
7
  import { diffSchemas } from '@prisma-next/framework-components/control';
8
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
8
9
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
9
10
  import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
10
11
  import { blindCast } from '@prisma-next/utils/casts';
@@ -19,6 +20,31 @@ import {
19
20
  import { contractToPostgresDatabaseSchemaNode } from './contract-to-postgres-database-schema-node';
20
21
  import { resolvePostgresNodeIssueControlPolicySubject } from './control-policy';
21
22
 
23
+ /**
24
+ * Whether a diff issue's subject node is cluster-scoped — it carries its own
25
+ * `namespaceId` field (only role and policy diff nodes do) and that
26
+ * coordinate is the unbound sentinel, meaning the node is not owned by any
27
+ * schema/namespace. A role node is always cluster-scoped (roles are
28
+ * cluster-level objects); a policy node's `namespaceId` is always its
29
+ * table's resolved DDL schema, never the sentinel. Namespace-ownership
30
+ * scoping (which compares `issue.path[1]` against the set of DDL schemas
31
+ * the contract owns) makes no sense for a cluster-scoped subject — its path
32
+ * segment at that index is the object's own name, not a schema name — so
33
+ * such an issue bypasses that scoping entirely.
34
+ */
35
+ function isClusterScopedIssue(issue: SchemaDiffIssue): boolean {
36
+ const node = issue.expected ?? issue.actual;
37
+ return node !== undefined && nodeNamespaceId(node) === UNBOUND_NAMESPACE_ID;
38
+ }
39
+
40
+ function nodeNamespaceId(node: DiffableNode): string | undefined {
41
+ if (!Object.hasOwn(node, 'namespaceId')) return undefined;
42
+ return blindCast<
43
+ { namespaceId: string },
44
+ 'presence of an own namespaceId field was just checked via Object.hasOwn'
45
+ >(node).namespaceId;
46
+ }
47
+
22
48
  function ownedSchemaNames(expected: PostgresDatabaseSchemaNode): ReadonlySet<string> {
23
49
  const policyNamespaces = Object.values(expected.namespaces).flatMap((ns) =>
24
50
  Object.values(ns.tables).flatMap((t) => t.policies.map((p) => p.namespaceId)),
@@ -53,10 +79,17 @@ function pruneTableLessNamespaces(
53
79
  }
54
80
 
55
81
  /**
56
- * Resolves a verdict-diff issue's subject table's declared control policy
57
- * directly from the contract, by delegating to the same node-typed resolver
82
+ * Resolves a verdict-diff issue's subject's declared control policy directly
83
+ * from the contract, by delegating to the same node-typed resolver
58
84
  * ({@link resolvePostgresNodeIssueControlPolicySubject}) the planner uses to
59
- * gate DDL calls. `undefined` when the issue resolves to no contract table.
85
+ * gate DDL calls. `undefined` when the issue resolves to no contract subject.
86
+ *
87
+ * A role issue resolves through that same resolver to `external`
88
+ * unconditionally (see its role branch), regardless of the contract's own
89
+ * default policy: a role is referenced by the contract but not owned, and
90
+ * `external`'s existing semantics — a missing declared subject still fails,
91
+ * every extra is suppressed — are exactly the wanted asymmetric grading for
92
+ * a cluster object the framework does not own.
60
93
  */
61
94
  function resolveControlPolicy(
62
95
  issue: SchemaDiffIssue,
@@ -75,8 +108,9 @@ function resolveControlPolicy(
75
108
  * the expected tree (resolved leaf values, expander threaded, FK schemas
76
109
  * resolved, table-less namespaces pruned), run the generic
77
110
  * differ over the trees as derived, and scope out `not-expected` findings under namespaces the
78
- * contract does not own. Ownership is role-aware, mirroring the legacy
79
- * decomposition: relational extras check the PRUNED owned set (the legacy
111
+ * contract does not own. Ownership scoping bypasses cluster-scoped subjects
112
+ * (roles today), mirroring the legacy decomposition: relational extras check
113
+ * the PRUNED owned set (the legacy
80
114
  * per-namespace walk never visited a table-less namespace, so its live
81
115
  * relational contents are invisible), while `structural` extras (RLS
82
116
  * policies) check the FULL owned set (the legacy policy diff governed
@@ -84,7 +118,7 @@ function resolveControlPolicy(
84
118
  * shrink because a namespace declares no tables). The codec `verifyType`
85
119
  * hooks run once per contract namespace with tables against that
86
120
  * namespace's paired actual node (the hooks read namespace-scoped state
87
- * such as `nativeEnumTypeNames`).
121
+ * such as `nativeEnums`).
88
122
  */
89
123
  export function diffPostgresSchema(input: {
90
124
  readonly contract: Contract<SqlStorage>;
@@ -107,9 +141,10 @@ export function diffPostgresSchema(input: {
107
141
  const structuralOwned = ownedSchemaNames(fullExpected);
108
142
  const issues = diffSchemas(expected, actual).filter((issue) => {
109
143
  if (issue.reason !== 'not-expected') return true;
144
+ if (isClusterScopedIssue(issue)) return true;
145
+ const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
110
146
  const namespaceSegment = issue.path[1];
111
147
  if (namespaceSegment === undefined) return true;
112
- const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
113
148
  const owned = granularity === 'structural' ? structuralOwned : relationalOwned;
114
149
  return owned.has(namespaceSegment);
115
150
  });
@@ -146,7 +181,6 @@ function padActualNamespaces(
146
181
  namespaces[schemaName] = new PostgresNamespaceSchemaNode({
147
182
  schemaName,
148
183
  tables: {},
149
- nativeEnumTypeNames: [],
150
184
  });
151
185
  padded = true;
152
186
  }
@@ -165,15 +199,15 @@ export interface PostgresPlanDiff {
165
199
  readonly expected: PostgresDatabaseSchemaNode;
166
200
  /** The live ("start") tree, padded with empty namespaces so a missing schema's tables pair. */
167
201
  readonly actual: PostgresDatabaseSchemaNode;
168
- /** The one node diff over the two trees: relational + policy drift, role-aware ownership filtered. */
202
+ /** The one node diff over the two trees: relational + policy drift, cluster-scope-aware ownership filtered. */
169
203
  readonly issues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[];
170
204
  }
171
205
 
172
206
  /**
173
207
  * The Postgres planner's diff input: the SAME tree-building
174
208
  * `diffPostgresSchema` uses (expander threaded, FK schemas resolved,
175
- * table-less namespaces pruned, role-aware ownership filter) plus actual
176
- * namespace padding (so a missing
209
+ * table-less namespaces pruned, cluster-scope-aware ownership filter) plus
210
+ * actual namespace padding (so a missing
177
211
  * schema's tables surface as `not-found` instead of a swallowed namespace
178
212
  * `not-found`). One differ drives both verify and plan; this is the
179
213
  * plan-side derivation. The single issue list covers tables / columns /
@@ -207,9 +241,10 @@ export function buildPostgresPlanDiff(input: {
207
241
  'both trees are PostgresDatabaseSchemaNodes, so every diff-issue node is a SqlSchemaDiffNode'
208
242
  >(diffSchemas(expected, paddedActual)).filter((issue) => {
209
243
  if (issue.reason !== 'not-expected') return true;
244
+ if (isClusterScopedIssue(issue)) return true;
245
+ const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
210
246
  const namespaceSegment = issue.path[1];
211
247
  if (namespaceSegment === undefined) return true;
212
- const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
213
248
  const owned = granularity === 'structural' ? structuralOwned : relationalOwned;
214
249
  return owned.has(namespaceSegment);
215
250
  });
@@ -45,6 +45,7 @@ import { ifDefined } from '@prisma-next/utils/defined';
45
45
  import type { Result } from '@prisma-next/utils/result';
46
46
  import { notOk, ok } from '@prisma-next/utils/result';
47
47
  import type { PostgresNamespaceSchemaNode } from '../schema-ir/postgres-namespace-schema-node';
48
+ import type { PostgresNativeEnumSchemaNode } from '../schema-ir/postgres-native-enum-schema-node';
48
49
  import type { PostgresTableSchemaNode } from '../schema-ir/postgres-table-schema-node';
49
50
  import { PostgresSchemaNodeKind } from '../schema-ir/schema-node-kinds';
50
51
  import { quoteIdentifier } from '../sql-utils';
@@ -61,6 +62,7 @@ import {
61
62
  AddUniqueCall,
62
63
  AlterColumnTypeCall,
63
64
  CreateIndexCall,
65
+ CreateNativeEnumTypeCall,
64
66
  CreateSchemaCall,
65
67
  CreateTableCall,
66
68
  DisableRowLevelSecurityCall,
@@ -69,6 +71,7 @@ import {
69
71
  DropConstraintCall,
70
72
  DropDefaultCall,
71
73
  DropIndexCall,
74
+ DropNativeEnumTypeCall,
72
75
  DropNotNullCall,
73
76
  DropTableCall,
74
77
  EnableRowLevelSecurityCall,
@@ -152,8 +155,10 @@ function classifyCall(call: PostgresOpFactoryCall): CallCategory {
152
155
  switch (call.factoryName) {
153
156
  case 'createExtension':
154
157
  case 'createSchema':
158
+ case 'createNativeEnumType':
155
159
  return 'dep';
156
160
  case 'dropTable':
161
+ case 'dropNativeEnumType':
157
162
  case 'dropColumn':
158
163
  case 'dropConstraint':
159
164
  case 'dropCheckConstraint':
@@ -250,17 +255,22 @@ function locationForCall(call: PostgresOpFactoryCall): SqlPlannerConflict['locat
250
255
  typeName?: string;
251
256
  };
252
257
  const location: {
253
- table?: string;
258
+ entityKind?: string;
259
+ entityName?: string;
254
260
  column?: string;
255
261
  index?: string;
256
262
  constraint?: string;
257
- type?: string;
258
263
  } = {};
259
- if (anyCall.tableName) location.table = anyCall.tableName;
264
+ if (anyCall.tableName) {
265
+ location.entityKind = 'table';
266
+ location.entityName = anyCall.tableName;
267
+ } else if (anyCall.typeName) {
268
+ location.entityKind = 'native_enum';
269
+ location.entityName = anyCall.typeName;
270
+ }
260
271
  if (anyCall.columnName) location.column = anyCall.columnName;
261
272
  if (anyCall.indexName) location.index = anyCall.indexName;
262
273
  if (anyCall.constraintName) location.constraint = anyCall.constraintName;
263
- if (anyCall.typeName) location.type = anyCall.typeName;
264
274
  return Object.keys(location).length > 0 ? (location as SqlPlannerConflictLocation) : undefined;
265
275
  }
266
276
 
@@ -374,6 +384,11 @@ export function nodeIssueOrder(issue: SchemaDiffIssue): number {
374
384
  switch (node.nodeKind) {
375
385
  case PostgresSchemaNodeKind.namespace:
376
386
  return 1;
387
+ case PostgresSchemaNodeKind.nativeEnum:
388
+ // Creates order right after namespace creates within the 'dep' bucket
389
+ // (CREATE SCHEMA before CREATE TYPE); drops order after table drops
390
+ // within the 'drop' bucket (DROP TYPE only after its dependents left).
391
+ return issue.reason === 'not-expected' ? 17 : 2;
377
392
  case RelationalSchemaNodeKind.foreignKey:
378
393
  return issue.reason === 'not-expected' ? 10 : 60;
379
394
  case RelationalSchemaNodeKind.unique:
@@ -527,6 +542,60 @@ function nodeConflict(kind: SqlPlannerConflict['kind'], message: string): SqlPla
527
542
  return issueConflict(kind, message);
528
543
  }
529
544
 
545
+ /**
546
+ * Managed native-enum issue -> op lowering. A missing declared type creates
547
+ * it; an unclaimed live type drops it (ownership-scoped upstream by
548
+ * `retainUnownedExtras`, destructiveness gated by the operation-class
549
+ * policy); a paired member-value mismatch is a NAMED unsupported diagnostic
550
+ * — never a silent no-op and never a drop-and-recreate. Slice B replaces the
551
+ * diagnostic with order-aware `ALTER TYPE ... ADD VALUE` / refusal semantics.
552
+ */
553
+ function mapNativeEnumNodeIssue(
554
+ issue: SchemaDiffIssue,
555
+ ctx: StrategyContext,
556
+ ): Result<readonly PostgresOpFactoryCall[], SqlPlannerConflict> {
557
+ const ddlSchemaName = issueSchemaName(issue);
558
+ if (ddlSchemaName === undefined) {
559
+ return notOk(
560
+ nodeConflict(
561
+ 'unsupportedOperation',
562
+ `Enum issue has no schema in its path: ${issue.path.join('/')}`,
563
+ ),
564
+ );
565
+ }
566
+ const schemaName = emissionSchemaName(ctx, ddlSchemaName);
567
+ if (issue.reason === 'not-found') {
568
+ const expected = blindCast<
569
+ PostgresNativeEnumSchemaNode,
570
+ 'a not-found native-enum issue always carries the expected PostgresNativeEnumSchemaNode'
571
+ >(issue.expected);
572
+ return ok([new CreateNativeEnumTypeCall(schemaName, expected.typeName, expected.members)]);
573
+ }
574
+ if (issue.reason === 'not-expected') {
575
+ const actual = blindCast<
576
+ PostgresNativeEnumSchemaNode,
577
+ 'a not-expected native-enum issue always carries the actual PostgresNativeEnumSchemaNode'
578
+ >(issue.actual);
579
+ return ok([new DropNativeEnumTypeCall(schemaName, actual.typeName)]);
580
+ }
581
+ const expected = blindCast<
582
+ PostgresNativeEnumSchemaNode,
583
+ 'a not-equal native-enum issue carries both sides; the expected node names the type'
584
+ >(issue.expected);
585
+ const actualMembers = blindCast<
586
+ PostgresNativeEnumSchemaNode,
587
+ 'a not-equal native-enum issue carries both sides'
588
+ >(issue.actual).members;
589
+ return notOk(
590
+ nodeConflict(
591
+ 'unsupportedOperation',
592
+ `Native enum type "${ddlSchemaName}"."${expected.typeName}" member values changed ` +
593
+ `(contract declares [${expected.members.join(', ')}], database has [${actualMembers.join(', ')}]); ` +
594
+ 'enum value changes are not auto-migrated yet. Author the change manually with `migration new`.',
595
+ ),
596
+ );
597
+ }
598
+
530
599
  function mapTableNodeIssue(
531
600
  issue: SchemaDiffIssue,
532
601
  schemaName: string,
@@ -814,6 +883,10 @@ export function mapNodeIssueToCall(
814
883
  return ok([new CreateSchemaCall(namespace.schemaName)]);
815
884
  }
816
885
 
886
+ if (node.nodeKind === PostgresSchemaNodeKind.nativeEnum) {
887
+ return mapNativeEnumNodeIssue(issue, ctx);
888
+ }
889
+
817
890
  const ddlSchemaName = issueSchemaName(issue);
818
891
  const tableName = issueTableName(issue);
819
892
  if (ddlSchemaName === undefined || tableName === undefined) {