@prisma-next/target-postgres 0.7.0-dev.7 → 0.8.0-dev.1

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 (149) hide show
  1. package/dist/{codec-ids-CplrEfmx.d.mts → codec-ids-RvYfmUmi.d.mts} +1 -1
  2. package/dist/{codec-ids-CplrEfmx.d.mts.map → codec-ids-RvYfmUmi.d.mts.map} +1 -1
  3. package/dist/codec-ids.d.mts +1 -1
  4. package/dist/{codec-types-lrsb3N07.d.mts → codec-types-667FxIW8.d.mts} +2 -2
  5. package/dist/{codec-types-lrsb3N07.d.mts.map → codec-types-667FxIW8.d.mts.map} +1 -1
  6. package/dist/codec-types.d.mts +1 -1
  7. package/dist/{codecs-Cue97Xqf.d.mts → codecs-DXeDABSO.d.mts} +2 -2
  8. package/dist/{codecs-Cue97Xqf.d.mts.map → codecs-DXeDABSO.d.mts.map} +1 -1
  9. package/dist/codecs.d.mts +1 -1
  10. package/dist/control.d.mts +2 -2
  11. package/dist/control.d.mts.map +1 -1
  12. package/dist/control.mjs +38 -7
  13. package/dist/control.mjs.map +1 -1
  14. package/dist/{data-transform-bIeAcZIJ.d.mts → data-transform-B6p02mFJ.d.mts} +3 -3
  15. package/dist/{data-transform-bIeAcZIJ.d.mts.map → data-transform-B6p02mFJ.d.mts.map} +1 -1
  16. package/dist/{data-transform-DKWXdHuZ.mjs → data-transform-aF9az88u.mjs} +1 -1
  17. package/dist/{data-transform-DKWXdHuZ.mjs.map → data-transform-aF9az88u.mjs.map} +1 -1
  18. package/dist/data-transform.d.mts +1 -1
  19. package/dist/data-transform.mjs +1 -1
  20. package/dist/{default-normalizer-C8XyZj85.mjs → default-normalizer-DHCsbfjc.mjs} +1 -1
  21. package/dist/{default-normalizer-C8XyZj85.mjs.map → default-normalizer-DHCsbfjc.mjs.map} +1 -1
  22. package/dist/default-normalizer.mjs +1 -1
  23. package/dist/descriptor-meta-DFUCClk_.mjs +124 -0
  24. package/dist/descriptor-meta-DFUCClk_.mjs.map +1 -0
  25. package/dist/enum-planning-Bqp96iIw.mjs +63 -0
  26. package/dist/enum-planning-Bqp96iIw.mjs.map +1 -0
  27. package/dist/enum-planning.d.mts +48 -0
  28. package/dist/enum-planning.d.mts.map +1 -0
  29. package/dist/enum-planning.mjs +2 -0
  30. package/dist/{errors-Chm2bKcS.mjs → errors-BiOloWUh.mjs} +1 -1
  31. package/dist/{errors-Chm2bKcS.mjs.map → errors-BiOloWUh.mjs.map} +1 -1
  32. package/dist/errors.mjs +1 -1
  33. package/dist/{issue-planner-DQ6WJkad.mjs → issue-planner-BhWVYyE1.mjs} +114 -49
  34. package/dist/issue-planner-BhWVYyE1.mjs.map +1 -0
  35. package/dist/issue-planner.d.mts +11 -9
  36. package/dist/issue-planner.d.mts.map +1 -1
  37. package/dist/issue-planner.mjs +1 -1
  38. package/dist/migration.d.mts +4 -4
  39. package/dist/migration.d.mts.map +1 -1
  40. package/dist/migration.mjs +3 -3
  41. package/dist/{native-type-normalizer-Cry4QoLf.mjs → native-type-normalizer-DMikJJ1V.mjs} +1 -1
  42. package/dist/{native-type-normalizer-Cry4QoLf.mjs.map → native-type-normalizer-DMikJJ1V.mjs.map} +1 -1
  43. package/dist/native-type-normalizer.mjs +1 -1
  44. package/dist/{op-factory-call-DeaFxa8_.mjs → op-factory-call-DerP9BoT.mjs} +8 -5
  45. package/dist/{op-factory-call-DeaFxa8_.mjs.map → op-factory-call-DerP9BoT.mjs.map} +1 -1
  46. package/dist/{op-factory-call-UFpUPJL6.d.mts → op-factory-call-c1zELk3U.d.mts} +5 -4
  47. package/dist/{op-factory-call-UFpUPJL6.d.mts.map → op-factory-call-c1zELk3U.d.mts.map} +1 -1
  48. package/dist/op-factory-call.d.mts +1 -1
  49. package/dist/op-factory-call.mjs +1 -1
  50. package/dist/pack.d.mts +13 -24
  51. package/dist/pack.d.mts.map +1 -1
  52. package/dist/pack.mjs +1 -1
  53. package/dist/{planner-CYtKhLYa.mjs → planner-DSDXUbQ4.mjs} +8 -6
  54. package/dist/planner-DSDXUbQ4.mjs.map +1 -0
  55. package/dist/{planner-ddl-builders-CLB7Umhh.mjs → planner-ddl-builders-5QIyhBUF.mjs} +3 -3
  56. package/dist/planner-ddl-builders-5QIyhBUF.mjs.map +1 -0
  57. package/dist/planner-ddl-builders.d.mts +5 -5
  58. package/dist/planner-ddl-builders.d.mts.map +1 -1
  59. package/dist/planner-ddl-builders.mjs +1 -1
  60. package/dist/{planner-identity-values-DTx0gePL.mjs → planner-identity-values-BUYNOCwb.mjs} +9 -3
  61. package/dist/planner-identity-values-BUYNOCwb.mjs.map +1 -0
  62. package/dist/planner-identity-values.d.mts +1 -1
  63. package/dist/planner-identity-values.d.mts.map +1 -1
  64. package/dist/planner-identity-values.mjs +1 -1
  65. package/dist/{planner-produced-postgres-migration-CjxWIVgh.d.mts → planner-produced-postgres-migration-D34ftfEK.d.mts} +3 -3
  66. package/dist/{planner-produced-postgres-migration-CjxWIVgh.d.mts.map → planner-produced-postgres-migration-D34ftfEK.d.mts.map} +1 -1
  67. package/dist/{planner-produced-postgres-migration-DphktB2N.mjs → planner-produced-postgres-migration-D8OCSSLM.mjs} +4 -4
  68. package/dist/{planner-produced-postgres-migration-DphktB2N.mjs.map → planner-produced-postgres-migration-D8OCSSLM.mjs.map} +1 -1
  69. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  70. package/dist/planner-produced-postgres-migration.mjs +1 -1
  71. package/dist/{planner-schema-lookup-B1ags8ys.mjs → planner-schema-lookup--u9whY_Y.mjs} +1 -1
  72. package/dist/{planner-schema-lookup-B1ags8ys.mjs.map → planner-schema-lookup--u9whY_Y.mjs.map} +1 -1
  73. package/dist/planner-schema-lookup.mjs +1 -1
  74. package/dist/{planner-sql-checks-DwZvGlV4.mjs → planner-sql-checks-Cd016Ycs.mjs} +7 -2
  75. package/dist/planner-sql-checks-Cd016Ycs.mjs.map +1 -0
  76. package/dist/planner-sql-checks.d.mts +2 -2
  77. package/dist/planner-sql-checks.d.mts.map +1 -1
  78. package/dist/planner-sql-checks.mjs +1 -1
  79. package/dist/{planner-target-details-bVVcanWh.d.mts → planner-target-details-iYJwzFHP.d.mts} +1 -1
  80. package/dist/{planner-target-details-bVVcanWh.d.mts.map → planner-target-details-iYJwzFHP.d.mts.map} +1 -1
  81. package/dist/planner-target-details.d.mts +1 -1
  82. package/dist/planner.d.mts +1 -1
  83. package/dist/planner.d.mts.map +1 -1
  84. package/dist/planner.mjs +1 -1
  85. package/dist/postgres-contract-serializer-D5VJk6lo.mjs +61 -0
  86. package/dist/postgres-contract-serializer-D5VJk6lo.mjs.map +1 -0
  87. package/dist/postgres-enum-type-CrKq8au9.d.mts +69 -0
  88. package/dist/postgres-enum-type-CrKq8au9.d.mts.map +1 -0
  89. package/dist/postgres-enum-type-DS-KLVRH.mjs +61 -0
  90. package/dist/postgres-enum-type-DS-KLVRH.mjs.map +1 -0
  91. package/dist/{postgres-migration-UkcHfZAA.d.mts → postgres-migration-CiQzhcMe.d.mts} +4 -4
  92. package/dist/{postgres-migration-UkcHfZAA.d.mts.map → postgres-migration-CiQzhcMe.d.mts.map} +1 -1
  93. package/dist/{postgres-migration-Bkv140RW.mjs → postgres-migration-Fdxzo6l2.mjs} +3 -3
  94. package/dist/{postgres-migration-Bkv140RW.mjs.map → postgres-migration-Fdxzo6l2.mjs.map} +1 -1
  95. package/dist/{render-ops--1nnfNus.mjs → render-ops-CkiuHSNj.mjs} +1 -1
  96. package/dist/{render-ops--1nnfNus.mjs.map → render-ops-CkiuHSNj.mjs.map} +1 -1
  97. package/dist/render-ops.d.mts +1 -1
  98. package/dist/render-ops.mjs +1 -1
  99. package/dist/{render-typescript-D3doH-vX.mjs → render-typescript-C9XWI8Ld.mjs} +1 -1
  100. package/dist/{render-typescript-D3doH-vX.mjs.map → render-typescript-C9XWI8Ld.mjs.map} +1 -1
  101. package/dist/render-typescript.mjs +1 -1
  102. package/dist/runtime.d.mts +25 -1
  103. package/dist/runtime.d.mts.map +1 -1
  104. package/dist/runtime.mjs +3 -2
  105. package/dist/runtime.mjs.map +1 -1
  106. package/dist/{shared-MpwjwAjM.d.mts → shared-DLYdmYo-.d.mts} +2 -2
  107. package/dist/{shared-MpwjwAjM.d.mts.map → shared-DLYdmYo-.d.mts.map} +1 -1
  108. package/dist/{sql-utils-CggjWNij.mjs → sql-utils-BewXAnsG.mjs} +1 -1
  109. package/dist/{sql-utils-CggjWNij.mjs.map → sql-utils-BewXAnsG.mjs.map} +1 -1
  110. package/dist/sql-utils.mjs +1 -1
  111. package/dist/{statement-builders-BT889jV0.mjs → statement-builders-BSIQMClE.mjs} +1 -1
  112. package/dist/{statement-builders-BT889jV0.mjs.map → statement-builders-BSIQMClE.mjs.map} +1 -1
  113. package/dist/statement-builders.mjs +1 -1
  114. package/dist/{tables-DgYIXjUt.mjs → tables-Ce_Q0I8B.mjs} +7 -7
  115. package/dist/{tables-DgYIXjUt.mjs.map → tables-Ce_Q0I8B.mjs.map} +1 -1
  116. package/dist/{types-CTqpysRY.d.mts → types-Dq74Z3eu.d.mts} +1 -1
  117. package/dist/types-Dq74Z3eu.d.mts.map +1 -0
  118. package/dist/types.d.mts +3 -2
  119. package/dist/types.mjs +2 -1
  120. package/package.json +18 -17
  121. package/src/core/authoring.ts +41 -9
  122. package/src/core/descriptor-meta.ts +6 -1
  123. package/src/core/migrations/enum-planning.ts +93 -0
  124. package/src/core/migrations/issue-planner.ts +25 -13
  125. package/src/core/migrations/op-factory-call.ts +12 -3
  126. package/src/core/migrations/operations/enums.ts +5 -4
  127. package/src/core/migrations/planner-ddl-builders.ts +4 -3
  128. package/src/core/migrations/planner-identity-values.ts +28 -4
  129. package/src/core/migrations/planner-recipes.ts +6 -2
  130. package/src/core/migrations/planner-sql-checks.ts +6 -2
  131. package/src/core/migrations/planner-strategies.ts +187 -74
  132. package/src/core/migrations/planner-type-resolution.ts +20 -2
  133. package/src/core/migrations/planner.ts +3 -0
  134. package/src/core/migrations/runner.ts +3 -0
  135. package/src/core/postgres-contract-serializer.ts +70 -0
  136. package/src/core/postgres-enum-type.ts +85 -0
  137. package/src/core/postgres-schema-verifier.ts +37 -0
  138. package/src/exports/control.ts +4 -0
  139. package/src/exports/enum-planning.ts +6 -0
  140. package/src/exports/runtime.ts +2 -0
  141. package/src/exports/types.ts +1 -0
  142. package/dist/descriptor-meta-Dde_BS3K.mjs +0 -99
  143. package/dist/descriptor-meta-Dde_BS3K.mjs.map +0 -1
  144. package/dist/issue-planner-DQ6WJkad.mjs.map +0 -1
  145. package/dist/planner-CYtKhLYa.mjs.map +0 -1
  146. package/dist/planner-ddl-builders-CLB7Umhh.mjs.map +0 -1
  147. package/dist/planner-identity-values-DTx0gePL.mjs.map +0 -1
  148. package/dist/planner-sql-checks-DwZvGlV4.mjs.map +0 -1
  149. package/dist/types-CTqpysRY.d.mts.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import type { CodecControlHooks } from '@prisma-next/family-sql/control';
2
2
  import type {
3
3
  ForeignKey,
4
+ PostgresEnumStorageEntry,
4
5
  ReferentialAction,
5
6
  StorageColumn,
6
7
  StorageTable,
@@ -15,7 +16,7 @@ export function buildCreateTableSql(
15
16
  qualifiedTableName: string,
16
17
  table: StorageTable,
17
18
  codecHooks: Map<string, CodecControlHooks>,
18
- storageTypes: Record<string, StorageTypeInstance> = {},
19
+ storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = {},
19
20
  ): string {
20
21
  const columnDefinitions = Object.entries(table.columns).map(
21
22
  ([columnName, column]: [string, StorageColumn]) => {
@@ -80,7 +81,7 @@ function assertSafeDefaultExpression(expression: string): void {
80
81
  export function buildColumnTypeSql(
81
82
  column: StorageColumn,
82
83
  codecHooks: Map<string, CodecControlHooks>,
83
- storageTypes: Record<string, StorageTypeInstance> = {},
84
+ storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = {},
84
85
  allowPseudoTypes = true,
85
86
  ): string {
86
87
  const resolved = resolveColumnTypeMetadata(column, storageTypes);
@@ -201,7 +202,7 @@ export function buildAddColumnSql(
201
202
  column: StorageColumn,
202
203
  codecHooks: Map<string, CodecControlHooks>,
203
204
  temporaryDefault?: string | null,
204
- storageTypes: Record<string, StorageTypeInstance> = {},
205
+ storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = {},
205
206
  ): string {
206
207
  const typeSql = buildColumnTypeSql(column, codecHooks, storageTypes);
207
208
  const defaultSql =
@@ -1,6 +1,12 @@
1
1
  import type { CodecControlHooks } from '@prisma-next/family-sql/control';
2
- import type { StorageColumn, StorageTypeInstance } from '@prisma-next/sql-contract/types';
2
+ import {
3
+ isPostgresEnumStorageEntry,
4
+ type PostgresEnumStorageEntry,
5
+ type StorageColumn,
6
+ type StorageTypeInstance,
7
+ } from '@prisma-next/sql-contract/types';
3
8
  import { ifDefined } from '@prisma-next/utils/defined';
9
+ import type { PostgresEnumType } from '../postgres-enum-type';
4
10
 
5
11
  /**
6
12
  * Resolves the identity value (monoid neutral element) as a SQL literal for a column's type.
@@ -10,12 +16,30 @@ import { ifDefined } from '@prisma-next/utils/defined';
10
16
  export function resolveIdentityValue(
11
17
  column: StorageColumn,
12
18
  codecHooks: Map<string, CodecControlHooks>,
13
- storageTypes: Record<string, StorageTypeInstance> = {},
19
+ storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = {},
14
20
  ): string | null {
15
21
  const referencedType = column.typeRef ? storageTypes[column.typeRef] : undefined;
16
- const codecId = referencedType?.codecId ?? column.codecId;
22
+ const referencedIsEnum =
23
+ referencedType !== undefined && isPostgresEnumStorageEntry(referencedType);
24
+ const referencedBinding = referencedIsEnum
25
+ ? ((referencedType as PostgresEnumType).codecBinding ?? {
26
+ codecId: (referencedType as PostgresEnumStorageEntry).codecId,
27
+ typeParams: { values: (referencedType as PostgresEnumStorageEntry).values },
28
+ })
29
+ : undefined;
30
+ const codecId =
31
+ referencedBinding?.codecId ??
32
+ (referencedType && !referencedIsEnum
33
+ ? (referencedType as StorageTypeInstance).codecId
34
+ : undefined) ??
35
+ column.codecId;
17
36
  const nativeType = referencedType?.nativeType ?? column.nativeType;
18
- const typeParams = referencedType?.typeParams ?? column.typeParams;
37
+ const typeParams =
38
+ (referencedBinding?.typeParams as Record<string, unknown> | undefined) ??
39
+ (referencedType && !referencedIsEnum
40
+ ? (referencedType as StorageTypeInstance).typeParams
41
+ : undefined) ??
42
+ column.typeParams;
19
43
 
20
44
  if (codecId) {
21
45
  const hookDefault = codecHooks.get(codecId)?.resolveIdentityValue?.({
@@ -1,5 +1,9 @@
1
1
  import type { CodecControlHooks, SqlMigrationPlanOperation } from '@prisma-next/family-sql/control';
2
- import type { StorageColumn, StorageTypeInstance } from '@prisma-next/sql-contract/types';
2
+ import type {
3
+ PostgresEnumStorageEntry,
4
+ StorageColumn,
5
+ StorageTypeInstance,
6
+ } from '@prisma-next/sql-contract/types';
3
7
  import { quoteIdentifier } from '../sql-utils';
4
8
  import { buildAddColumnSql } from './planner-ddl-builders';
5
9
  import {
@@ -35,7 +39,7 @@ export function buildAddNotNullColumnWithTemporaryDefaultOperation(options: {
35
39
  readonly columnName: string;
36
40
  readonly column: StorageColumn;
37
41
  readonly codecHooks: Map<string, CodecControlHooks>;
38
- readonly storageTypes: Record<string, StorageTypeInstance>;
42
+ readonly storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
39
43
  readonly temporaryDefault: string;
40
44
  }): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
41
45
  const { schema, tableName, columnName, column, codecHooks, storageTypes, temporaryDefault } =
@@ -1,5 +1,9 @@
1
1
  import type { CodecControlHooks } from '@prisma-next/family-sql/control';
2
- import type { StorageColumn, StorageTypeInstance } from '@prisma-next/sql-contract/types';
2
+ import type {
3
+ PostgresEnumStorageEntry,
4
+ StorageColumn,
5
+ StorageTypeInstance,
6
+ } from '@prisma-next/sql-contract/types';
3
7
  import { escapeLiteral, quoteIdentifier } from '../sql-utils';
4
8
  import { resolveColumnTypeMetadata } from './planner-type-resolution';
5
9
 
@@ -230,7 +234,7 @@ function formatUserDefinedTypeName(identifier: string): string {
230
234
  export function buildExpectedFormatType(
231
235
  column: StorageColumn,
232
236
  codecHooks: Map<string, CodecControlHooks>,
233
- storageTypes: Record<string, StorageTypeInstance> = {},
237
+ storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = {},
234
238
  ): string {
235
239
  const resolved = resolveColumnTypeMetadata(column, storageTypes);
236
240
 
@@ -26,8 +26,15 @@ import type {
26
26
  } from '@prisma-next/family-sql/control';
27
27
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
28
28
  import type { SchemaIssue } from '@prisma-next/framework-components/control';
29
- import type { SqlStorage, StorageTypeInstance } from '@prisma-next/sql-contract/types';
29
+ import {
30
+ isPostgresEnumStorageEntry,
31
+ type PostgresEnumStorageEntry,
32
+ type SqlStorage,
33
+ type StorageTypeInstance,
34
+ } from '@prisma-next/sql-contract/types';
30
35
  import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
36
+ import { PostgresEnumType } from '../postgres-enum-type';
37
+ import { determineEnumDiff, readExistingEnumValues } from './enum-planning';
31
38
  import {
32
39
  AddColumnCall,
33
40
  AddEnumValuesCall,
@@ -80,7 +87,7 @@ export interface StrategyContext {
80
87
  readonly fromContract: Contract<SqlStorage> | null;
81
88
  readonly schemaName: string;
82
89
  readonly codecHooks: ReadonlyMap<string, CodecControlHooks>;
83
- readonly storageTypes: Readonly<Record<string, StorageTypeInstance>>;
90
+ readonly storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>;
84
91
  readonly schema: SqlSchemaIR;
85
92
  readonly policy: MigrationOperationPolicy;
86
93
  readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
@@ -101,10 +108,12 @@ export type CallMigrationStrategy = (
101
108
  /**
102
109
  * `true` for strategies that emit cohesive sequential recipes whose
103
110
  * calls must stay contiguous and in the returned order — e.g.
104
- * `enumChangeCallStrategy` (dataTransformcreateEnumType
105
- * dropEnumType), `notNullBackfillCallStrategy` (addColumn
106
- * dataTransform → setNotNull). Defaults to `false`, which lets
107
- * `planIssues` hoist individual calls into their DDL sequencing bucket.
111
+ * `nativeEnumPlanCallStrategy` (createEnumTypealterColumnType
112
+ * dropEnumType → renameType, optionally prefixed by a
113
+ * `DataTransformCall` placeholder), `notNullBackfillCallStrategy`
114
+ * (addColumn dataTransform setNotNull). Defaults to `false`,
115
+ * which lets `planIssues` hoist individual calls into their DDL
116
+ * sequencing bucket.
108
117
  */
109
118
  recipe?: boolean;
110
119
  }
@@ -119,7 +128,10 @@ function buildColumnSpec(
119
128
  const col = ctx.toContract.storage.tables[table]?.columns[column];
120
129
  if (!col) throw new Error(`Column "${table}"."${column}" not found in destination contract`);
121
130
  const mutableHooks = ctx.codecHooks as Map<string, CodecControlHooks>;
122
- const mutableTypes = ctx.storageTypes as Record<string, StorageTypeInstance>;
131
+ const mutableTypes = ctx.storageTypes as Record<
132
+ string,
133
+ StorageTypeInstance | PostgresEnumStorageEntry
134
+ >;
123
135
  return {
124
136
  name: column,
125
137
  typeSql: buildColumnTypeSql(col, mutableHooks, mutableTypes),
@@ -137,7 +149,10 @@ function buildAlterTypeOptions(
137
149
  const col = ctx.toContract.storage.tables[table]?.columns[column];
138
150
  if (!col) throw new Error(`Column "${table}"."${column}" not found in destination contract`);
139
151
  const mutableHooks = ctx.codecHooks as Map<string, CodecControlHooks>;
140
- const mutableTypes = ctx.storageTypes as Record<string, StorageTypeInstance>;
152
+ const mutableTypes = ctx.storageTypes as Record<
153
+ string,
154
+ StorageTypeInstance | PostgresEnumStorageEntry
155
+ >;
141
156
  const qualifiedTargetType = buildColumnTypeSql(col, mutableHooks, mutableTypes, false);
142
157
  const formatTypeExpected = buildExpectedFormatType(col, mutableHooks, mutableTypes);
143
158
  return {
@@ -276,8 +291,11 @@ function enumRebuildCallRecipe(
276
291
  ): readonly PostgresOpFactoryCall[] {
277
292
  const toType = ctx.toContract.storage.types?.[typeName];
278
293
  if (!toType) return [];
294
+ const isEnum = isPostgresEnumStorageEntry(toType);
279
295
  const nativeType = toType.nativeType;
280
- const desiredValues = (toType.typeParams['values'] ?? []) as readonly string[];
296
+ const desiredValues: readonly string[] = isEnum
297
+ ? toType.values
298
+ : (((toType as StorageTypeInstance).typeParams['values'] ?? []) as readonly string[]);
281
299
  const tempName = `${nativeType}${REBUILD_SUFFIX}`;
282
300
 
283
301
  const columnRefs: { table: string; column: string }[] = [];
@@ -305,70 +323,149 @@ function enumRebuildCallRecipe(
305
323
  ];
306
324
  }
307
325
 
308
- export const enumChangeCallStrategy: CallMigrationStrategy = (issues, ctx) => {
309
- // The shrink/rebuild branches emit a `DataTransformCall` placeholder or a
310
- // destructive rebuild that should be authored explicitly. When the policy
311
- // excludes `'data'` (`db update` / `db init`), skip the entire strategy so
312
- // `storageTypePlanCallStrategy` (codec-hook driven) takes over with the
313
- // dev-push enum behavior.
314
- if (!ctx.policy.allowedOperationClasses.includes('data')) return { kind: 'no_match' };
326
+ // ============================================================================
327
+ // Native enum planner strategy
328
+ // ============================================================================
315
329
 
316
- const matched: SchemaIssue[] = [];
317
- const calls: PostgresOpFactoryCall[] = [];
330
+ /**
331
+ * Single planner strategy for `PostgresEnumType` instances. Walks
332
+ * `toContract.storage.types` directly (no codec-hook dispatch) and
333
+ * resolves existing values via `readExistingEnumValues`, the same
334
+ * Postgres bridging adapter the verifier uses.
335
+ *
336
+ * Per-enum dispatch:
337
+ *
338
+ * - No existing type → `CreateEnumTypeCall` with the contract's desired
339
+ * values.
340
+ * - Diff is `unchanged` → no calls emitted (consumes the matching
341
+ * `enum_values_changed` issue if present).
342
+ * - Diff is `add_values` → `AddEnumValuesCall` with the new labels.
343
+ * - Diff is `rebuild` → the create-temp / migrate-columns /
344
+ * drop-original / rename rebuild recipe. When
345
+ * `policy.allowedOperationClasses` includes `'data'` and the rebuild
346
+ * removes labels (`removedValues.length > 0`), prepend a
347
+ * `DataTransformCall` placeholder so the user can author the value
348
+ * remap before the destructive recipe runs. Without `'data'` in the
349
+ * policy (`db update` / `db init`), the rebuild's PG `USING ::text`
350
+ * cast surfaces any value-removal data loss as a runtime error rather
351
+ * than silent loss.
352
+ *
353
+ * Returns `recipe: true` only when a rebuild recipe was emitted (its
354
+ * `createEnumType(temp) → alterColumnType → dropEnumType(orig) →
355
+ * renameType` sequence mixes `dep`-class and `alter`-class calls that
356
+ * would mis-order if the planner hoisted them into its DDL sequencing
357
+ * buckets). For the create-only and add-values paths the strategy
358
+ * returns `recipe: false` so the planner hoists `CreateEnumTypeCall`
359
+ * into the `dep` bucket — i.e. `CREATE TYPE` runs before any
360
+ * `CreateTableCall` that references the new enum.
361
+ */
362
+ export const nativeEnumPlanCallStrategy: CallMigrationStrategy = (issues, ctx) => {
363
+ const enumTypes = collectPostgresEnumTypes(ctx.toContract.storage.types);
364
+ if (enumTypes.size === 0) return { kind: 'no_match' };
318
365
 
319
- for (const issue of issues) {
320
- if (issue.kind !== 'enum_values_changed') continue;
321
- matched.push(issue);
366
+ const dataAllowed = ctx.policy.allowedOperationClasses.includes('data');
322
367
 
323
- if (issue.removedValues.length > 0) {
368
+ const calls: PostgresOpFactoryCall[] = [];
369
+ const handledTypeNames = new Set<string>();
370
+ const introducedTypeNames = new Set<string>();
371
+ const rebuiltTypeNames = new Set<string>();
372
+ let emittedRebuildRecipe = false;
373
+
374
+ for (const [typeName, enumType] of enumTypes) {
375
+ const desired = enumType.values;
376
+ const existing = readExistingEnumValues(ctx.schema, enumType.nativeType);
377
+ if (!existing) {
378
+ calls.push(new CreateEnumTypeCall(ctx.schemaName, typeName, desired, enumType.nativeType));
379
+ handledTypeNames.add(typeName);
380
+ introducedTypeNames.add(typeName);
381
+ continue;
382
+ }
383
+ const diff = determineEnumDiff(existing, desired);
384
+ if (diff.kind === 'unchanged') {
385
+ handledTypeNames.add(typeName);
386
+ continue;
387
+ }
388
+ if (diff.kind === 'add_values') {
389
+ calls.push(new AddEnumValuesCall(ctx.schemaName, typeName, enumType.nativeType, diff.values));
390
+ handledTypeNames.add(typeName);
391
+ continue;
392
+ }
393
+ if (dataAllowed && diff.removedValues.length > 0) {
324
394
  calls.push(
325
395
  new DataTransformCall(
326
- `migrate-${issue.typeName}-values`,
327
- `migrate-${issue.typeName}-values:check`,
328
- `migrate-${issue.typeName}-values:run`,
396
+ `migrate-${typeName}-values`,
397
+ `migrate-${typeName}-values:check`,
398
+ `migrate-${typeName}-values:run`,
329
399
  ),
330
- ...enumRebuildCallRecipe(issue.typeName, ctx),
331
400
  );
332
- } else if (issue.addedValues.length === 0) {
333
- calls.push(...enumRebuildCallRecipe(issue.typeName, ctx));
334
- } else {
335
- const toType = ctx.toContract.storage.types?.[issue.typeName];
336
- if (toType) {
337
- calls.push(
338
- new AddEnumValuesCall(
339
- ctx.schemaName,
340
- issue.typeName,
341
- toType.nativeType,
342
- issue.addedValues,
343
- ),
344
- );
345
- }
346
401
  }
402
+ calls.push(...enumRebuildCallRecipe(typeName, ctx));
403
+ emittedRebuildRecipe = true;
404
+ handledTypeNames.add(typeName);
405
+ rebuiltTypeNames.add(typeName);
347
406
  }
348
407
 
349
- if (matched.length === 0) return { kind: 'no_match' };
350
- return {
351
- kind: 'match',
352
- issues: issues.filter((i) => !matched.includes(i)),
353
- calls,
354
- recipe: true,
355
- };
408
+ // The strategy emits a single `recipe` flag for the entire pass,
409
+ // which routes every emitted call to either the contiguous recipe
410
+ // slot (rebuild path) or the `dep` bucket (introduce / add-values
411
+ // path). A plan that needs both shapes simultaneously cannot be
412
+ // expressed today — the introduced `CreateEnumTypeCall` would land
413
+ // in the recipe slot and any `CreateTableCall` referencing the new
414
+ // enum would fail at runtime with a confusing `type "X" does not
415
+ // exist` error. Surface the unrepresentable case here as a
416
+ // planner-time error so the failure mode is loud, not silent.
417
+ if (introducedTypeNames.size > 0 && rebuiltTypeNames.size > 0) {
418
+ throw new Error(
419
+ `nativeEnumPlanCallStrategy: cannot emit both a brand-new enum and a rebuild on a different enum in the same plan; the single recipe flag cannot route them to different buckets. Introduced: [${[...introducedTypeNames].sort().join(', ')}]; rebuilt: [${[...rebuiltTypeNames].sort().join(', ')}]. Split the strategy or grow the \`match\` return type before this case lands.`,
420
+ );
421
+ }
422
+
423
+ const remaining = issues.filter(
424
+ (issue) =>
425
+ !(
426
+ (issue.kind === 'type_missing' || issue.kind === 'enum_values_changed') &&
427
+ issue.typeName &&
428
+ handledTypeNames.has(issue.typeName)
429
+ ),
430
+ );
431
+
432
+ if (calls.length === 0 && remaining.length === issues.length) {
433
+ return { kind: 'no_match' };
434
+ }
435
+ // `recipe: true` is required for the rebuild path — its
436
+ // `createEnumType(temp) → alterColumnType → dropEnumType(orig) →
437
+ // renameType` mixes `dep`-class and `alter`-class calls that would
438
+ // mis-order if the planner hoisted them into its DDL sequencing
439
+ // buckets. For the type_missing / add_values paths we want the
440
+ // opposite: hoisted into the `dep` bucket so a brand-new
441
+ // `CreateEnumTypeCall` runs *before* the `CreateTableCall` that
442
+ // references it. The two cases never co-occur in the same plan
443
+ // (introducing a new enum type and rebuilding an existing one in
444
+ // one shot would require both buckets — a shape today's interface
445
+ // does not surface; if that combination ever needs to land we'd
446
+ // split this strategy or grow the `match` return type).
447
+ return { kind: 'match', issues: remaining, calls, recipe: emittedRebuildRecipe };
356
448
  };
357
449
 
358
- // ============================================================================
359
- // Walk-schema strategies (absorbed from the legacy planner)
360
- // ============================================================================
450
+ function collectPostgresEnumTypes(
451
+ storageTypes: SqlStorage['types'],
452
+ ): ReadonlyMap<string, PostgresEnumType> {
453
+ const result = new Map<string, PostgresEnumType>();
454
+ for (const [name, instance] of Object.entries(storageTypes ?? {}).sort(([a], [b]) =>
455
+ a.localeCompare(b),
456
+ )) {
457
+ if (instance instanceof PostgresEnumType) {
458
+ result.set(name, instance);
459
+ }
460
+ }
461
+ return result;
462
+ }
361
463
 
362
464
  /**
363
- * Dispatches storage types through their codec's `planTypeOperations` hook.
364
- * Replaces the walk-schema `buildStorageTypeOperations` path: the hook is
365
- * the authoritative source for codec-driven DDL (enum create/rebuild/add-
366
- * value, custom type creation, etc.).
367
- *
368
- * Runs after `enumChangeCallStrategy` so the structured enum path (value
369
- * add, rebuild recipe) gets first pick at `enum_values_changed` issues;
370
- * this strategy then handles remaining `type_missing` / `enum_values_changed`
371
- * issues for types whose hook produced at least one op.
465
+ * Dispatches non-enum codec-typed storage types through their codec's
466
+ * `planTypeOperations` hook (the authoritative source for codec-driven DDL
467
+ * such as custom type creation). Enum dispatch lives in
468
+ * `nativeEnumPlanCallStrategy` and no longer relies on codec hooks.
372
469
  */
373
470
  export const storageTypePlanCallStrategy: CallMigrationStrategy = (issues, ctx) => {
374
471
  const storageTypes = ctx.toContract.storage.types ?? {};
@@ -380,11 +477,16 @@ export const storageTypePlanCallStrategy: CallMigrationStrategy = (issues, ctx)
380
477
  for (const [typeName, typeInstance] of Object.entries(storageTypes).sort(([a], [b]) =>
381
478
  a.localeCompare(b),
382
479
  )) {
383
- const hook = ctx.codecHooks.get(typeInstance.codecId);
480
+ // Enums walk natively in `nativeEnumPlanCallStrategy`; codec-hook
481
+ // dispatch here is reserved for genuinely codec-typed entries
482
+ // (decimal, varchar, pgvector, …).
483
+ if (isPostgresEnumStorageEntry(typeInstance)) continue;
484
+ const codecInstance = typeInstance as StorageTypeInstance;
485
+ const hook = ctx.codecHooks.get(codecInstance.codecId);
384
486
  if (!hook?.planTypeOperations) continue;
385
487
  const planResult = hook.planTypeOperations({
386
488
  typeName,
387
- typeInstance,
489
+ typeInstance: codecInstance,
388
490
  contract: ctx.toContract,
389
491
  schema: ctx.schema,
390
492
  schemaName: ctx.schemaName,
@@ -446,7 +548,10 @@ export const notNullAddColumnCallStrategy: CallMigrationStrategy = (issues, ctx)
446
548
  const schemaLookups = buildSchemaLookupMap(ctx.schema);
447
549
 
448
550
  const mutableCodecHooks = ctx.codecHooks as Map<string, CodecControlHooks>;
449
- const mutableStorageTypes = ctx.storageTypes as Record<string, StorageTypeInstance>;
551
+ const mutableStorageTypes = ctx.storageTypes as Record<
552
+ string,
553
+ StorageTypeInstance | PostgresEnumType
554
+ >;
450
555
 
451
556
  for (const issue of issues) {
452
557
  if (issue.kind !== 'missing_column' || !issue.table || !issue.column) continue;
@@ -597,25 +702,33 @@ function canUseSharedTemporaryDefaultStrategy(options: {
597
702
  * `policy.allowedOperationClasses`:
598
703
  *
599
704
  * - When `'data'` is allowed (`migration plan`), the data-safe strategies
600
- * (`enumChangeCallStrategy`, `notNullBackfillCallStrategy`,
601
- * `typeChangeCallStrategy`, `nullableTighteningCallStrategy`) consume their
602
- * matching issues and emit `DataTransformCall` placeholders or recipe ops.
705
+ * (`notNullBackfillCallStrategy`, `typeChangeCallStrategy`,
706
+ * `nullableTighteningCallStrategy`) and the enum walk
707
+ * (`nativeEnumPlanCallStrategy`) consume their matching issues and emit
708
+ * `DataTransformCall` placeholders or recipe ops.
603
709
  *
604
- * - When `'data'` is not allowed (`db update` / `db init`), each data-safe
605
- * strategy short-circuits to `no_match`, leaving the issue for the
606
- * downstream walk-schema strategies (`storageTypePlanCallStrategy`,
607
- * `notNullAddColumnCallStrategy`) or the `mapIssueToCall` default to handle
608
- * with direct DDL.
710
+ * - When `'data'` is not allowed (`db update` / `db init`), the
711
+ * placeholder-emitting strategies short-circuit to `no_match`, leaving
712
+ * the issue for the downstream walk-schema strategies
713
+ * (`storageTypePlanCallStrategy`, `notNullAddColumnCallStrategy`) or the
714
+ * `mapIssueToCall` default to handle with direct DDL.
715
+ * `nativeEnumPlanCallStrategy` runs in both modes; under `db update` /
716
+ * `db init` it emits the rebuild recipe without the data-transform
717
+ * placeholder so value-removal data loss surfaces as a runtime cast
718
+ * error rather than silent loss.
609
719
  *
610
- * Order matters: data-safe strategies must run before the walk-schema
611
- * strategies on overlapping issue kinds (e.g. `enum_values_changed`,
612
- * `missing_column` for NOT NULL) so they take priority when active.
720
+ * Enum dispatch is unified into a single strategy: the
721
+ * `nativeEnumPlanCallStrategy` decides per-emission whether to emit a
722
+ * rebuild recipe (`recipe: true`, contiguous slot) or hoist the call
723
+ * into the `dep` bucket (`recipe: false`, so a brand-new
724
+ * `CreateEnumTypeCall` runs before any `CreateTableCall` referencing
725
+ * it). Codec-typed entries continue through `storageTypePlanCallStrategy`.
613
726
  */
614
727
  export const postgresPlannerStrategies: readonly CallMigrationStrategy[] = [
615
- enumChangeCallStrategy,
616
728
  notNullBackfillCallStrategy,
617
729
  typeChangeCallStrategy,
618
730
  nullableTighteningCallStrategy,
731
+ nativeEnumPlanCallStrategy,
619
732
  storageTypePlanCallStrategy,
620
733
  notNullAddColumnCallStrategy,
621
734
  ];
@@ -1,4 +1,9 @@
1
- import type { StorageColumn, StorageTypeInstance } from '@prisma-next/sql-contract/types';
1
+ import {
2
+ isPostgresEnumStorageEntry,
3
+ type PostgresEnumStorageEntry,
4
+ type StorageColumn,
5
+ type StorageTypeInstance,
6
+ } from '@prisma-next/sql-contract/types';
2
7
 
3
8
  export type ResolvedColumnTypeMetadata = Pick<
4
9
  StorageColumn,
@@ -7,7 +12,7 @@ export type ResolvedColumnTypeMetadata = Pick<
7
12
 
8
13
  export function resolveColumnTypeMetadata(
9
14
  column: StorageColumn,
10
- storageTypes: Record<string, StorageTypeInstance>,
15
+ storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>,
11
16
  ): ResolvedColumnTypeMetadata {
12
17
  if (!column.typeRef) {
13
18
  return column;
@@ -18,6 +23,19 @@ export function resolveColumnTypeMetadata(
18
23
  return column;
19
24
  }
20
25
 
26
+ if (isPostgresEnumStorageEntry(referencedType)) {
27
+ // Enum types are referenced by name (`quoteIdentifier(nativeType)`),
28
+ // not via parameterised codec expansion. The structural shape
29
+ // carries `codecId` as an enumerable property (mirroring the
30
+ // codec-typed view); `typeParams` is intentionally omitted here so
31
+ // `expandParameterizedTypeSql` does not try to look up a
32
+ // (deliberately absent) `expandNativeType` hook for `pg/enum@*`.
33
+ return {
34
+ codecId: referencedType.codecId,
35
+ nativeType: referencedType.nativeType,
36
+ };
37
+ }
38
+
21
39
  return {
22
40
  codecId: referencedType.codecId,
23
41
  nativeType: referencedType.nativeType,
@@ -19,6 +19,7 @@ import type {
19
19
  } from '@prisma-next/framework-components/control';
20
20
  import { parsePostgresDefault } from '../default-normalizer';
21
21
  import { normalizeSchemaNativeType } from '../native-type-normalizer';
22
+ import { readExistingEnumValues } from './enum-planning';
22
23
  import { planIssues } from './issue-planner';
23
24
  import { TypeScriptRenderablePostgresMigration } from './planner-produced-postgres-migration';
24
25
  import { postgresPlannerStrategies } from './planner-strategies';
@@ -217,6 +218,8 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
217
218
  frameworkComponents: options.frameworkComponents,
218
219
  normalizeDefault: parsePostgresDefault,
219
220
  normalizeNativeType: normalizeSchemaNativeType,
221
+ resolveExistingEnumValues: (schema, enumType) =>
222
+ readExistingEnumValues(schema, enumType.nativeType),
220
223
  };
221
224
  const verifyResult = verifySqlSchema(verifyOptions);
222
225
  return verifyResult.schema.issues;
@@ -22,6 +22,7 @@ import type { Result } from '@prisma-next/utils/result';
22
22
  import { notOk, ok, okVoid } from '@prisma-next/utils/result';
23
23
  import { parsePostgresDefault } from '../default-normalizer';
24
24
  import { normalizeSchemaNativeType } from '../native-type-normalizer';
25
+ import { readExistingEnumValues } from './enum-planning';
25
26
  import type { PostgresPlanTargetDetails } from './planner-target-details';
26
27
  import {
27
28
  buildLedgerInsertStatement,
@@ -187,6 +188,8 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
187
188
  frameworkComponents: options.frameworkComponents,
188
189
  normalizeDefault: parsePostgresDefault,
189
190
  normalizeNativeType: normalizeSchemaNativeType,
191
+ resolveExistingEnumValues: (schema, enumType) =>
192
+ readExistingEnumValues(schema, enumType.nativeType),
190
193
  });
191
194
  if (!schemaVerifyResult.ok) {
192
195
  return runnerFailure('SCHEMA_VERIFY_FAILED', schemaVerifyResult.summary, {
@@ -0,0 +1,70 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+ import {
3
+ SqlContractSerializerBase,
4
+ type SqlEntityHydrationFactory,
5
+ } from '@prisma-next/family-sql/ir';
6
+ import type { AuthoringEntityContext } from '@prisma-next/framework-components/authoring';
7
+ import type { SqlStorage, SqlStorageTypeEntry } from '@prisma-next/sql-contract/types';
8
+ import { postgresAuthoringEntityTypes } from './authoring';
9
+ import { PostgresEnumType } from './postgres-enum-type';
10
+
11
+ /**
12
+ * Build the hydration registry from this target pack's literal
13
+ * `postgresAuthoringEntityTypes`. Extension-pack-contributed entity
14
+ * types do not reach this registry today; the surface is honest for
15
+ * in-tree consumers (Postgres pack only) and the slot stays
16
+ * deserializable because the family-layer validator's
17
+ * `StorageTypeEntrySchema` only admits kinds whose factory the
18
+ * Postgres pack already ships.
19
+ *
20
+ * Future open (F14): lift the registry build to descriptor-composition
21
+ * time, threading the composed `AuthoringContributions.entityTypes`
22
+ * from extension packs, so a real extension pack shipping a
23
+ * round-trip-needing entity type can be deserialized end-to-end.
24
+ * Earned by the first such extension pack in tree.
25
+ */
26
+ function buildPostgresEntityTypeRegistry(): ReadonlyMap<string, SqlEntityHydrationFactory> {
27
+ const ctx: AuthoringEntityContext = { family: 'sql', target: 'postgres' };
28
+ const registry = new Map<string, SqlEntityHydrationFactory>();
29
+ for (const descriptor of Object.values(postgresAuthoringEntityTypes)) {
30
+ if (descriptor.kind !== 'entity') {
31
+ continue;
32
+ }
33
+ if (!('factory' in descriptor.output)) {
34
+ continue;
35
+ }
36
+ const factory = descriptor.output.factory as (
37
+ input: never,
38
+ ctx: AuthoringEntityContext,
39
+ ) => SqlStorageTypeEntry;
40
+ registry.set(descriptor.discriminator, (entry) => {
41
+ if (entry instanceof PostgresEnumType) {
42
+ return entry;
43
+ }
44
+ return factory(entry as never, ctx);
45
+ });
46
+ }
47
+ return registry;
48
+ }
49
+
50
+ /**
51
+ * Postgres target `ContractSerializer` concretion. Inherits the full
52
+ * SQL-family deserialization pipeline (structural validation +
53
+ * hydration walker that materialises the SQL Contract IR class
54
+ * hierarchy from the validated JSON envelope). Polymorphic
55
+ * `storage.types` entries hydrate through the pack contribution registry
56
+ * keyed by each entity type's declared `discriminator` (matching the
57
+ * enumerable `kind` on the persisted JSON envelope).
58
+ *
59
+ * `serializeContract` falls through to the family-base default —
60
+ * Postgres' contract is JSON-clean today (`PostgresEnumType`
61
+ * instances are frozen with enumerable own properties, so
62
+ * `JSON.stringify` produces the canonical envelope shape). Once
63
+ * target-only fields land (e.g. per-target derived storage fields)
64
+ * this is the home for stripping them from the persisted envelope.
65
+ */
66
+ export class PostgresContractSerializer extends SqlContractSerializerBase<Contract<SqlStorage>> {
67
+ constructor() {
68
+ super(buildPostgresEntityTypeRegistry());
69
+ }
70
+ }