@prisma-next/target-postgres 0.14.0-dev.61 → 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 (117) hide show
  1. package/dist/{authoring-BLkL4fFG.mjs → authoring-CdIZ0yvD.mjs} +2 -2
  2. package/dist/{authoring-BLkL4fFG.mjs.map → authoring-CdIZ0yvD.mjs.map} +1 -1
  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-BpmChlAK.mjs → control-policy-DKp0HKpY.mjs} +180 -45
  7. package/dist/control-policy-DKp0HKpY.mjs.map +1 -0
  8. package/dist/control.mjs +21 -25
  9. package/dist/control.mjs.map +1 -1
  10. package/dist/{ddl-BovUVFVi.mjs → ddl-gmxHf68z.mjs} +20 -4
  11. package/dist/{ddl-BovUVFVi.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-C1T4BXJ8.mjs → descriptor-meta-DEuXJfpR.mjs} +3 -3
  15. package/dist/{descriptor-meta-C1T4BXJ8.mjs.map → descriptor-meta-DEuXJfpR.mjs.map} +1 -1
  16. package/dist/{diff-database-schema-RMwnSOnz.mjs → diff-database-schema-D2sjvMw9.mjs} +14 -10
  17. package/dist/diff-database-schema-D2sjvMw9.mjs.map +1 -0
  18. package/dist/diff-database-schema.d.mts +1 -1
  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-CI00nS4z.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.mjs +1 -1
  37. package/dist/{planner-CAHZN1Ez.mjs → planner-BtEd5LMr.mjs} +32 -64
  38. package/dist/planner-BtEd5LMr.mjs.map +1 -0
  39. package/dist/{planner-ddl-builders-B8Nn6nHN.mjs → planner-ddl-builders-BKlmGDJl.mjs} +7 -3
  40. package/dist/planner-ddl-builders-BKlmGDJl.mjs.map +1 -0
  41. package/dist/planner-ddl-builders.d.mts.map +1 -1
  42. package/dist/planner-ddl-builders.mjs +1 -1
  43. package/dist/{planner-produced-postgres-migration-a-tjfKsA.d.mts → planner-produced-postgres-migration-CXue-6M9.d.mts} +2 -2
  44. package/dist/{planner-produced-postgres-migration-a-tjfKsA.d.mts.map → planner-produced-postgres-migration-CXue-6M9.d.mts.map} +1 -1
  45. package/dist/{planner-produced-postgres-migration-flkG4Y7n.mjs → planner-produced-postgres-migration-Jat3hg4u.mjs} +2 -2
  46. package/dist/{planner-produced-postgres-migration-flkG4Y7n.mjs.map → planner-produced-postgres-migration-Jat3hg4u.mjs.map} +1 -1
  47. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  48. package/dist/planner-produced-postgres-migration.mjs +1 -1
  49. package/dist/{planner-sql-checks-C1-Kk3Eu.mjs → planner-sql-checks-Br1c_QsH.mjs} +3 -3
  50. package/dist/{planner-sql-checks-C1-Kk3Eu.mjs.map → planner-sql-checks-Br1c_QsH.mjs.map} +1 -1
  51. package/dist/planner-sql-checks.mjs +1 -1
  52. package/dist/planner.d.mts +2 -2
  53. package/dist/planner.d.mts.map +1 -1
  54. package/dist/planner.mjs +2 -2
  55. package/dist/{postgres-contract-view-DWLNQbdR.mjs → postgres-contract-view-BAPW0ec1.mjs} +3 -3
  56. package/dist/{postgres-contract-view-DWLNQbdR.mjs.map → postgres-contract-view-BAPW0ec1.mjs.map} +1 -1
  57. package/dist/{postgres-database-schema-node-BVdOffjC.d.mts → postgres-database-schema-node-CJv5bfbs.d.mts} +62 -21
  58. package/dist/postgres-database-schema-node-CJv5bfbs.d.mts.map +1 -0
  59. package/dist/{postgres-migration-C92swCzd.mjs → postgres-migration-ChLMQRd-.mjs} +19 -3
  60. package/dist/postgres-migration-ChLMQRd-.mjs.map +1 -0
  61. package/dist/{postgres-migration-Bzzi0Asr.d.mts → postgres-migration-DkmgGNcv.d.mts} +21 -2
  62. package/dist/postgres-migration-DkmgGNcv.d.mts.map +1 -0
  63. package/dist/{postgres-role-schema-node-BmCboEa_.mjs → postgres-role-schema-node-Cm-j0ZGM.mjs} +11 -19
  64. package/dist/postgres-role-schema-node-Cm-j0ZGM.mjs.map +1 -0
  65. package/dist/{postgres-schema-COEca21H.mjs → postgres-schema-DWQ0-CPb.mjs} +13 -2
  66. package/dist/postgres-schema-DWQ0-CPb.mjs.map +1 -0
  67. package/dist/postgres-schema-DsrtYp2v.d.mts.map +1 -1
  68. package/dist/{postgres-table-schema-node-9FCtCUz2.mjs → postgres-table-schema-node-D6LvInCe.mjs} +71 -8
  69. package/dist/postgres-table-schema-node-D6LvInCe.mjs.map +1 -0
  70. package/dist/{resolve-ddl-schema-ZkKxt2Fm.mjs → resolve-ddl-schema-CAza65Iy.mjs} +2 -2
  71. package/dist/{resolve-ddl-schema-ZkKxt2Fm.mjs.map → resolve-ddl-schema-CAza65Iy.mjs.map} +1 -1
  72. package/dist/runtime.mjs +1 -1
  73. package/dist/schema-ir-annotations.mjs +1 -1
  74. package/dist/{sql-utils-DcfMz4MQ.mjs → sql-utils-SU4FDvIV.mjs} +12 -3
  75. package/dist/{sql-utils-DcfMz4MQ.mjs.map → sql-utils-SU4FDvIV.mjs.map} +1 -1
  76. package/dist/sql-utils.d.mts +8 -1
  77. package/dist/sql-utils.d.mts.map +1 -1
  78. package/dist/sql-utils.mjs +2 -2
  79. package/dist/types.d.mts +2 -2
  80. package/dist/types.mjs +4 -4
  81. package/package.json +20 -20
  82. package/src/contract-free/ddl.ts +27 -0
  83. package/src/core/ddl/nodes.ts +44 -0
  84. package/src/core/migrations/contract-to-postgres-database-schema-node.ts +12 -1
  85. package/src/core/migrations/control-policy.ts +143 -45
  86. package/src/core/migrations/diff-database-schema.ts +1 -2
  87. package/src/core/migrations/issue-planner.ts +77 -4
  88. package/src/core/migrations/op-factory-call.ts +84 -0
  89. package/src/core/migrations/operations/native-enum-types.ts +62 -0
  90. package/src/core/migrations/planner-ddl-builders.ts +12 -2
  91. package/src/core/migrations/planner.ts +25 -62
  92. package/src/core/migrations/postgres-migration.ts +31 -0
  93. package/src/core/migrations/verify-postgres-namespaces.ts +0 -1
  94. package/src/core/postgres-schema.ts +22 -1
  95. package/src/core/psl-infer/infer-psl-contract.ts +6 -16
  96. package/src/core/schema-ir/node-storage-coordinate.ts +26 -0
  97. package/src/core/schema-ir/postgres-namespace-schema-node.ts +18 -27
  98. package/src/core/schema-ir/postgres-native-enum-schema-node.ts +83 -0
  99. package/src/core/schema-ir/schema-node-kinds.ts +12 -3
  100. package/src/core/sql-utils.ts +15 -2
  101. package/src/exports/ddl.ts +2 -0
  102. package/src/exports/op-factory-call.ts +2 -0
  103. package/src/exports/sql-utils.ts +1 -0
  104. package/src/exports/types.ts +4 -1
  105. package/dist/control-policy-BpmChlAK.mjs.map +0 -1
  106. package/dist/diff-database-schema-RMwnSOnz.mjs.map +0 -1
  107. package/dist/nodes-B815pj_X.mjs.map +0 -1
  108. package/dist/nodes-BtQyZha6.d.mts.map +0 -1
  109. package/dist/op-factory-call-CI00nS4z.mjs.map +0 -1
  110. package/dist/planner-CAHZN1Ez.mjs.map +0 -1
  111. package/dist/planner-ddl-builders-B8Nn6nHN.mjs.map +0 -1
  112. package/dist/postgres-database-schema-node-BVdOffjC.d.mts.map +0 -1
  113. package/dist/postgres-migration-Bzzi0Asr.d.mts.map +0 -1
  114. package/dist/postgres-migration-C92swCzd.mjs.map +0 -1
  115. package/dist/postgres-role-schema-node-BmCboEa_.mjs.map +0 -1
  116. package/dist/postgres-schema-COEca21H.mjs.map +0 -1
  117. package/dist/postgres-table-schema-node-9FCtCUz2.mjs.map +0 -1
@@ -1,5 +1,9 @@
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';
@@ -7,6 +11,7 @@ import { blindCast } from '@prisma-next/utils/casts';
7
11
  import { ifDefined } from '@prisma-next/utils/defined';
8
12
  import { type PostgresRole, ROLE_DEFAULT_CONTROL_POLICY } from '../postgres-role';
9
13
  import { isPostgresSchema } from '../postgres-schema';
14
+ import { postgresNodeStorageCoordinate } from '../schema-ir/node-storage-coordinate';
10
15
  import type { PostgresNamespaceSchemaNode } from '../schema-ir/postgres-namespace-schema-node';
11
16
  import { PostgresTableSchemaNode } from '../schema-ir/postgres-table-schema-node';
12
17
  import type { SqlSchemaDiffNode } from '../schema-ir/schema-node-kinds';
@@ -24,13 +29,16 @@ import type { PostgresOpFactoryCall } from './op-factory-call';
24
29
  * and is fail-closed. Any call not listed here — including future or
25
30
  * extension-contributed factories — is treated as NOT object-creation, so it
26
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.
27
40
  */
28
- const OBJECT_CREATION_FACTORIES: ReadonlySet<string> = new Set<string>([
29
- 'createTable',
30
- 'createSchema',
31
- 'createRlsPolicy',
32
- 'enableRowLevelSecurity',
33
- ]);
41
+ const OBJECT_CREATION_FACTORIES: ReadonlySet<string> = new Set<string>(['createRlsPolicy']);
34
42
 
35
43
  function createsNewTopLevelObject(call: PostgresOpFactoryCall): boolean {
36
44
  return OBJECT_CREATION_FACTORIES.has(call.factoryName);
@@ -41,18 +49,25 @@ function ddlSchemaNameForNamespace(contract: Contract<SqlStorage>, namespaceId:
41
49
  return isPostgresSchema(namespace) ? namespace.ddlSchemaName(contract.storage) : namespaceId;
42
50
  }
43
51
 
44
- 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(
45
64
  contract: Contract<SqlStorage>,
46
- tableName: string,
65
+ coordinate: { readonly entityKind: string; readonly entityName: string },
47
66
  ddlSchemaName: string | undefined,
48
67
  ): string {
49
68
  for (const namespaceId of Object.keys(contract.storage.namespaces)) {
50
- const table = entityAt<StorageTable>(contract.storage, {
51
- namespaceId,
52
- entityKind: 'table',
53
- entityName: tableName,
54
- });
55
- if (!table) continue;
69
+ const entity = entityAt(contract.storage, { namespaceId, ...coordinate });
70
+ if (!entity) continue;
56
71
  if (
57
72
  ddlSchemaName === undefined ||
58
73
  ddlSchemaNameForNamespace(contract, namespaceId) === ddlSchemaName
@@ -63,6 +78,18 @@ function resolveNamespaceIdForTable(
63
78
  return UNBOUND_NAMESPACE_ID;
64
79
  }
65
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
+
66
93
  export function resolveNamespaceIdForDdlSchema(
67
94
  contract: Contract<SqlStorage>,
68
95
  ddlSchemaName: string,
@@ -93,16 +120,60 @@ function postgresCallFields(call: PostgresOpFactoryCall): PostgresCallFields {
93
120
  };
94
121
  }
95
122
 
96
- export function formatPostgresControlPolicySubjectLabel(
97
- factoryName: string,
123
+ function formatSuppressionSubjectLabel(
98
124
  subject: ControlPolicySubject | undefined,
99
125
  contract: Contract<SqlStorage>,
100
126
  ): string {
101
- if (subject?.table) {
102
- const ddlSchema = ddlSchemaNameForNamespace(contract, subject.namespaceId);
103
- 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}"`;
104
131
  }
105
- return factoryName;
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'`;
144
+ }
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
+ };
106
177
  }
107
178
 
108
179
  export function resolvePostgresCallControlPolicySubject(
@@ -132,9 +203,10 @@ export function resolvePostgresCallControlPolicySubject(
132
203
  })?.control;
133
204
  return {
134
205
  namespaceId,
135
- ...ifDefined('explicitNodeControlPolicy', tableControlPolicy),
136
- table: callFields.tableName,
206
+ entityKind: 'table',
207
+ entityName: callFields.tableName,
137
208
  ...ifDefined('column', callFields.columnName),
209
+ ...ifDefined('explicitNodeControlPolicy', tableControlPolicy),
138
210
  createsNewObject,
139
211
  };
140
212
  }
@@ -150,10 +222,10 @@ export function resolvePostgresCallControlPolicySubject(
150
222
  }
151
223
 
152
224
  /**
153
- * Node kinds that describe the absence of a whole, top-level Postgres
154
- * object the same objects `createsNewTopLevelObject` recognises for calls.
155
- * Used by {@link resolvePostgresNodeIssueCreationFactoryName} to decide
156
- * 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
157
229
  * (create-if-absent) and to seed the suppressed-subject warning's
158
230
  * `factoryName` when the planner is skipped. RLS policy creation is not
159
231
  * listed here — policy issues never reach this issue-based partition (they
@@ -163,13 +235,15 @@ export function resolvePostgresCallControlPolicySubject(
163
235
  const POSTGRES_NODE_CREATION_FACTORY: Readonly<Record<string, string>> = Object.freeze({
164
236
  [PostgresSchemaNodeKind.namespace]: 'createSchema',
165
237
  [PostgresSchemaNodeKind.table]: 'createTable',
238
+ [PostgresSchemaNodeKind.nativeEnum]: 'createNativeEnumType',
166
239
  });
167
240
 
168
241
  /**
169
242
  * A table `not-equal` issue whose `rlsEnabled` flips OFF→ON (expected on,
170
- * actual off) is enablement toward `ENABLE ROW LEVEL SECURITY`
171
- * creation-class per `OBJECT_CREATION_FACTORIES` ('enableRowLevelSecurity'
172
- * 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
173
247
  * policy set attaches to, the same grant `tolerated` extends to creating the
174
248
  * policies themselves. The opposite direction (`DISABLE`) is a modification
175
249
  * and stays managed-only. Keying on the actual delta (not just the expected
@@ -207,15 +281,25 @@ export function resolvePostgresNodeIssueCreationFactoryName(
207
281
  * {@link SchemaDiffIssue}. Mirrors the resolution
208
282
  * `resolvePostgresCallControlPolicySubject` performs for a generated DDL
209
283
  * call, but works *off the issue* — so the planner can partition issues by
210
- * effective policy before the diff engine runs. `createsNewObject` is
211
- * derived from the node kind + reason: a `not-found` schema/table describes
212
- * a brand-new top-level object; everything else touches an existing object.
284
+ * effective policy before the diff engine runs.
285
+ *
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`).
213
297
  *
214
- * A `not-expected` table carries no contract namespace coordinate (the live
215
- * table isn't claimed by any contract namespace), so the subject's
216
- * `namespaceId` falls back to {@link UNBOUND_NAMESPACE_ID} via
217
- * `resolveNamespaceIdForTable`'s own fallback the call-side resolver does
218
- * the same for the `DropTableCall` it produces.
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.
219
303
  *
220
304
  * A role node resolves against the `PostgresRole` contract entity at that
221
305
  * role's name (roles are root-level diff subjects, so the name is the
@@ -263,6 +347,22 @@ export function resolvePostgresNodeIssueControlPolicySubject(
263
347
  };
264
348
  }
265
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
+
266
366
  const tableName = issue.path[2];
267
367
  if (tableName === undefined) return undefined;
268
368
  const ddlSchemaName = issue.path[1];
@@ -272,15 +372,13 @@ export function resolvePostgresNodeIssueControlPolicySubject(
272
372
  entityKind: 'table',
273
373
  entityName: tableName,
274
374
  });
275
- const createsNewObject =
276
- (node.nodeKind === PostgresSchemaNodeKind.table && issue.reason === 'not-found') ||
277
- isEnablementCreationIssue(issue);
278
375
 
279
376
  return {
280
377
  namespaceId,
281
- ...ifDefined('explicitNodeControlPolicy', table?.control),
282
- table: tableName,
378
+ entityKind: 'table',
379
+ entityName: tableName,
283
380
  ...ifDefined('column', issueColumnName(issue)),
284
- createsNewObject,
381
+ ...ifDefined('explicitNodeControlPolicy', table?.control),
382
+ createsNewObject: resolvePostgresNodeIssueCreationFactoryName(issue) !== undefined,
285
383
  };
286
384
  }
@@ -118,7 +118,7 @@ function resolveControlPolicy(
118
118
  * shrink because a namespace declares no tables). The codec `verifyType`
119
119
  * hooks run once per contract namespace with tables against that
120
120
  * namespace's paired actual node (the hooks read namespace-scoped state
121
- * such as `nativeEnumTypeNames`).
121
+ * such as `nativeEnums`).
122
122
  */
123
123
  export function diffPostgresSchema(input: {
124
124
  readonly contract: Contract<SqlStorage>;
@@ -181,7 +181,6 @@ function padActualNamespaces(
181
181
  namespaces[schemaName] = new PostgresNamespaceSchemaNode({
182
182
  schemaName,
183
183
  tables: {},
184
- nativeEnumTypeNames: [],
185
184
  });
186
185
  padded = true;
187
186
  }
@@ -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) {
@@ -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