@prisma-next/target-postgres 0.16.0-dev.3 → 0.16.0-dev.31

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 (166) hide show
  1. package/README.md +1 -1
  2. package/dist/{authoring-34L18kA1.mjs → authoring-DnO1B0N9.mjs} +29 -18
  3. package/dist/authoring-DnO1B0N9.mjs.map +1 -0
  4. package/dist/canonicalize-C7zKHw8m.mjs +59 -0
  5. package/dist/canonicalize-C7zKHw8m.mjs.map +1 -0
  6. package/dist/{canonicalize-D1F4vUnP.d.mts → canonicalize-CQlxSR4D.d.mts} +3 -18
  7. package/dist/canonicalize-CQlxSR4D.d.mts.map +1 -0
  8. package/dist/{codec-types-CQ07nOz-.d.mts → codec-types-DdoPc2dy.d.mts} +3 -2
  9. package/dist/codec-types-DdoPc2dy.d.mts.map +1 -0
  10. package/dist/codec-types.d.mts +1 -1
  11. package/dist/{codecs-G4DTY0F_.d.mts → codecs-CGE5dh94.d.mts} +7 -2
  12. package/dist/codecs-CGE5dh94.d.mts.map +1 -0
  13. package/dist/{codecs-TCsGgW-Y.mjs → codecs-Deand7sy.mjs} +84 -26
  14. package/dist/codecs-Deand7sy.mjs.map +1 -0
  15. package/dist/codecs.d.mts +2 -2
  16. package/dist/codecs.mjs +2 -2
  17. package/dist/contract-free.d.mts +1 -1
  18. package/dist/contract-free.d.mts.map +1 -1
  19. package/dist/contract-free.mjs +2 -2
  20. package/dist/{control-policy-CZKdMztu.mjs → control-policy-DEwdS3oK.mjs} +29 -29
  21. package/dist/control-policy-DEwdS3oK.mjs.map +1 -0
  22. package/dist/control.mjs +91 -239
  23. package/dist/control.mjs.map +1 -1
  24. package/dist/data-transform-BOWpliq8.mjs.map +1 -1
  25. package/dist/{ddl-DHXKzQ99.mjs → ddl-BQC6u-L9.mjs} +28 -4
  26. package/dist/{ddl-DHXKzQ99.mjs.map → ddl-BQC6u-L9.mjs.map} +1 -1
  27. package/dist/ddl.d.mts +3 -3
  28. package/dist/ddl.mjs +2 -2
  29. package/dist/{descriptor-meta-qubYaxQA.mjs → descriptor-meta-DGlJurgA.mjs} +4 -4
  30. package/dist/{descriptor-meta-qubYaxQA.mjs.map → descriptor-meta-DGlJurgA.mjs.map} +1 -1
  31. package/dist/{diff-database-schema-C-hnJ5mr.mjs → diff-database-schema-qRPwuYBH.mjs} +37 -15
  32. package/dist/diff-database-schema-qRPwuYBH.mjs.map +1 -0
  33. package/dist/diff-database-schema.mjs +1 -1
  34. package/dist/{errors-ByDS0xJL.mjs → errors-DBSNx7EJ.mjs} +6 -2
  35. package/dist/errors-DBSNx7EJ.mjs.map +1 -0
  36. package/dist/errors.d.mts +4 -1
  37. package/dist/errors.d.mts.map +1 -1
  38. package/dist/errors.mjs +2 -2
  39. package/dist/issue-planner.d.mts +2 -2
  40. package/dist/issue-planner.d.mts.map +1 -1
  41. package/dist/issue-planner.mjs +1 -1
  42. package/dist/migration.d.mts +2 -2
  43. package/dist/migration.mjs +2 -2
  44. package/dist/{nodes-DJC5B8Id.mjs → nodes-04EILM-z.mjs} +65 -2
  45. package/dist/nodes-04EILM-z.mjs.map +1 -0
  46. package/dist/{nodes-CQzM2hR0.d.mts → nodes-DjCsN-uv.d.mts} +69 -4
  47. package/dist/nodes-DjCsN-uv.d.mts.map +1 -0
  48. package/dist/{op-factory-call-DT7kdjVP.d.mts → op-factory-call-0TB5XNij.d.mts} +46 -8
  49. package/dist/op-factory-call-0TB5XNij.d.mts.map +1 -0
  50. package/dist/{op-factory-call-DuRj7WXX.mjs → op-factory-call-CIaPDDUc.mjs} +147 -64
  51. package/dist/op-factory-call-CIaPDDUc.mjs.map +1 -0
  52. package/dist/op-factory-call.d.mts +2 -2
  53. package/dist/op-factory-call.mjs +2 -2
  54. package/dist/pack.d.mts +2 -2
  55. package/dist/pack.d.mts.map +1 -1
  56. package/dist/pack.mjs +1 -1
  57. package/dist/{planner-DPGxFcXo.mjs → planner-BhnkIJX-.mjs} +121 -13
  58. package/dist/planner-BhnkIJX-.mjs.map +1 -0
  59. package/dist/{planner-ddl-builders-BKlmGDJl.mjs → planner-ddl-builders-CVo3L7Nd.mjs} +12 -7
  60. package/dist/planner-ddl-builders-CVo3L7Nd.mjs.map +1 -0
  61. package/dist/planner-ddl-builders.d.mts.map +1 -1
  62. package/dist/planner-ddl-builders.mjs +1 -1
  63. package/dist/{planner-produced-postgres-migration-CFDpqWWE.mjs → planner-produced-postgres-migration-BE3EJIRK.mjs} +9 -6
  64. package/dist/planner-produced-postgres-migration-BE3EJIRK.mjs.map +1 -0
  65. package/dist/{planner-produced-postgres-migration-C-dwGdb2.d.mts → planner-produced-postgres-migration-CRAHl6ak.d.mts} +3 -3
  66. package/dist/planner-produced-postgres-migration-CRAHl6ak.d.mts.map +1 -0
  67. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  68. package/dist/planner-produced-postgres-migration.mjs +1 -1
  69. package/dist/{planner-schema-lookup-CiVaAQP-.mjs → planner-schema-lookup-D3Y_OL-C.mjs} +3 -3
  70. package/dist/planner-schema-lookup-D3Y_OL-C.mjs.map +1 -0
  71. package/dist/planner-schema-lookup.mjs +1 -1
  72. package/dist/{planner-sql-checks-BuB877wH.mjs → planner-sql-checks-CmiQ6Nei.mjs} +3 -3
  73. package/dist/{planner-sql-checks-BuB877wH.mjs.map → planner-sql-checks-CmiQ6Nei.mjs.map} +1 -1
  74. package/dist/planner-sql-checks.mjs +1 -1
  75. package/dist/planner.d.mts +27 -1
  76. package/dist/planner.d.mts.map +1 -1
  77. package/dist/planner.mjs +2 -2
  78. package/dist/{postgres-contract-view-DebHMqGR.mjs → postgres-contract-view-BeIEyZfD.mjs} +3 -3
  79. package/dist/{postgres-contract-view-DebHMqGR.mjs.map → postgres-contract-view-BeIEyZfD.mjs.map} +1 -1
  80. package/dist/{postgres-migration-C_OPFhIF.mjs → postgres-migration--BzvpA92.mjs} +9 -6
  81. package/dist/postgres-migration--BzvpA92.mjs.map +1 -0
  82. package/dist/{postgres-migration-DLIvzHjX.d.mts → postgres-migration-BXelnqHO.d.mts} +15 -10
  83. package/dist/postgres-migration-BXelnqHO.d.mts.map +1 -0
  84. package/dist/{postgres-schema-CewLYHB7.mjs → postgres-schema-CTplQZDK.mjs} +2 -2
  85. package/dist/{postgres-schema-CewLYHB7.mjs.map → postgres-schema-CTplQZDK.mjs.map} +1 -1
  86. package/dist/{render-ops-BREh1kHe.mjs → render-ops-TcY9XEkz.mjs} +7 -2
  87. package/dist/render-ops-TcY9XEkz.mjs.map +1 -0
  88. package/dist/render-ops.d.mts.map +1 -1
  89. package/dist/render-ops.mjs +1 -1
  90. package/dist/{render-typescript-7yqILcwr.mjs → render-typescript-Ta7_6CGf.mjs} +20 -8
  91. package/dist/render-typescript-Ta7_6CGf.mjs.map +1 -0
  92. package/dist/render-typescript.d.mts +2 -0
  93. package/dist/render-typescript.d.mts.map +1 -1
  94. package/dist/render-typescript.mjs +1 -1
  95. package/dist/{resolve-ddl-schema-D0wi_P9Q.mjs → resolve-ddl-schema-BAsJEKW8.mjs} +2 -2
  96. package/dist/{resolve-ddl-schema-D0wi_P9Q.mjs.map → resolve-ddl-schema-BAsJEKW8.mjs.map} +1 -1
  97. package/dist/rls-canonicalize.d.mts +2 -26
  98. package/dist/rls-canonicalize.mjs +2 -2
  99. package/dist/runtime.mjs +1 -1
  100. package/dist/schema-ir-annotations.mjs +1 -1
  101. package/dist/{sql-utils-SU4FDvIV.mjs → sql-utils-B2lzkKfP.mjs} +22 -19
  102. package/dist/sql-utils-B2lzkKfP.mjs.map +1 -0
  103. package/dist/sql-utils.d.mts +4 -9
  104. package/dist/sql-utils.d.mts.map +1 -1
  105. package/dist/sql-utils.mjs +2 -2
  106. package/dist/types.mjs +1 -1
  107. package/package.json +20 -20
  108. package/src/contract-free/ddl.ts +47 -0
  109. package/src/core/authoring.ts +35 -26
  110. package/src/core/codec-helpers.ts +69 -16
  111. package/src/core/codec-type-map.ts +2 -0
  112. package/src/core/codecs.ts +29 -14
  113. package/src/core/ddl/nodes.ts +104 -0
  114. package/src/core/errors.ts +30 -0
  115. package/src/core/migrations/column-ddl-rendering.ts +2 -1
  116. package/src/core/migrations/contract-to-postgres-database-schema-node.ts +38 -14
  117. package/src/core/migrations/issue-planner.ts +40 -13
  118. package/src/core/migrations/op-factory-call.ts +189 -44
  119. package/src/core/migrations/operations/constraints.ts +3 -2
  120. package/src/core/migrations/operations/data-transform.ts +1 -1
  121. package/src/core/migrations/operations/indexes.ts +80 -32
  122. package/src/core/migrations/operations/rls.ts +4 -1
  123. package/src/core/migrations/planner-ddl-builders.ts +16 -6
  124. package/src/core/migrations/planner-produced-postgres-migration.ts +8 -1
  125. package/src/core/migrations/planner-schema-lookup.ts +2 -2
  126. package/src/core/migrations/planner.ts +163 -5
  127. package/src/core/migrations/postgres-migration.ts +26 -8
  128. package/src/core/migrations/render-ops.ts +4 -1
  129. package/src/core/migrations/render-typescript.ts +20 -8
  130. package/src/core/migrations/runner.ts +7 -3
  131. package/src/core/psl-infer/infer-psl-contract.ts +36 -185
  132. package/src/core/psl-infer/postgres-type-map.ts +43 -65
  133. package/src/core/rls/canonicalize.ts +4 -21
  134. package/src/core/sql-utils.ts +16 -27
  135. package/src/exports/codecs.ts +2 -0
  136. package/src/exports/ddl.ts +4 -0
  137. package/src/exports/errors.ts +1 -1
  138. package/src/exports/op-factory-call.ts +1 -0
  139. package/src/exports/rls-canonicalize.ts +0 -6
  140. package/src/exports/sql-utils.ts +0 -1
  141. package/dist/authoring-34L18kA1.mjs.map +0 -1
  142. package/dist/canonicalize-D1F4vUnP.d.mts.map +0 -1
  143. package/dist/codec-types-CQ07nOz-.d.mts.map +0 -1
  144. package/dist/codecs-G4DTY0F_.d.mts.map +0 -1
  145. package/dist/codecs-TCsGgW-Y.mjs.map +0 -1
  146. package/dist/control-policy-CZKdMztu.mjs.map +0 -1
  147. package/dist/diff-database-schema-C-hnJ5mr.mjs.map +0 -1
  148. package/dist/errors-ByDS0xJL.mjs.map +0 -1
  149. package/dist/nodes-CQzM2hR0.d.mts.map +0 -1
  150. package/dist/nodes-DJC5B8Id.mjs.map +0 -1
  151. package/dist/op-factory-call-DT7kdjVP.d.mts.map +0 -1
  152. package/dist/op-factory-call-DuRj7WXX.mjs.map +0 -1
  153. package/dist/planner-DPGxFcXo.mjs.map +0 -1
  154. package/dist/planner-ddl-builders-BKlmGDJl.mjs.map +0 -1
  155. package/dist/planner-produced-postgres-migration-C-dwGdb2.d.mts.map +0 -1
  156. package/dist/planner-produced-postgres-migration-CFDpqWWE.mjs.map +0 -1
  157. package/dist/planner-schema-lookup-CiVaAQP-.mjs.map +0 -1
  158. package/dist/postgres-migration-C_OPFhIF.mjs.map +0 -1
  159. package/dist/postgres-migration-DLIvzHjX.d.mts.map +0 -1
  160. package/dist/render-ops-BREh1kHe.mjs.map +0 -1
  161. package/dist/render-typescript-7yqILcwr.mjs.map +0 -1
  162. package/dist/rls-canonicalize.d.mts.map +0 -1
  163. package/dist/sql-utils-SU4FDvIV.mjs.map +0 -1
  164. package/dist/wire-name-DbQ9-2RC.mjs +0 -104
  165. package/dist/wire-name-DbQ9-2RC.mjs.map +0 -1
  166. package/src/core/rls/wire-name.ts +0 -33
@@ -1,7 +1,11 @@
1
1
  import type { ExecuteRequestLowerer } from '@prisma-next/family-sql/control-adapter';
2
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
2
3
  import { indexExistsAst } from '../../../contract-free/checks';
3
- import { escapeLiteral, quoteIdentifier } from '../../sql-utils';
4
- import { qualifyTableName } from '../planner-sql-checks';
4
+ import {
5
+ alterIndexRename as alterIndexRenameDdl,
6
+ createIndex as createIndexDdl,
7
+ dropIndex as dropIndexDdl,
8
+ } from '../../../contract-free/ddl';
5
9
  import { type Op, step, targetDetails } from './shared';
6
10
 
7
11
  type CheckStep = { sql: string; params?: readonly unknown[] };
@@ -17,40 +21,51 @@ async function indexExistsSteps(
17
21
  return { present, absent };
18
22
  }
19
23
 
24
+ /** An unbound-namespace object renders unqualified: the DDL node takes no schema. */
25
+ function ddlSchemaOf(schemaName: string): string | undefined {
26
+ return schemaName === UNBOUND_NAMESPACE_ID ? undefined : schemaName;
27
+ }
28
+
20
29
  export interface CreateIndexExtras {
21
30
  readonly type?: string;
22
31
  readonly options?: Record<string, unknown>;
32
+ /**
33
+ * Partial-index predicate (WHERE body, without the keyword). Inserted
34
+ * verbatim, never quoted or escaped — the same opaque-SQL stance as RLS
35
+ * policy predicates.
36
+ */
37
+ readonly where?: string;
38
+ readonly unique?: boolean;
23
39
  }
24
40
 
25
- function renderIndexOptionValue(key: string, value: unknown): string {
26
- if (typeof value === 'string') return `'${escapeLiteral(value)}'`;
27
- if (typeof value === 'number' && Number.isFinite(value)) return String(value);
28
- if (typeof value === 'boolean') return value ? 'true' : 'false';
29
- throw new Error(
30
- `Index option "${key}" must be a string, finite number, or boolean; got ${typeof value}`,
31
- );
32
- }
33
-
34
- function renderIndexOptions(options: Record<string, unknown>): string {
35
- return Object.entries(options)
36
- .map(([key, value]) => `${quoteIdentifier(key)} = ${renderIndexOptionValue(key, value)}`)
37
- .join(', ');
38
- }
41
+ /**
42
+ * The element list between the parens of CREATE INDEX: either a column
43
+ * tuple (each identifier quoted) or one opaque expression string covering
44
+ * the entire list, inserted verbatim.
45
+ */
46
+ export type CreateIndexElements =
47
+ | { readonly columns: readonly string[] }
48
+ | { readonly expression: string };
39
49
 
40
50
  export async function createIndex(
41
51
  schemaName: string,
42
52
  tableName: string,
43
53
  indexName: string,
44
- columns: readonly string[],
54
+ elements: CreateIndexElements,
45
55
  lowerer: ExecuteRequestLowerer,
46
56
  extras?: CreateIndexExtras,
47
57
  ): Promise<Op> {
48
- const qualified = qualifyTableName(schemaName, tableName);
49
- const columnList = columns.map(quoteIdentifier).join(', ');
50
- const using = extras?.type ? ` USING ${quoteIdentifier(extras.type)}` : '';
51
- const options = extras?.options;
52
- const withClause =
53
- options && Object.keys(options).length > 0 ? ` WITH (${renderIndexOptions(options)})` : '';
58
+ const ddlNode = createIndexDdl({
59
+ schema: ddlSchemaOf(schemaName),
60
+ table: tableName,
61
+ name: indexName,
62
+ unique: extras?.unique === true,
63
+ elements,
64
+ type: extras?.type,
65
+ options: extras?.options,
66
+ where: extras?.where,
67
+ });
68
+ const execute = await lowerer.lowerToExecuteRequest(ddlNode);
54
69
  const { present, absent } = await indexExistsSteps(lowerer, schemaName, indexName);
55
70
  return {
56
71
  id: `index.${tableName}.${indexName}`,
@@ -58,22 +73,57 @@ export async function createIndex(
58
73
  operationClass: 'additive',
59
74
  target: targetDetails('index', indexName, schemaName, tableName),
60
75
  precheck: [step(`ensure index "${indexName}" does not exist`, absent.sql, absent.params)],
61
- execute: [
62
- step(
63
- `create index "${indexName}"`,
64
- `CREATE INDEX ${quoteIdentifier(indexName)} ON ${qualified}${using} (${columnList})${withClause}`,
65
- ),
66
- ],
76
+ execute: [step(`create index "${indexName}"`, execute.sql, execute.params)],
67
77
  postcheck: [step(`verify index "${indexName}" exists`, present.sql, present.params)],
68
78
  };
69
79
  }
70
80
 
81
+ /**
82
+ * `ALTER INDEX … RENAME TO`. `widening` for the same typology reason as the
83
+ * RLS policy rename: a rename is neither additive creation nor destructive,
84
+ * and the class vocabulary has no neutral middle class — it is NOT that a
85
+ * rename widens anything.
86
+ */
87
+ export async function renameIndex(
88
+ schemaName: string,
89
+ tableName: string,
90
+ fromName: string,
91
+ toName: string,
92
+ lowerer: ExecuteRequestLowerer,
93
+ ): Promise<Op> {
94
+ const fromChecks = indexExistsAst(schemaName, fromName);
95
+ const toChecks = indexExistsAst(schemaName, toName);
96
+ const fromPresent = await lowerer.lowerToExecuteRequest(fromChecks.indexPresent());
97
+ const toAbsent = await lowerer.lowerToExecuteRequest(toChecks.indexAbsent());
98
+ const toPresent = await lowerer.lowerToExecuteRequest(toChecks.indexPresent());
99
+ const ddlNode = alterIndexRenameDdl({
100
+ schema: ddlSchemaOf(schemaName),
101
+ from: fromName,
102
+ to: toName,
103
+ });
104
+ const execute = await lowerer.lowerToExecuteRequest(ddlNode);
105
+ return {
106
+ id: `index.${schemaName}.${tableName}.${fromName}.rename`,
107
+ label: `Rename index "${fromName}" to "${toName}" on "${tableName}"`,
108
+ operationClass: 'widening',
109
+ target: targetDetails('index', toName, schemaName, tableName),
110
+ precheck: [
111
+ step(`ensure index "${fromName}" exists`, fromPresent.sql, fromPresent.params),
112
+ step(`ensure index "${toName}" does not exist`, toAbsent.sql, toAbsent.params),
113
+ ],
114
+ execute: [step(`rename index "${fromName}" to "${toName}"`, execute.sql, execute.params)],
115
+ postcheck: [step(`verify index "${toName}" exists`, toPresent.sql, toPresent.params)],
116
+ };
117
+ }
118
+
71
119
  export async function dropIndex(
72
120
  schemaName: string,
73
121
  tableName: string,
74
122
  indexName: string,
75
123
  lowerer: ExecuteRequestLowerer,
76
124
  ): Promise<Op> {
125
+ const ddlNode = dropIndexDdl({ schema: ddlSchemaOf(schemaName), name: indexName });
126
+ const execute = await lowerer.lowerToExecuteRequest(ddlNode);
77
127
  const { present, absent } = await indexExistsSteps(lowerer, schemaName, indexName);
78
128
  return {
79
129
  id: `dropIndex.${tableName}.${indexName}`,
@@ -81,9 +131,7 @@ export async function dropIndex(
81
131
  operationClass: 'destructive',
82
132
  target: targetDetails('index', indexName, schemaName, tableName),
83
133
  precheck: [step(`ensure index "${indexName}" exists`, present.sql, present.params)],
84
- execute: [
85
- step(`drop index "${indexName}"`, `DROP INDEX ${qualifyTableName(schemaName, indexName)}`),
86
- ],
134
+ execute: [step(`drop index "${indexName}"`, execute.sql, execute.params)],
87
135
  postcheck: [step(`verify index "${indexName}" does not exist`, absent.sql, absent.params)],
88
136
  };
89
137
  }
@@ -7,6 +7,7 @@ import {
7
7
  disableRowLevelSecurity as disableRowLevelSecurityDdl,
8
8
  dropPolicy,
9
9
  } from '../../../contract-free/ddl';
10
+ import { postgresError } from '../../errors';
10
11
  import type { PostgresRlsPolicy } from '../../postgres-rls-policy';
11
12
  import { qualifyTableName } from '../planner-sql-checks';
12
13
  import { type Op, step, targetDetails } from './shared';
@@ -15,8 +16,10 @@ const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
15
16
 
16
17
  function validateRoleName(role: string): string {
17
18
  if (!PLAIN_IDENTIFIER.test(role)) {
18
- throw new Error(
19
+ throw postgresError(
20
+ 'CONTRACT.ROLE_INVALID',
19
21
  `Invalid role name ${JSON.stringify(role)}: role names must be plain SQL identifiers matching ^[A-Za-z_][A-Za-z0-9_$]*$`,
22
+ { meta: { role, reason: 'not-a-plain-identifier' } },
20
23
  );
21
24
  }
22
25
  return role;
@@ -1,6 +1,8 @@
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 { isPgEnumParams } from '../codecs';
5
+ import { postgresError } from '../errors';
4
6
  import { escapeLiteral, quoteIdentifier, quoteQualifiedName } from '../sql-utils';
5
7
  import type { PostgresColumnDefault } from '../types';
6
8
  import { resolveColumnTypeMetadata } from './planner-type-resolution';
@@ -14,9 +16,11 @@ const SAFE_NATIVE_TYPE_PATTERN = /^[a-zA-Z][a-zA-Z0-9_ ]*(\[\])?$/;
14
16
 
15
17
  function assertSafeNativeType(nativeType: string): void {
16
18
  if (!SAFE_NATIVE_TYPE_PATTERN.test(nativeType)) {
17
- throw new Error(
19
+ throw postgresError(
20
+ 'CONTRACT.NATIVE_TYPE_INVALID',
18
21
  `Unsafe native type name in contract: "${nativeType}". ` +
19
22
  'Native type names must match /^[a-zA-Z][a-zA-Z0-9_ ]*(\\[\\])?$/',
23
+ { meta: { nativeType } },
20
24
  );
21
25
  }
22
26
  }
@@ -28,9 +32,11 @@ function assertSafeNativeType(nativeType: string): void {
28
32
  */
29
33
  function assertSafeDefaultExpression(expression: string): void {
30
34
  if (expression.includes(';') || /--|\/\*|\$\$|\bSELECT\b/i.test(expression)) {
31
- throw new Error(
35
+ throw postgresError(
36
+ 'CONTRACT.DEFAULT_INVALID',
32
37
  `Unsafe default expression in contract: "${expression}". ` +
33
38
  'Default expressions must not contain semicolons, SQL comment tokens, dollar-quoting, or subqueries.',
39
+ { meta: { expression } },
34
40
  );
35
41
  }
36
42
  }
@@ -70,7 +76,7 @@ export function buildColumnTypeSql(
70
76
  // as its quoted, schema-qualified type-name identifier. DDL-render only — the
71
77
  // verify comparison value (`resolvedNativeType`) stays the bare name the
72
78
  // family expander produces, matching introspection.
73
- if (typeof resolved.typeParams?.['typeName'] === 'string') {
79
+ if (isPgEnumParams(resolved.typeParams)) {
74
80
  const quoted = quoteQualifiedName(resolved.nativeType);
75
81
  return column.many ? `${quoted}[]` : quoted;
76
82
  }
@@ -98,9 +104,11 @@ function expandParameterizedTypeSql(
98
104
  }
99
105
 
100
106
  if (!column.codecId) {
101
- throw new Error(
107
+ throw postgresError(
108
+ 'CONTRACT.CODEC_DESCRIPTOR_MISSING',
102
109
  `Column declares typeParams for nativeType "${column.nativeType}" but has no codecId. ` +
103
110
  'Ensure the column is associated with a codec.',
111
+ { meta: { nativeType: column.nativeType } },
104
112
  );
105
113
  }
106
114
 
@@ -109,10 +117,12 @@ function expandParameterizedTypeSql(
109
117
  if (hooks?.planTypeOperations) {
110
118
  return null;
111
119
  }
112
- throw new Error(
120
+ throw postgresError(
121
+ 'CONTRACT.PACK_CONTRIBUTION_INVALID',
113
122
  `Column declares typeParams for nativeType "${column.nativeType}" ` +
114
123
  `but no expandNativeType hook is registered for codecId "${column.codecId}". ` +
115
- 'Ensure the extension providing this codec is included in extensionPacks.',
124
+ 'Ensure the extension providing this codec is included in extensions.',
125
+ { meta: { codecId: column.codecId, nativeType: column.nativeType } },
116
126
  );
117
127
  }
118
128
 
@@ -42,6 +42,7 @@ export class TypeScriptRenderablePostgresMigration
42
42
  readonly #calls: readonly OpFactoryCall[];
43
43
  readonly #meta: MigrationMeta;
44
44
  readonly #spaceId: string;
45
+ readonly #snapshotsImportPath: string;
45
46
  readonly #lowerer: ExecuteRequestLowerer | undefined;
46
47
  #operationsCache:
47
48
  | readonly (
@@ -54,12 +55,14 @@ export class TypeScriptRenderablePostgresMigration
54
55
  calls: readonly OpFactoryCall[],
55
56
  meta: MigrationMeta,
56
57
  spaceId: string,
58
+ snapshotsImportPath: string,
57
59
  lowerer?: ExecuteRequestLowerer,
58
60
  ) {
59
61
  super();
60
62
  this.#calls = calls;
61
63
  this.#meta = meta;
62
64
  this.#spaceId = spaceId;
65
+ this.#snapshotsImportPath = snapshotsImportPath;
63
66
  this.#lowerer = lowerer;
64
67
  }
65
68
 
@@ -85,6 +88,10 @@ export class TypeScriptRenderablePostgresMigration
85
88
  }
86
89
 
87
90
  renderTypeScript(): string {
88
- return renderCallsToTypeScript(this.#calls, { from: this.#meta.from, to: this.#meta.to });
91
+ return renderCallsToTypeScript(this.#calls, {
92
+ from: this.#meta.from,
93
+ to: this.#meta.to,
94
+ snapshotsImportPath: this.#snapshotsImportPath,
95
+ });
89
96
  }
90
97
  }
@@ -23,9 +23,9 @@ export function buildSchemaLookupMap(schema: SqlSchemaIR): ReadonlyMap<string, S
23
23
 
24
24
  function buildSchemaTableLookup(table: SqlSchemaIR['tables'][string]): SchemaTableLookup {
25
25
  const uniqueKeys = new Set(table.uniques.map((u) => u.columns.join(',')));
26
- const indexKeys = new Set(table.indexes.map((i) => i.columns.join(',')));
26
+ const indexKeys = new Set(table.indexes.map((i) => (i.columns ?? []).join(',')));
27
27
  const uniqueIndexKeys = new Set(
28
- table.indexes.filter((i) => i.unique).map((i) => i.columns.join(',')),
28
+ table.indexes.filter((i) => i.unique).map((i) => (i.columns ?? []).join(',')),
29
29
  );
30
30
  const fkKeys = new Set<string>();
31
31
  for (const fk of table.foreignKeys) {
@@ -24,11 +24,12 @@ import type {
24
24
  import { issueOutcome } from '@prisma-next/framework-components/control';
25
25
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
26
26
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
27
+ import { parseWireName } from '@prisma-next/sql-schema-ir/naming';
27
28
  import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
29
+ import { SqlIndexIR } from '@prisma-next/sql-schema-ir/types';
28
30
  import { blindCast } from '@prisma-next/utils/casts';
29
31
  import { ifDefined } from '@prisma-next/utils/defined';
30
32
  import { PostgresRlsPolicy } from '../postgres-rls-policy';
31
- import { parseRlsPolicyWireName } from '../rls/wire-name';
32
33
  import { postgresNodeStorageCoordinate } from '../schema-ir/node-storage-coordinate';
33
34
  import { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
34
35
  import { PostgresPolicySchemaNode } from '../schema-ir/postgres-policy-schema-node';
@@ -46,6 +47,7 @@ import type { PostgresOpFactoryCall } from './op-factory-call';
46
47
  import {
47
48
  CreatePostgresRlsPolicyCall,
48
49
  DropPostgresRlsPolicyCall,
50
+ RenameIndexCall,
49
51
  RenamePostgresRlsPolicyCall,
50
52
  } from './op-factory-call';
51
53
  import { TypeScriptRenderablePostgresMigration } from './planner-produced-postgres-migration';
@@ -141,6 +143,12 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
141
143
  * {@link SqlMigrationPlannerPlanOptions.ownership}.
142
144
  */
143
145
  readonly ownership?: SchemaOwnership;
146
+ /**
147
+ * POSIX-relative path from the migration package dir to
148
+ * `migrations/snapshots` — see
149
+ * {@link SqlMigrationPlannerPlanOptions.snapshotsImportPath}.
150
+ */
151
+ readonly snapshotsImportPath: string;
144
152
  }): PostgresPlanResult {
145
153
  return this.planSql(options as SqlMigrationPlannerPlanOptions);
146
154
  }
@@ -156,6 +164,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
156
164
  to: context.toHash,
157
165
  },
158
166
  spaceId,
167
+ context.snapshotsImportPath,
159
168
  this.#lowerer,
160
169
  );
161
170
  }
@@ -229,6 +238,18 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
229
238
  });
230
239
  const schemaIssues = [...namespaceIssues, ...gated];
231
240
 
241
+ // Index rename post-pass (the policy pass's structure, generalized): a
242
+ // `not-found` and a `not-expected` index that are one rename collapse
243
+ // into a single widening `ALTER INDEX … RENAME TO` before the per-issue
244
+ // mapping turns them into create + drop. Consumed issues never reach
245
+ // `planIssues`; the rename calls go through the same call-side
246
+ // control-policy partition the policy ops use.
247
+ const indexRenames = this.pairIndexRenames(options, schemaIssues);
248
+ const plannableIssues =
249
+ indexRenames.consumed.size === 0
250
+ ? schemaIssues
251
+ : schemaIssues.filter((issue) => !indexRenames.consumed.has(issue));
252
+
232
253
  const codecHooks = extractCodecControlHooks(options.frameworkComponents);
233
254
  const storageTypes = options.contract.storage.types ?? {};
234
255
  // The strategy layer reads the live schema by bare table name for existence
@@ -246,7 +267,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
246
267
  // user-visible message survives even when the planner would have failed
247
268
  // to model the subject's live shape.
248
269
  const issuePartition = partitionIssuesByControlPolicy({
249
- issues: schemaIssues,
270
+ issues: plannableIssues,
250
271
  contract: options.contract,
251
272
  resolveControlPolicySubject: (issue) =>
252
273
  resolvePostgresNodeIssueControlPolicySubject(issue, options.contract),
@@ -275,6 +296,14 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
275
296
  return plannerFailure(result.failure);
276
297
  }
277
298
 
299
+ const indexRenamePartition = partitionCallsByControlPolicy({
300
+ calls: indexRenames.calls,
301
+ contract: options.contract,
302
+ resolveControlPolicySubject: (call) =>
303
+ resolvePostgresCallControlPolicySubject(call, options.contract),
304
+ resolveFactoryName: (call) => call.factoryName,
305
+ });
306
+
278
307
  const schemaDiffCalls = this.planPostgresSchemaDiff(options, policyDiffIssues);
279
308
  const schemaDiffPartition = partitionCallsByControlPolicy({
280
309
  calls: schemaDiffCalls,
@@ -307,9 +336,15 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
307
336
  resolvePostgresCallControlPolicySubject(call, options.contract),
308
337
  resolveFactoryName: (call) => call.factoryName,
309
338
  });
310
- const calls = [...result.value.calls, ...schemaDiffPartition.kept, ...fieldEventPartition.kept];
339
+ const calls = [
340
+ ...result.value.calls,
341
+ ...indexRenamePartition.kept,
342
+ ...schemaDiffPartition.kept,
343
+ ...fieldEventPartition.kept,
344
+ ];
311
345
  const warnings: SqlPlannerConflict[] = [
312
346
  ...issuePartition.suppressions,
347
+ ...indexRenamePartition.suppressions,
313
348
  ...schemaDiffPartition.suppressions,
314
349
  ...fieldEventPartition.suppressions,
315
350
  ].map((record) => renderPostgresSuppression(record, options.contract));
@@ -323,12 +358,135 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
323
358
  to: options.contract.storage.storageHash,
324
359
  },
325
360
  options.spaceId,
361
+ options.snapshotsImportPath,
326
362
  this.#lowerer,
327
363
  ),
328
364
  ...(warnings.length > 0 ? { warnings: Object.freeze(warnings) } : {}),
329
365
  });
330
366
  }
331
367
 
368
+ /**
369
+ * Rename post-pass for indexes, per `(schema, table)`, widening-only,
370
+ * deterministic by sorted names — the same structure as the policy pass
371
+ * below (which stays untouched: policies pair by hash only).
372
+ *
373
+ * Phase 1 — hash pairing: extras whose live names parse as wire names,
374
+ * grouped by `(schema, table, hash)`; missing nodes iterated in sorted-name
375
+ * order consume the sorted-name-first candidate — a prefix-only rename.
376
+ *
377
+ * Phase 2 — content pairing: the remaining managed-missing nodes
378
+ * (`prefix` defined) against the remaining extras of any name shape,
379
+ * paired iff content-equal (columns ordered-strict both-defined-or-
380
+ * both-undefined, `unique`/`type` strict, `options` loose, bodies
381
+ * byte-equal) — exact→managed adoption convergence.
382
+ *
383
+ * Leftovers proceed as create/drop exactly as before; without the
384
+ * widening allowance the pass is skipped and pairing degrades to the
385
+ * additive half, like the policy pass.
386
+ */
387
+ private pairIndexRenames(
388
+ options: PlannerOptionsWithComponents,
389
+ issues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[],
390
+ ): {
391
+ readonly calls: readonly RenameIndexCall[];
392
+ readonly consumed: ReadonlySet<SchemaDiffIssue<SqlSchemaDiffNode>>;
393
+ } {
394
+ const consumed = new Set<SchemaDiffIssue<SqlSchemaDiffNode>>();
395
+ const calls: RenameIndexCall[] = [];
396
+ if (!options.policy.allowedOperationClasses.includes('widening')) {
397
+ return { calls, consumed };
398
+ }
399
+
400
+ interface IndexFinding {
401
+ readonly issue: SchemaDiffIssue<SqlSchemaDiffNode>;
402
+ readonly node: SqlIndexIR;
403
+ readonly ddlSchema: string;
404
+ readonly tableName: string;
405
+ }
406
+ const missing: IndexFinding[] = [];
407
+ const extra: IndexFinding[] = [];
408
+ for (const issue of issues) {
409
+ const node = issueNode(issue);
410
+ if (node === undefined || !SqlIndexIR.is(node)) continue;
411
+ const ddlSchema = issue.path[1];
412
+ const tableName = issue.path[2];
413
+ if (ddlSchema === undefined || tableName === undefined) continue;
414
+ if (issueOutcome(issue) === 'not-found') {
415
+ missing.push({ issue, node, ddlSchema, tableName });
416
+ } else if (issueOutcome(issue) === 'not-expected') {
417
+ extra.push({ issue, node, ddlSchema, tableName });
418
+ }
419
+ }
420
+ if (missing.length === 0 || extra.length === 0) {
421
+ return { calls, consumed };
422
+ }
423
+
424
+ const byName = (a: IndexFinding, b: IndexFinding): number =>
425
+ a.node.name < b.node.name ? -1 : a.node.name > b.node.name ? 1 : 0;
426
+ // DDL emission must stay unqualified for the unbound namespace, exactly
427
+ // like the per-issue mapping's `emissionSchemaName`.
428
+ const emissionSchema = (ddlSchema: string): string =>
429
+ resolveNamespaceIdForDdlSchema(options.contract, ddlSchema) === UNBOUND_NAMESPACE_ID
430
+ ? UNBOUND_NAMESPACE_ID
431
+ : ddlSchema;
432
+ const pairingKey = (finding: IndexFinding, hash: string): string =>
433
+ JSON.stringify([finding.ddlSchema, finding.tableName, hash]);
434
+ const rename = (missingFinding: IndexFinding, candidate: IndexFinding): void => {
435
+ consumed.add(missingFinding.issue);
436
+ consumed.add(candidate.issue);
437
+ calls.push(
438
+ new RenameIndexCall(
439
+ emissionSchema(missingFinding.ddlSchema),
440
+ missingFinding.tableName,
441
+ candidate.node.name,
442
+ missingFinding.node.name,
443
+ ),
444
+ );
445
+ };
446
+
447
+ const sortedMissing = [...missing].sort(byName);
448
+
449
+ const extrasByHash = new Map<string, IndexFinding[]>();
450
+ for (const finding of extra) {
451
+ const parsed = parseWireName(finding.node.name);
452
+ if (parsed === undefined) continue;
453
+ const key = pairingKey(finding, parsed.hash);
454
+ const group = extrasByHash.get(key) ?? [];
455
+ group.push(finding);
456
+ extrasByHash.set(key, group);
457
+ }
458
+ for (const group of extrasByHash.values()) {
459
+ group.sort(byName);
460
+ }
461
+ for (const missingFinding of sortedMissing) {
462
+ const parsed = parseWireName(missingFinding.node.name);
463
+ if (parsed === undefined) continue;
464
+ const candidate = extrasByHash.get(pairingKey(missingFinding, parsed.hash))?.shift();
465
+ if (candidate === undefined) continue;
466
+ rename(missingFinding, candidate);
467
+ }
468
+
469
+ const sortedExtras = [...extra].sort(byName);
470
+ for (const missingFinding of sortedMissing) {
471
+ if (consumed.has(missingFinding.issue)) continue;
472
+ if (missingFinding.node.prefix === undefined) continue;
473
+ const candidate = sortedExtras.find(
474
+ (extraFinding) =>
475
+ !consumed.has(extraFinding.issue) &&
476
+ extraFinding.ddlSchema === missingFinding.ddlSchema &&
477
+ extraFinding.tableName === missingFinding.tableName &&
478
+ missingFinding.node.contentEquals(extraFinding.node, {
479
+ columnPresence: 'matching',
480
+ bodies: 'verbatim',
481
+ }),
482
+ );
483
+ if (candidate === undefined) continue;
484
+ rename(missingFinding, candidate);
485
+ }
486
+
487
+ return { calls, consumed };
488
+ }
489
+
332
490
  /**
333
491
  * Maps the RLS policy presence findings of the one combined tree diff
334
492
  * (`buildPostgresPlanDiff`, already ownership-filtered) into
@@ -404,7 +562,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
404
562
  if (allowsWidening) {
405
563
  const extrasByGroup = new Map<string, PolicyFinding[]>();
406
564
  for (const finding of extra) {
407
- const parsed = parseRlsPolicyWireName(finding.node.name);
565
+ const parsed = parseWireName(finding.node.name);
408
566
  if (parsed === undefined) continue;
409
567
  const key = pairingKey(finding, parsed.hash);
410
568
  const group = extrasByGroup.get(key) ?? [];
@@ -419,7 +577,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
419
577
  a.node.name < b.node.name ? -1 : a.node.name > b.node.name ? 1 : 0,
420
578
  );
421
579
  for (const missingFinding of sortedMissing) {
422
- const parsed = parseRlsPolicyWireName(missingFinding.node.name);
580
+ const parsed = parseWireName(missingFinding.node.name);
423
581
  if (parsed === undefined) continue;
424
582
  const candidates = extrasByGroup.get(pairingKey(missingFinding, parsed.hash));
425
583
  const candidate = candidates?.shift();
@@ -34,6 +34,7 @@ import {
34
34
  DropPostgresRlsPolicyCall,
35
35
  DropTableCall,
36
36
  EnableRowLevelSecurityCall,
37
+ RenameIndexCall,
37
38
  RenamePostgresRlsPolicyCall,
38
39
  SetDefaultCall,
39
40
  SetNotNullCall,
@@ -392,22 +393,39 @@ export abstract class PostgresMigration<
392
393
  );
393
394
  }
394
395
 
395
- protected createIndex(options: {
396
- readonly schema: string;
397
- readonly table: string;
398
- readonly index: string;
399
- readonly columns: readonly string[];
400
- readonly extras?: CreateIndexExtras;
401
- }): Promise<SqlMigrationPlanOperation<PostgresPlanTargetDetails>> {
396
+ protected createIndex(
397
+ options: {
398
+ readonly schema: string;
399
+ readonly table: string;
400
+ readonly index: string;
401
+ readonly extras?: CreateIndexExtras;
402
+ } & (
403
+ | { readonly columns: readonly string[]; readonly expression?: never }
404
+ | { readonly expression: string; readonly columns?: never }
405
+ ),
406
+ ): Promise<SqlMigrationPlanOperation<PostgresPlanTargetDetails>> {
402
407
  return new CreateIndexCall(
403
408
  options.schema,
404
409
  options.table,
405
410
  options.index,
406
- options.columns,
411
+ options.columns !== undefined
412
+ ? { columns: options.columns }
413
+ : { expression: options.expression },
407
414
  options.extras,
408
415
  ).toOp(this.controlAdapterFor('createIndex'));
409
416
  }
410
417
 
418
+ protected renameIndex(options: {
419
+ readonly schema: string;
420
+ readonly table: string;
421
+ readonly from: string;
422
+ readonly to: string;
423
+ }): Promise<SqlMigrationPlanOperation<PostgresPlanTargetDetails>> {
424
+ return new RenameIndexCall(options.schema, options.table, options.from, options.to).toOp(
425
+ this.controlAdapterFor('renameIndex'),
426
+ );
427
+ }
428
+
411
429
  protected dropIndex(options: {
412
430
  readonly schema: string;
413
431
  readonly table: string;
@@ -6,6 +6,7 @@ import type {
6
6
  } from '@prisma-next/framework-components/control';
7
7
  import { blindCast } from '@prisma-next/utils/casts';
8
8
  import { isThenable } from '@prisma-next/utils/promise';
9
+ import { postgresError } from '../errors';
9
10
  import type { PostgresPlanTargetDetails } from './planner-target-details';
10
11
 
11
12
  type Op = SqlMigrationPlanOperation<PostgresPlanTargetDetails>;
@@ -23,8 +24,10 @@ function assertPostgresOp(op: MigrationPlanOperation, callFactoryName: string):
23
24
  'op.target is present on concrete SqlMigrationPlanOperation but absent on the framework MigrationPlanOperation base'
24
25
  >(op).target?.id;
25
26
  if (targetId !== 'postgres') {
26
- throw new Error(
27
+ throw postgresError(
28
+ 'MIGRATION.TARGET_MISMATCH',
27
29
  `renderOps: expected postgres op but got target.id="${String(targetId)}" for op.id="${op.id}" (factoryName="${callFactoryName}"). An OpFactoryCall produced an op for a different target on the postgres planner path; check the call's target binding.`,
30
+ { meta: { opId: op.id, targetId: String(targetId), factoryName: callFactoryName } },
28
31
  );
29
32
  }
30
33
  }