@prisma-next/target-postgres 0.14.0-dev.44 → 0.14.0-dev.46

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 (116) hide show
  1. package/dist/authoring-DzHOdWTl.mjs +364 -0
  2. package/dist/authoring-DzHOdWTl.mjs.map +1 -0
  3. package/dist/{codec-ids-CnXu9Qy3.d.mts → codec-ids-BzrFF-I4.d.mts} +3 -2
  4. package/dist/codec-ids-BzrFF-I4.d.mts.map +1 -0
  5. package/dist/{codec-ids-BvytN2P8.mjs → codec-ids-C_-Hj6bL.mjs} +3 -2
  6. package/dist/codec-ids-C_-Hj6bL.mjs.map +1 -0
  7. package/dist/codec-ids.d.mts +2 -2
  8. package/dist/codec-ids.mjs +2 -2
  9. package/dist/{codec-types-CueCoGD0.d.mts → codec-types-BDVmvKAW.d.mts} +3 -2
  10. package/dist/codec-types-BDVmvKAW.d.mts.map +1 -0
  11. package/dist/codec-types.d.mts +1 -1
  12. package/dist/{codecs-Ca8upop6.d.mts → codecs-Dk2sxGLv.d.mts} +66 -4
  13. package/dist/codecs-Dk2sxGLv.d.mts.map +1 -0
  14. package/dist/codecs.d.mts +1 -1
  15. package/dist/codecs.mjs +83 -1
  16. package/dist/codecs.mjs.map +1 -1
  17. package/dist/contract-free.mjs +3 -3
  18. package/dist/control.d.mts +1 -1
  19. package/dist/control.mjs +138 -13
  20. package/dist/control.mjs.map +1 -1
  21. package/dist/{data-transform-qKy1U5V9.d.mts → data-transform-DPzwBMhW.d.mts} +2 -2
  22. package/dist/{data-transform-qKy1U5V9.d.mts.map → data-transform-DPzwBMhW.d.mts.map} +1 -1
  23. package/dist/data-transform.d.mts +1 -1
  24. package/dist/{ddl-BmLIe1z_.mjs → ddl-ujxH2A7r.mjs} +3 -3
  25. package/dist/{ddl-BmLIe1z_.mjs.map → ddl-ujxH2A7r.mjs.map} +1 -1
  26. package/dist/{descriptor-meta-tjatyhtZ.mjs → descriptor-meta-CuGDsrG4.mjs} +5 -4
  27. package/dist/descriptor-meta-CuGDsrG4.mjs.map +1 -0
  28. package/dist/{issue-planner-DbFvaAgu.mjs → issue-planner-Cbh-xTEr.mjs} +6 -6
  29. package/dist/{issue-planner-DbFvaAgu.mjs.map → issue-planner-Cbh-xTEr.mjs.map} +1 -1
  30. package/dist/issue-planner.d.mts +2 -2
  31. package/dist/issue-planner.mjs +1 -1
  32. package/dist/migration.d.mts +3 -3
  33. package/dist/migration.d.mts.map +1 -1
  34. package/dist/migration.mjs +2 -2
  35. package/dist/namespace-ids-xtp_ZY86.mjs +7 -0
  36. package/dist/namespace-ids-xtp_ZY86.mjs.map +1 -0
  37. package/dist/{op-factory-call-t65c4jyY.mjs → op-factory-call-BuoGT5UI.mjs} +6 -5
  38. package/dist/op-factory-call-BuoGT5UI.mjs.map +1 -0
  39. package/dist/{op-factory-call-CJWUxyUW.d.mts → op-factory-call-Caqus-Qg.d.mts} +2 -2
  40. package/dist/{op-factory-call-CJWUxyUW.d.mts.map → op-factory-call-Caqus-Qg.d.mts.map} +1 -1
  41. package/dist/op-factory-call.d.mts +1 -1
  42. package/dist/op-factory-call.mjs +1 -1
  43. package/dist/pack.d.mts +43 -3
  44. package/dist/pack.d.mts.map +1 -1
  45. package/dist/pack.mjs +1 -1
  46. package/dist/{planner-CG2pLHk-.mjs → planner-CMXuFrQB.mjs} +10 -9
  47. package/dist/planner-CMXuFrQB.mjs.map +1 -0
  48. package/dist/planner-ddl-builders.d.mts +1 -1
  49. package/dist/planner-identity-values.d.mts +1 -1
  50. package/dist/{planner-produced-postgres-migration-BolD0Mvc.mjs → planner-produced-postgres-migration-CikV-edi.mjs} +2 -2
  51. package/dist/{planner-produced-postgres-migration-BolD0Mvc.mjs.map → planner-produced-postgres-migration-CikV-edi.mjs.map} +1 -1
  52. package/dist/{planner-produced-postgres-migration-Cz9--XOT.d.mts → planner-produced-postgres-migration-CtJi4sgS.d.mts} +2 -2
  53. package/dist/{planner-produced-postgres-migration-Cz9--XOT.d.mts.map → planner-produced-postgres-migration-CtJi4sgS.d.mts.map} +1 -1
  54. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  55. package/dist/planner-produced-postgres-migration.mjs +1 -1
  56. package/dist/{planner-sql-checks-DE9c4519.mjs → planner-sql-checks-Cze3vMfe.mjs} +2 -2
  57. package/dist/{planner-sql-checks-DE9c4519.mjs.map → planner-sql-checks-Cze3vMfe.mjs.map} +1 -1
  58. package/dist/planner-sql-checks.d.mts +1 -1
  59. package/dist/planner-sql-checks.mjs +1 -1
  60. package/dist/planner.d.mts +3 -3
  61. package/dist/planner.mjs +1 -1
  62. package/dist/{postgres-contract-view-BegduLr6.mjs → postgres-contract-view-V3rGJCoe.mjs} +23 -14
  63. package/dist/postgres-contract-view-V3rGJCoe.mjs.map +1 -0
  64. package/dist/{postgres-migration-BfMmK6tv.mjs → postgres-migration-CZApTUCK.mjs} +3 -3
  65. package/dist/{postgres-migration-BfMmK6tv.mjs.map → postgres-migration-CZApTUCK.mjs.map} +1 -1
  66. package/dist/{postgres-migration-D7kNVWVG.d.mts → postgres-migration-CfyKDT7b.d.mts} +4 -4
  67. package/dist/{postgres-migration-D7kNVWVG.d.mts.map → postgres-migration-CfyKDT7b.d.mts.map} +1 -1
  68. package/dist/postgres-native-enum-xdiqmBf_.mjs +41 -0
  69. package/dist/postgres-native-enum-xdiqmBf_.mjs.map +1 -0
  70. package/dist/{postgres-schema-BLIHBEfX.mjs → postgres-schema-CajYAwny.mjs} +32 -7
  71. package/dist/postgres-schema-CajYAwny.mjs.map +1 -0
  72. package/dist/{postgres-schema-BywoeKlt.d.mts → postgres-schema-DqnhQRvR.d.mts} +40 -3
  73. package/dist/postgres-schema-DqnhQRvR.d.mts.map +1 -0
  74. package/dist/{postgres-table-schema-node-1nNc8XDT.mjs → postgres-table-schema-node-Clei_xel.mjs} +2 -2
  75. package/dist/{postgres-table-schema-node-1nNc8XDT.mjs.map → postgres-table-schema-node-Clei_xel.mjs.map} +1 -1
  76. package/dist/{resolve-ddl-schema-CL1RR1mx.mjs → resolve-ddl-schema-Bv1ZvjX8.mjs} +8 -6
  77. package/dist/resolve-ddl-schema-Bv1ZvjX8.mjs.map +1 -0
  78. package/dist/runtime.d.mts +9 -0
  79. package/dist/runtime.d.mts.map +1 -1
  80. package/dist/runtime.mjs +1 -1
  81. package/dist/schema-ir-annotations.d.mts +4 -3
  82. package/dist/schema-ir-annotations.d.mts.map +1 -1
  83. package/dist/schema-ir-annotations.mjs +1 -1
  84. package/dist/types.d.mts +1 -1
  85. package/dist/types.mjs +2 -2
  86. package/package.json +20 -20
  87. package/src/core/authoring.ts +173 -2
  88. package/src/core/codec-ids.ts +1 -0
  89. package/src/core/codec-type-map.ts +2 -0
  90. package/src/core/codecs.ts +106 -0
  91. package/src/core/descriptor-meta.ts +2 -1
  92. package/src/core/entity-kinds.ts +15 -1
  93. package/src/core/migrations/operations/dependencies.ts +2 -1
  94. package/src/core/migrations/resolve-ddl-schema.ts +6 -4
  95. package/src/core/migrations/verify-postgres-namespaces.ts +2 -1
  96. package/src/core/namespace-ids.ts +3 -0
  97. package/src/core/postgres-contract-serializer.ts +27 -21
  98. package/src/core/postgres-native-enum.ts +51 -0
  99. package/src/core/postgres-schema.ts +28 -4
  100. package/src/core/postgres-validators.ts +7 -0
  101. package/src/core/psl-infer/infer-psl-contract.ts +246 -7
  102. package/src/exports/control.ts +2 -2
  103. package/dist/authoring-BX1WHIdN.mjs +0 -220
  104. package/dist/authoring-BX1WHIdN.mjs.map +0 -1
  105. package/dist/codec-ids-BvytN2P8.mjs.map +0 -1
  106. package/dist/codec-ids-CnXu9Qy3.d.mts.map +0 -1
  107. package/dist/codec-types-CueCoGD0.d.mts.map +0 -1
  108. package/dist/codecs-Ca8upop6.d.mts.map +0 -1
  109. package/dist/descriptor-meta-tjatyhtZ.mjs.map +0 -1
  110. package/dist/op-factory-call-t65c4jyY.mjs.map +0 -1
  111. package/dist/planner-CG2pLHk-.mjs.map +0 -1
  112. package/dist/postgres-contract-view-BegduLr6.mjs.map +0 -1
  113. package/dist/postgres-schema-BLIHBEfX.mjs.map +0 -1
  114. package/dist/postgres-schema-BywoeKlt.d.mts.map +0 -1
  115. package/dist/resolve-ddl-schema-CL1RR1mx.mjs.map +0 -1
  116. package/src/core/postgres-enum-type-schema.ts +0 -17
@@ -1,18 +1,44 @@
1
1
  import { temporalAuthoringPresets } from '@prisma-next/family-sql/control';
2
2
  import type {
3
3
  AuthoringEntityContext,
4
+ AuthoringEntityTypeFactoryOutput,
4
5
  AuthoringEntityTypeNamespace,
5
6
  AuthoringFieldNamespace,
6
7
  AuthoringPslBlockDescriptorNamespace,
7
8
  AuthoringTypeNamespace,
8
9
  PslExtensionBlock,
9
10
  } from '@prisma-next/framework-components/authoring';
11
+ import type { SqlValueSetDerivingEntityTypeOutput } from '@prisma-next/sql-contract/value-set-derivation-hook';
12
+ import { PG_ENUM_CODEC_ID } from './codec-ids';
13
+ import { PostgresNativeEnum } from './postgres-native-enum';
10
14
  import { PostgresRlsPolicy } from './postgres-rls-policy';
11
15
  import { PostgresRole, type PostgresRoleInput } from './postgres-role';
12
- import { PostgresRlsPolicySchema, PostgresRoleSchema } from './postgres-validators';
16
+ import {
17
+ PostgresNativeEnumSchema,
18
+ PostgresRlsPolicySchema,
19
+ PostgresRoleSchema,
20
+ } from './postgres-validators';
13
21
  import { computeContentHash, normalizePredicate } from './rls/canonicalize';
14
22
 
15
- export const postgresAuthoringTypes = {} as const satisfies AuthoringTypeNamespace;
23
+ /**
24
+ * `pg.enum(<ref>)` registers as an ordinary type constructor whose sole
25
+ * positional argument names a `native_enum` entity instead of carrying a
26
+ * literal value. The interpreter resolves the ref to the `native_enum`
27
+ * entity generically (driven by `entityRefArg`); the `pg/enum@1` codec
28
+ * descriptor's `columnFromEntity` hook (see `codecs.ts`) converts that
29
+ * entity into the column's `typeParams` and native type.
30
+ */
31
+ export const postgresAuthoringTypes = {
32
+ pg: {
33
+ enum: {
34
+ kind: 'typeConstructor',
35
+ entityRefArg: { index: 0, entityKind: 'native_enum' },
36
+ output: {
37
+ codecId: PG_ENUM_CODEC_ID,
38
+ },
39
+ },
40
+ },
41
+ } as const satisfies AuthoringTypeNamespace;
16
42
 
17
43
  export interface RlsPolicyExtensionBlock extends PslExtensionBlock {
18
44
  readonly namespaceId: string;
@@ -71,6 +97,127 @@ function lowerRlsPolicyFromBlock(
71
97
  });
72
98
  }
73
99
 
100
+ /**
101
+ * Lowers a `native_enum { memberName = "value" … @@map("type_name") }` block
102
+ * into a {@link PostgresNativeEnum}. Members must be authored as explicit
103
+ * `key = "value"` pairs — a bare (value-less) member is a diagnostic, not
104
+ * accepted (authoring-design.md §2.1). The parsed `memberName` is only used
105
+ * to duplicate-check and report diagnostics; the lowered entity carries just
106
+ * the member values (a native enum is value-only — the member "name" isn't
107
+ * a separate authoring concept from the value). `typeName` comes from
108
+ * `@@map` or defaults to the block name verbatim.
109
+ */
110
+ function lowerNativeEnumFromBlock(
111
+ block: PslExtensionBlock,
112
+ ctx: AuthoringEntityContext,
113
+ ): PostgresNativeEnum | undefined {
114
+ const sourceId = ctx.sourceId ?? 'unknown';
115
+ const diagnostics = ctx.diagnostics;
116
+
117
+ const mapAttr = block.blockAttributes.find((a) => a.name === 'map');
118
+ let typeName = block.name;
119
+ if (mapAttr) {
120
+ const rawArg = mapAttr.args[0]?.value;
121
+ const mapped = rawArg !== undefined ? unwrapQuotedString(rawArg) : undefined;
122
+ if (mapped === undefined) {
123
+ diagnostics?.push({
124
+ code: 'PSL_NATIVE_ENUM_INVALID_MAP',
125
+ message: `native_enum "${block.name}" @@map attribute must have a quoted type-name argument`,
126
+ sourceId,
127
+ span: mapAttr.span,
128
+ });
129
+ return undefined;
130
+ }
131
+ typeName = mapped;
132
+ }
133
+
134
+ let memberError = false;
135
+ const seenValues = new Set<string>();
136
+ const members: string[] = [];
137
+ for (const [memberName, paramValue] of Object.entries(block.parameters)) {
138
+ if (paramValue.kind === 'bare') {
139
+ diagnostics?.push({
140
+ code: 'PSL_NATIVE_ENUM_BARE_MEMBER',
141
+ message: `native_enum "${block.name}" member "${memberName}" has no value; members must be authored as "${memberName} = \\"value\\""`,
142
+ sourceId,
143
+ span: paramValue.span,
144
+ });
145
+ memberError = true;
146
+ continue;
147
+ }
148
+ if (paramValue.kind !== 'value') continue;
149
+
150
+ let jsonValue: unknown;
151
+ try {
152
+ jsonValue = JSON.parse(paramValue.raw);
153
+ } catch {
154
+ diagnostics?.push({
155
+ code: 'PSL_EXTENSION_INVALID_VALUE',
156
+ message: `native_enum "${block.name}" member "${memberName}" value "${paramValue.raw}" is not valid JSON`,
157
+ sourceId,
158
+ span: paramValue.span,
159
+ });
160
+ memberError = true;
161
+ continue;
162
+ }
163
+ if (typeof jsonValue !== 'string') {
164
+ diagnostics?.push({
165
+ code: 'PSL_EXTENSION_INVALID_VALUE',
166
+ message: `native_enum "${block.name}" member "${memberName}" value must be a string`,
167
+ sourceId,
168
+ span: paramValue.span,
169
+ });
170
+ memberError = true;
171
+ continue;
172
+ }
173
+ if (seenValues.has(jsonValue)) {
174
+ diagnostics?.push({
175
+ code: 'PSL_NATIVE_ENUM_DUPLICATE_MEMBER_VALUE',
176
+ message: `native_enum "${block.name}": duplicate member value "${jsonValue}"`,
177
+ sourceId,
178
+ span: paramValue.span,
179
+ });
180
+ memberError = true;
181
+ continue;
182
+ }
183
+ seenValues.add(jsonValue);
184
+ members.push(jsonValue);
185
+ }
186
+
187
+ if (memberError) return undefined;
188
+
189
+ if (members.length === 0) {
190
+ diagnostics?.push({
191
+ code: 'PSL_NATIVE_ENUM_MISSING_MEMBERS',
192
+ message: `native_enum "${block.name}" must have at least one member`,
193
+ sourceId,
194
+ span: block.span,
195
+ });
196
+ return undefined;
197
+ }
198
+
199
+ // `control` stays unset — the effective grade is resolved at read time via `effectiveControlPolicy`, like `StorageTable`/`StorageColumn`.
200
+ return new PostgresNativeEnum({ typeName, members });
201
+ }
202
+
203
+ /**
204
+ * `native_enum`'s entity-type factory output, checked separately from the assembled
205
+ * `postgresAuthoringEntityTypes` map below: `deriveValueSet` is SQL-family surface
206
+ * ({@link SqlValueSetDerivingEntityTypeOutput}), not part of the framework
207
+ * `AuthoringEntityTypeFactoryOutput` shape, so folding it directly into the map's single
208
+ * `satisfies AuthoringEntityTypeNamespace` check would trip an excess-property error. Checking it
209
+ * here against the intersection of both shapes keeps it structurally valid against each without
210
+ * widening the map's own check.
211
+ */
212
+ const nativeEnumEntityTypeOutput = {
213
+ factory: lowerNativeEnumFromBlock,
214
+ deriveValueSet: (entity: PostgresNativeEnum) => ({
215
+ kind: 'valueSet' as const,
216
+ values: [...entity.members],
217
+ }),
218
+ } satisfies AuthoringEntityTypeFactoryOutput<PslExtensionBlock, PostgresNativeEnum | undefined> &
219
+ SqlValueSetDerivingEntityTypeOutput;
220
+
74
221
  export const postgresAuthoringEntityTypes = {
75
222
  role: {
76
223
  kind: 'entity',
@@ -88,6 +235,12 @@ export const postgresAuthoringEntityTypes = {
88
235
  factory: lowerRlsPolicyFromBlock,
89
236
  },
90
237
  },
238
+ native_enum: {
239
+ kind: 'entity',
240
+ discriminator: 'native_enum',
241
+ validatorSchema: PostgresNativeEnumSchema,
242
+ output: nativeEnumEntityTypeOutput,
243
+ },
91
244
  } as const satisfies AuthoringEntityTypeNamespace;
92
245
 
93
246
  /**
@@ -134,6 +287,24 @@ export const postgresAuthoringPslBlockDescriptors = {
134
287
  using: { kind: 'value', codecId: 'pg/text@1', required: true },
135
288
  },
136
289
  },
290
+ /**
291
+ * PSL block descriptor for `native_enum`.
292
+ *
293
+ * Reuses the existing variadic-block mechanism (the same shape the SQL
294
+ * family's `enum` block ships): the body is an open `memberName = "value"`
295
+ * list. `variadicParameters: true` opens the block to arbitrary keys
296
+ * beyond the declared (empty) `parameters` set — the lowering factory
297
+ * (`lowerNativeEnumFromBlock`) turns the variadic entries into ordered
298
+ * members and rejects a bare (value-less) member.
299
+ */
300
+ native_enum: {
301
+ kind: 'pslBlock',
302
+ keyword: 'native_enum',
303
+ discriminator: 'native_enum',
304
+ name: { required: true },
305
+ parameters: {},
306
+ variadicParameters: true,
307
+ },
137
308
  } as const satisfies AuthoringPslBlockDescriptorNamespace;
138
309
 
139
310
  export const postgresAuthoringFieldPresets = {
@@ -8,6 +8,7 @@ export {
8
8
  } from '@prisma-next/sql-relational-core/ast';
9
9
  export const PG_TEXT_CODEC_ID = 'pg/text@1' as const;
10
10
  export const PG_TEXT_ARRAY_CODEC_ID = 'pg/text-array@1' as const;
11
+ export const PG_ENUM_CODEC_ID = 'pg/enum@1' as const;
11
12
  export const PG_CHAR_CODEC_ID = 'pg/char@1' as const;
12
13
  export const PG_VARCHAR_CODEC_ID = 'pg/varchar@1' as const;
13
14
  export const PG_INT_CODEC_ID = 'pg/int@1' as const;
@@ -21,6 +21,7 @@ import {
21
21
  pgBoolDescriptor,
22
22
  pgByteaDescriptor,
23
23
  pgCharDescriptor,
24
+ pgEnumDescriptor,
24
25
  pgFloat4Descriptor,
25
26
  pgFloat8Descriptor,
26
27
  pgFloatDescriptor,
@@ -50,6 +51,7 @@ export const codecDescriptorMap = {
50
51
  'sql-text': sqlTextDescriptor,
51
52
  'sql-timestamp': sqlTimestampDescriptor,
52
53
  text: pgTextDescriptor,
54
+ enum: pgEnumDescriptor,
53
55
  character: pgCharDescriptor,
54
56
  'character varying': pgVarcharDescriptor,
55
57
  integer: pgIntDescriptor,
@@ -19,12 +19,14 @@ import {
19
19
  CodecDescriptorImpl,
20
20
  CodecImpl,
21
21
  type CodecInstanceContext,
22
+ type CodecMeta,
22
23
  type ColumnHelperFor,
23
24
  type ColumnHelperForStrict,
24
25
  column,
25
26
  renderTsLiteral,
26
27
  voidParamsSchema,
27
28
  } from '@prisma-next/framework-components/codec';
29
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
28
30
  import {
29
31
  SqlCharCodec,
30
32
  SqlFloatCodec,
@@ -60,6 +62,7 @@ import {
60
62
  PG_BOOL_CODEC_ID,
61
63
  PG_BYTEA_CODEC_ID,
62
64
  PG_CHAR_CODEC_ID,
65
+ PG_ENUM_CODEC_ID,
63
66
  PG_FLOAT_CODEC_ID,
64
67
  PG_FLOAT4_CODEC_ID,
65
68
  PG_FLOAT8_CODEC_ID,
@@ -81,6 +84,8 @@ import {
81
84
  PG_VARBIT_CODEC_ID,
82
85
  PG_VARCHAR_CODEC_ID,
83
86
  } from './codec-ids';
87
+ import { DEFAULT_NAMESPACE_ID } from './namespace-ids';
88
+ import { PostgresNativeEnum } from './postgres-native-enum';
84
89
 
85
90
  type LengthParams = { readonly length?: number };
86
91
  type PrecisionParams = { readonly precision?: number };
@@ -165,6 +170,106 @@ export const pgTextColumn = () =>
165
170
  pgTextColumn satisfies ColumnHelperFor<PgTextDescriptor>;
166
171
  pgTextColumn satisfies ColumnHelperForStrict<PgTextDescriptor>;
167
172
 
173
+ /**
174
+ * Codec for a `pg.enum(Ref)` column bound to a native Postgres enum type.
175
+ * Text passthrough, identical to `pg/text@1` — encode/decode do not carry the
176
+ * enum's member values; membership is enforced by the native type itself, not
177
+ * by this codec. `renderValueLiteral` renders a member value as its TS
178
+ * literal, which is what drives the column's typed value-union (via
179
+ * `renderValueSetType` reading the column's `valueSet` ref) — the codec
180
+ * itself carries no params of its own; typing comes entirely from the
181
+ * column's value-set, not from `pg/enum@1`.
182
+ *
183
+ * A distinct codec id (rather than reusing `pg/text@1` on a plain text
184
+ * column) keeps native-enum columns independently identifiable — from a
185
+ * column's `codecId` alone, without also inspecting `nativeType` — which
186
+ * the managed (DDL) phase needs to target `CREATE TYPE`/`ALTER TYPE`
187
+ * operations at exactly the columns that use one.
188
+ */
189
+ export class PgEnumCodec extends CodecImpl<
190
+ typeof PG_ENUM_CODEC_ID,
191
+ readonly ['equality', 'order', 'textual'],
192
+ string,
193
+ string
194
+ > {
195
+ async encode(value: string, _ctx: CodecCallContext): Promise<string> {
196
+ return value;
197
+ }
198
+ async decode(wire: string, _ctx: CodecCallContext): Promise<string> {
199
+ return wire;
200
+ }
201
+ encodeJson(value: string): JsonValue {
202
+ return value;
203
+ }
204
+ decodeJson(json: JsonValue): string {
205
+ return blindCast<
206
+ string,
207
+ 'text codec: a native-enum member value is stored as its wire string form'
208
+ >(json);
209
+ }
210
+ }
211
+
212
+ type PgEnumParams = { readonly typeName: string };
213
+
214
+ const pgEnumParamsSchema = arktype({
215
+ typeName: 'string',
216
+ }) satisfies StandardSchemaV1<PgEnumParams>;
217
+
218
+ function isJsonObject(
219
+ value: JsonValue | undefined,
220
+ ): value is { readonly [key: string]: JsonValue } {
221
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
222
+ }
223
+
224
+ export class PgEnumDescriptor extends CodecDescriptorImpl<PgEnumParams> {
225
+ override readonly codecId = PG_ENUM_CODEC_ID;
226
+ override readonly traits = ['equality', 'order', 'textual'] as const;
227
+ override readonly targetTypes = ['text'] as const;
228
+ override readonly meta = PG_TEXT_META;
229
+ override readonly paramsSchema = pgEnumParamsSchema satisfies StandardSchemaV1<PgEnumParams>;
230
+ override renderValueLiteral(value: JsonValue): string | undefined {
231
+ return renderTsLiteral(value);
232
+ }
233
+ override metaFor(typeParams: JsonValue | undefined): CodecMeta | undefined {
234
+ if (!isJsonObject(typeParams)) return this.meta;
235
+ const typeName = typeParams['typeName'];
236
+ return typeof typeName === 'string'
237
+ ? { db: { sql: { postgres: { nativeType: typeName } } } }
238
+ : this.meta;
239
+ }
240
+ override factory(_params: PgEnumParams): (ctx: CodecInstanceContext) => PgEnumCodec {
241
+ return () => new PgEnumCodec(this);
242
+ }
243
+
244
+ /**
245
+ * Authoring-time hook a `pg.enum(<ref>)` type constructor calls once it has
246
+ * resolved its ref argument to the referenced `native_enum` entity: given
247
+ * that entity and the field's namespace id, produces this codec's
248
+ * per-column `typeParams` and native type. The type name is schema-qualified
249
+ * for a named non-default schema (matching `format_type()`); `public` and
250
+ * the unbound namespace stay bare (`search_path`). `nativeType` mirrors
251
+ * `typeParams.typeName` — the same value {@link metaFor} derives at
252
+ * render time — so the column's declared native type and the render-time
253
+ * cast agree. Returns `undefined` if `entity` is not a `PostgresNativeEnum`
254
+ * (a contributor bug, not a user-schema error — the caller decides how to
255
+ * report it).
256
+ */
257
+ columnFromEntity(
258
+ entity: object,
259
+ namespaceId?: string,
260
+ ): { readonly typeParams: PgEnumParams; readonly nativeType: string } | undefined {
261
+ if (!PostgresNativeEnum.is(entity)) return undefined;
262
+ const qualifyNativeType =
263
+ namespaceId !== undefined &&
264
+ namespaceId !== DEFAULT_NAMESPACE_ID &&
265
+ namespaceId !== UNBOUND_NAMESPACE_ID;
266
+ const typeName = qualifyNativeType ? `${namespaceId}.${entity.typeName}` : entity.typeName;
267
+ return { typeParams: { typeName }, nativeType: typeName };
268
+ }
269
+ }
270
+
271
+ export const pgEnumDescriptor = new PgEnumDescriptor();
272
+
168
273
  /**
169
274
  * Postgres `text[]` codec. Encode is an identity pass-through: the pg wire
170
275
  * driver serialises a JS `string[]` to a Postgres array literal under the
@@ -1086,6 +1191,7 @@ export const codecDescriptors: readonly AnyCodecDescriptor[] = [
1086
1191
  sqlTextDescriptor,
1087
1192
  sqlTimestampDescriptor,
1088
1193
  pgTextDescriptor,
1194
+ pgEnumDescriptor,
1089
1195
  pgCharDescriptor,
1090
1196
  pgVarcharDescriptor,
1091
1197
  pgIntDescriptor,
@@ -6,11 +6,12 @@ import {
6
6
  postgresAuthoringTypes,
7
7
  } from './authoring';
8
8
  import { postgresTargetDescriptorMetaRuntime } from './descriptor-meta-runtime';
9
+ import { DEFAULT_NAMESPACE_ID } from './namespace-ids';
9
10
  import { postgresCreateNamespace } from './postgres-schema';
10
11
 
11
12
  const postgresTargetDescriptorMetaBase = {
12
13
  ...postgresTargetDescriptorMetaRuntime,
13
- defaultNamespaceId: 'public',
14
+ defaultNamespaceId: DEFAULT_NAMESPACE_ID,
14
15
  authoring: {
15
16
  type: postgresAuthoringTypes,
16
17
  field: postgresAuthoringFieldPresets,
@@ -1,7 +1,12 @@
1
1
  import type { EntityKindDescriptor } from '@prisma-next/framework-components/ir';
2
+ import { PostgresNativeEnum, type PostgresNativeEnumInput } from './postgres-native-enum';
2
3
  import { PostgresRlsPolicy, type PostgresRlsPolicyInput } from './postgres-rls-policy';
3
4
  import { PostgresRole, type PostgresRoleInput } from './postgres-role';
4
- import { PostgresRlsPolicySchema, PostgresRoleSchema } from './postgres-validators';
5
+ import {
6
+ PostgresNativeEnumSchema,
7
+ PostgresRlsPolicySchema,
8
+ PostgresRoleSchema,
9
+ } from './postgres-validators';
5
10
 
6
11
  export const policyEntityKind: EntityKindDescriptor<PostgresRlsPolicyInput, PostgresRlsPolicy> = {
7
12
  kind: 'policy',
@@ -14,3 +19,12 @@ export const roleEntityKind: EntityKindDescriptor<PostgresRoleInput, PostgresRol
14
19
  schema: PostgresRoleSchema,
15
20
  construct: (input) => new PostgresRole(input),
16
21
  };
22
+
23
+ export const nativeEnumEntityKind: EntityKindDescriptor<
24
+ PostgresNativeEnumInput,
25
+ PostgresNativeEnum
26
+ > = {
27
+ kind: 'native_enum',
28
+ schema: PostgresNativeEnumSchema,
29
+ construct: (input) => new PostgresNativeEnum(input),
30
+ };
@@ -1,5 +1,6 @@
1
1
  import type { ExecuteRequestLowerer } from '@prisma-next/family-sql/control-adapter';
2
2
  import { extensionExistsAst } from '../../../contract-free/checks';
3
+ import { DEFAULT_NAMESPACE_ID } from '../../namespace-ids';
3
4
  import { quoteIdentifier } from '../../sql-utils';
4
5
  import { type Op, step } from './shared';
5
6
 
@@ -55,7 +56,7 @@ export async function installExtension(
55
56
  invariantId,
56
57
  target: {
57
58
  id: 'postgres',
58
- details: { schema: 'public', objectType: 'dependency', name: extensionName },
59
+ details: { schema: DEFAULT_NAMESPACE_ID, objectType: 'dependency', name: extensionName },
59
60
  },
60
61
  precheck: [
61
62
  step(`verify extension "${extensionName}" is not already enabled`, absent.sql, absent.params),
@@ -1,20 +1,22 @@
1
1
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
2
2
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
3
+ import { DEFAULT_NAMESPACE_ID } from '../namespace-ids';
3
4
  import { isPostgresSchema } from '../postgres-schema';
4
5
 
5
6
  /**
6
7
  * Given the contract's storage and a namespace id, returns the live Postgres
7
8
  * DDL schema name that namespace maps to. A named Postgres namespace dispatches
8
- * to its `ddlSchemaName(storage)`; the unbound sentinel resolves to `public`
9
- * (the search-path default for offline planning); a bare object payload (used
10
- * by some tests) falls back to the namespace id itself.
9
+ * to its `ddlSchemaName(storage)`; the unbound sentinel resolves to
10
+ * `DEFAULT_NAMESPACE_ID` (the search-path default for offline planning); a
11
+ * bare object payload (used by some tests) falls back to the namespace id
12
+ * itself.
11
13
  */
12
14
  export function resolveDdlSchemaForNamespaceStorage(
13
15
  storage: SqlStorage,
14
16
  namespaceId: string,
15
17
  ): string {
16
18
  if (namespaceId === UNBOUND_NAMESPACE_ID) {
17
- return 'public';
19
+ return DEFAULT_NAMESPACE_ID;
18
20
  }
19
21
  const namespace = storage.namespaces[namespaceId];
20
22
  if (namespace && isPostgresSchema(namespace)) {
@@ -3,6 +3,7 @@ import type { SchemaIssue } from '@prisma-next/framework-components/control';
3
3
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
4
4
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
5
5
  import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
6
+ import { DEFAULT_NAMESPACE_ID } from '../namespace-ids';
6
7
  import { isPostgresSchema } from '../postgres-schema';
7
8
  import { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
8
9
 
@@ -43,7 +44,7 @@ function existingSchemasFromSchema(schema: SqlSchemaIRNode): readonly string[] {
43
44
  if (PostgresDatabaseSchemaNode.is(schema)) {
44
45
  return schema.existingSchemas;
45
46
  }
46
- return ['public'];
47
+ return [DEFAULT_NAMESPACE_ID];
47
48
  }
48
49
 
49
50
  /**
@@ -0,0 +1,3 @@
1
+ // Leaf module: authoring.ts and descriptor-meta.ts both need this and import each other.
2
+ /** Postgres's default schema; `postgresTargetDescriptorMeta.defaultNamespaceId` is this value. */
3
+ export const DEFAULT_NAMESPACE_ID = 'public' as const;
@@ -19,7 +19,7 @@ import { blindCast } from '@prisma-next/utils/casts';
19
19
  import type { JsonObject, JsonValue } from '@prisma-next/utils/json';
20
20
  import { postgresAuthoringEntityTypes } from './authoring';
21
21
  import { PG_INT_CODEC_ID, PG_TEXT_CODEC_ID } from './codec-ids';
22
- import { policyEntityKind, roleEntityKind } from './entity-kinds';
22
+ import { nativeEnumEntityKind, policyEntityKind, roleEntityKind } from './entity-kinds';
23
23
  import { isPostgresSchema, PostgresSchema } from './postgres-schema';
24
24
 
25
25
  const POSTGRES_AUTHORING_CTX: AuthoringEntityContext = {
@@ -70,7 +70,12 @@ function collectStorageTypesHydrators(
70
70
  export class PostgresContractSerializer extends SqlContractSerializerBase<Contract<SqlStorage>> {
71
71
  constructor(extraPackEntityKinds: readonly AnyEntityKindDescriptor[] = []) {
72
72
  const storageTypesHydrators = collectStorageTypesHydrators(postgresAuthoringEntityTypes);
73
- super(storageTypesHydrators, [policyEntityKind, roleEntityKind, ...extraPackEntityKinds]);
73
+ super(storageTypesHydrators, [
74
+ policyEntityKind,
75
+ roleEntityKind,
76
+ nativeEnumEntityKind,
77
+ ...extraPackEntityKinds,
78
+ ]);
74
79
  }
75
80
 
76
81
  protected override hydrateSqlNamespaceEntry(
@@ -143,26 +148,19 @@ export class PostgresContractSerializer extends SqlContractSerializerBase<Contra
143
148
  });
144
149
  }
145
150
 
151
+ /**
152
+ * Native enums are deliberately not serialized here — `native_enum` is
153
+ * authoring-time-only. Once lowered, its member values live on in the
154
+ * `valueSet` slot it derives (via the SQL family's generic
155
+ * `deriveValueSet` mechanism); nothing downstream reads the
156
+ * `PostgresNativeEnum` entity itself, so re-emitting it into
157
+ * `contract.json` would be dead weight.
158
+ */
146
159
  private serializePostgresNamespace(ns: PostgresSchema, isUnboundSlot: boolean): JsonObject {
147
- const tablesOut: Record<string, JsonObject> = {};
148
- for (const [tableName, table] of Object.entries(ns.table)) {
149
- tablesOut[tableName] = this.serializeJsonObject(table);
150
- }
151
- const valueSetEntries = ns.valueSet;
152
- const valueSetOut: Record<string, JsonObject> = {};
153
- if (valueSetEntries !== undefined) {
154
- for (const [valueSetName, valueSet] of Object.entries(valueSetEntries)) {
155
- valueSetOut[valueSetName] = this.serializeJsonObject(valueSet);
156
- }
157
- }
158
- const roleOut: Record<string, JsonObject> = {};
159
- for (const [roleName, role] of Object.entries(ns.role)) {
160
- roleOut[roleName] = this.serializeJsonObject(role);
161
- }
162
- const policyOut: Record<string, JsonObject> = {};
163
- for (const [policyName, policy] of Object.entries(ns.policy)) {
164
- policyOut[policyName] = this.serializeJsonObject(policy);
165
- }
160
+ const tablesOut = this.serializeEntries(ns.table);
161
+ const valueSetOut = this.serializeEntries(ns.valueSet ?? {});
162
+ const roleOut = this.serializeEntries(ns.role);
163
+ const policyOut = this.serializeEntries(ns.policy);
166
164
  return {
167
165
  id: ns.id,
168
166
  kind: isUnboundSlot ? 'postgres-unbound-schema' : 'postgres-schema',
@@ -175,6 +173,14 @@ export class PostgresContractSerializer extends SqlContractSerializerBase<Contra
175
173
  };
176
174
  }
177
175
 
176
+ private serializeEntries(entries: Readonly<Record<string, unknown>>): Record<string, JsonObject> {
177
+ const out: Record<string, JsonObject> = {};
178
+ for (const [name, entry] of Object.entries(entries)) {
179
+ out[name] = this.serializeJsonObject(entry);
180
+ }
181
+ return out;
182
+ }
183
+
178
184
  private serializeJsonObject(value: unknown): JsonObject {
179
185
  return blindCast<
180
186
  JsonObject,
@@ -0,0 +1,51 @@
1
+ import type { ControlPolicy } from '@prisma-next/contract/types';
2
+ import { freezeNode } from '@prisma-next/framework-components/ir';
3
+ import { SqlNode } from '@prisma-next/sql-contract/types';
4
+
5
+ export interface PostgresNativeEnumInput {
6
+ /** The Postgres type name (`CREATE TYPE <typeName> AS ENUM (…)`). */
7
+ readonly typeName: string;
8
+ /** Member values in declaration order — this is the Postgres enum sort order. */
9
+ readonly members: readonly string[];
10
+ readonly control?: ControlPolicy;
11
+ }
12
+
13
+ /**
14
+ * Postgres contract-IR class for a native enum type (`CREATE TYPE … AS ENUM (…)`).
15
+ *
16
+ * This is an authored, serialized Contract-IR entity — it is registered as an entity
17
+ * kind, extends `SqlNode`, and is stored in `contract.json`. It is NOT a DiffableNode;
18
+ * the schema-diff tree will use `PostgresNativeEnumSchemaNode` when the managed phase
19
+ * builds it.
20
+ *
21
+ * A member is a value, not a name→value pair — matching `CREATE TYPE … AS ENUM
22
+ * ('a', 'b')`, which has no separate member name.
23
+ *
24
+ * Target-only concept — no SQL-family abstract. Extends `SqlNode` directly,
25
+ * frozen at construction via `freezeNode(this)`. The `kind: 'postgres-enum'`
26
+ * discriminant is enumerable so it survives JSON. Lives at
27
+ * `storage.namespaces[ns].entries.native_enum[HandleName]`; the entries key
28
+ * (`native_enum`) is the entity-kind descriptor's `kind`, decoupled from this
29
+ * node's own `kind` literal — the same shape as `table`/`StorageTable` and
30
+ * `valueSet`/`StorageValueSet`.
31
+ */
32
+ export class PostgresNativeEnum extends SqlNode {
33
+ static is(node: unknown): node is PostgresNativeEnum {
34
+ return (
35
+ typeof node === 'object' && node !== null && 'kind' in node && node.kind === 'postgres-enum'
36
+ );
37
+ }
38
+
39
+ override readonly kind = 'postgres-enum' as const;
40
+ readonly typeName: string;
41
+ readonly members: readonly string[];
42
+ declare readonly control?: ControlPolicy;
43
+
44
+ constructor(input: PostgresNativeEnumInput) {
45
+ super();
46
+ this.typeName = input.typeName;
47
+ this.members = Object.freeze([...input.members]);
48
+ if (input.control !== undefined) this.control = input.control;
49
+ freezeNode(this);
50
+ }
51
+ }