@prisma-next/target-postgres 0.16.0-dev.25 → 0.16.0-dev.26

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 (99) hide show
  1. package/dist/{authoring-BCzcFRay.mjs → authoring-DnO1B0N9.mjs} +7 -14
  2. package/dist/authoring-DnO1B0N9.mjs.map +1 -0
  3. package/dist/canonicalize-C7zKHw8m.mjs +59 -0
  4. package/dist/canonicalize-C7zKHw8m.mjs.map +1 -0
  5. package/dist/{canonicalize-D1F4vUnP.d.mts → canonicalize-CQlxSR4D.d.mts} +3 -18
  6. package/dist/canonicalize-CQlxSR4D.d.mts.map +1 -0
  7. package/dist/contract-free.d.mts +1 -1
  8. package/dist/contract-free.d.mts.map +1 -1
  9. package/dist/contract-free.mjs +1 -1
  10. package/dist/{control-policy-B-_34g05.mjs → control-policy-DEwdS3oK.mjs} +23 -24
  11. package/dist/control-policy-DEwdS3oK.mjs.map +1 -0
  12. package/dist/control.mjs +10 -7
  13. package/dist/control.mjs.map +1 -1
  14. package/dist/{ddl-DYEob9xQ.mjs → ddl-BQC6u-L9.mjs} +27 -3
  15. package/dist/{ddl-DYEob9xQ.mjs.map → ddl-BQC6u-L9.mjs.map} +1 -1
  16. package/dist/ddl.d.mts +3 -3
  17. package/dist/ddl.mjs +2 -2
  18. package/dist/{descriptor-meta-D_0ZKcj-.mjs → descriptor-meta-DGlJurgA.mjs} +2 -2
  19. package/dist/{descriptor-meta-D_0ZKcj-.mjs.map → descriptor-meta-DGlJurgA.mjs.map} +1 -1
  20. package/dist/{diff-database-schema-C9H09OD3.mjs → diff-database-schema-qRPwuYBH.mjs} +22 -12
  21. package/dist/diff-database-schema-qRPwuYBH.mjs.map +1 -0
  22. package/dist/diff-database-schema.mjs +1 -1
  23. package/dist/errors.d.mts +4 -2
  24. package/dist/errors.d.mts.map +1 -1
  25. package/dist/errors.mjs +2 -2
  26. package/dist/issue-planner.d.mts +2 -2
  27. package/dist/issue-planner.d.mts.map +1 -1
  28. package/dist/issue-planner.mjs +1 -1
  29. package/dist/migration.d.mts +2 -2
  30. package/dist/migration.mjs +2 -2
  31. package/dist/{nodes-DJC5B8Id.mjs → nodes-04EILM-z.mjs} +65 -2
  32. package/dist/nodes-04EILM-z.mjs.map +1 -0
  33. package/dist/{nodes-CQzM2hR0.d.mts → nodes-DjCsN-uv.d.mts} +69 -4
  34. package/dist/nodes-DjCsN-uv.d.mts.map +1 -0
  35. package/dist/{op-factory-call-DT7kdjVP.d.mts → op-factory-call-0TB5XNij.d.mts} +46 -8
  36. package/dist/op-factory-call-0TB5XNij.d.mts.map +1 -0
  37. package/dist/{op-factory-call-BVYLIojV.mjs → op-factory-call-CIaPDDUc.mjs} +104 -27
  38. package/dist/op-factory-call-CIaPDDUc.mjs.map +1 -0
  39. package/dist/op-factory-call.d.mts +2 -2
  40. package/dist/op-factory-call.mjs +2 -2
  41. package/dist/pack.d.mts.map +1 -1
  42. package/dist/pack.mjs +1 -1
  43. package/dist/{planner-4sj5cJCY.mjs → planner-BhnkIJX-.mjs} +117 -9
  44. package/dist/planner-BhnkIJX-.mjs.map +1 -0
  45. package/dist/{planner-produced-postgres-migration-CcQPDWmX.mjs → planner-produced-postgres-migration-BE3EJIRK.mjs} +2 -2
  46. package/dist/{planner-produced-postgres-migration-CcQPDWmX.mjs.map → planner-produced-postgres-migration-BE3EJIRK.mjs.map} +1 -1
  47. package/dist/{planner-produced-postgres-migration-8QP8eMph.d.mts → planner-produced-postgres-migration-CRAHl6ak.d.mts} +2 -2
  48. package/dist/planner-produced-postgres-migration-CRAHl6ak.d.mts.map +1 -0
  49. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  50. package/dist/planner-produced-postgres-migration.mjs +1 -1
  51. package/dist/{planner-schema-lookup-CiVaAQP-.mjs → planner-schema-lookup-D3Y_OL-C.mjs} +3 -3
  52. package/dist/planner-schema-lookup-D3Y_OL-C.mjs.map +1 -0
  53. package/dist/planner-schema-lookup.mjs +1 -1
  54. package/dist/planner.d.mts +21 -1
  55. package/dist/planner.d.mts.map +1 -1
  56. package/dist/planner.mjs +2 -2
  57. package/dist/{postgres-contract-view-B8yXVWYe.mjs → postgres-contract-view-BeIEyZfD.mjs} +2 -2
  58. package/dist/{postgres-contract-view-B8yXVWYe.mjs.map → postgres-contract-view-BeIEyZfD.mjs.map} +1 -1
  59. package/dist/{postgres-migration-GIOABsAa.mjs → postgres-migration--BzvpA92.mjs} +7 -4
  60. package/dist/postgres-migration--BzvpA92.mjs.map +1 -0
  61. package/dist/{postgres-migration-DLIvzHjX.d.mts → postgres-migration-BXelnqHO.d.mts} +15 -10
  62. package/dist/postgres-migration-BXelnqHO.d.mts.map +1 -0
  63. package/dist/rls-canonicalize.d.mts +2 -26
  64. package/dist/rls-canonicalize.mjs +2 -2
  65. package/dist/runtime.mjs +1 -1
  66. package/package.json +20 -20
  67. package/src/contract-free/ddl.ts +47 -0
  68. package/src/core/authoring.ts +10 -22
  69. package/src/core/ddl/nodes.ts +104 -0
  70. package/src/core/migrations/contract-to-postgres-database-schema-node.ts +24 -13
  71. package/src/core/migrations/issue-planner.ts +40 -13
  72. package/src/core/migrations/op-factory-call.ts +100 -9
  73. package/src/core/migrations/operations/indexes.ts +80 -35
  74. package/src/core/migrations/planner-schema-lookup.ts +2 -2
  75. package/src/core/migrations/planner.ts +155 -5
  76. package/src/core/migrations/postgres-migration.ts +26 -8
  77. package/src/core/psl-infer/infer-psl-contract.ts +8 -1
  78. package/src/core/rls/canonicalize.ts +4 -21
  79. package/src/exports/ddl.ts +4 -0
  80. package/src/exports/errors.ts +1 -1
  81. package/src/exports/op-factory-call.ts +1 -0
  82. package/src/exports/rls-canonicalize.ts +0 -6
  83. package/dist/authoring-BCzcFRay.mjs.map +0 -1
  84. package/dist/canonicalize-D1F4vUnP.d.mts.map +0 -1
  85. package/dist/control-policy-B-_34g05.mjs.map +0 -1
  86. package/dist/diff-database-schema-C9H09OD3.mjs.map +0 -1
  87. package/dist/nodes-CQzM2hR0.d.mts.map +0 -1
  88. package/dist/nodes-DJC5B8Id.mjs.map +0 -1
  89. package/dist/op-factory-call-BVYLIojV.mjs.map +0 -1
  90. package/dist/op-factory-call-DT7kdjVP.d.mts.map +0 -1
  91. package/dist/planner-4sj5cJCY.mjs.map +0 -1
  92. package/dist/planner-produced-postgres-migration-8QP8eMph.d.mts.map +0 -1
  93. package/dist/planner-schema-lookup-CiVaAQP-.mjs.map +0 -1
  94. package/dist/postgres-migration-DLIvzHjX.d.mts.map +0 -1
  95. package/dist/postgres-migration-GIOABsAa.mjs.map +0 -1
  96. package/dist/rls-canonicalize.d.mts.map +0 -1
  97. package/dist/wire-name-DbQ9-2RC.mjs +0 -104
  98. package/dist/wire-name-DbQ9-2RC.mjs.map +0 -1
  99. package/src/core/rls/wire-name.ts +0 -33
@@ -74,7 +74,13 @@ import {
74
74
  dropConstraint,
75
75
  } from './operations/constraints';
76
76
  import { createExtension } from './operations/dependencies';
77
- import { createIndex, dropIndex } from './operations/indexes';
77
+ import {
78
+ type CreateIndexElements,
79
+ type CreateIndexExtras,
80
+ createIndex,
81
+ dropIndex,
82
+ renameIndex,
83
+ } from './operations/indexes';
78
84
  import { createNativeEnumType, dropNativeEnumType } from './operations/native-enum-types';
79
85
  import {
80
86
  createRlsPolicy,
@@ -1137,27 +1143,35 @@ export class CreateIndexCall extends PostgresOpFactoryCallNode {
1137
1143
  readonly schemaName: string;
1138
1144
  readonly tableName: string;
1139
1145
  readonly indexName: string;
1140
- readonly columns: readonly string[];
1146
+ /** Column tuple when the index is column-based; absent for expression indexes. */
1147
+ readonly columns: readonly string[] | undefined;
1148
+ /** Opaque whole element list when the index is expression-based. */
1149
+ readonly expression: string | undefined;
1141
1150
  // Named indexType (not typeName): `locationForCall` in issue-planner.ts reads
1142
1151
  // a call's `typeName` as a CREATE TYPE target location, which an index is not.
1143
1152
  readonly indexType: string | undefined;
1144
1153
  readonly options: Record<string, unknown> | undefined;
1154
+ readonly where: string | undefined;
1155
+ readonly unique: boolean;
1145
1156
  readonly label: string;
1146
1157
 
1147
1158
  constructor(
1148
1159
  schemaName: string,
1149
1160
  tableName: string,
1150
1161
  indexName: string,
1151
- columns: readonly string[],
1152
- extras?: { readonly type?: string; readonly options?: Record<string, unknown> },
1162
+ elements: CreateIndexElements,
1163
+ extras?: CreateIndexExtras,
1153
1164
  ) {
1154
1165
  super();
1155
1166
  this.schemaName = schemaName;
1156
1167
  this.tableName = tableName;
1157
1168
  this.indexName = indexName;
1158
- this.columns = columns;
1169
+ this.columns = 'columns' in elements ? elements.columns : undefined;
1170
+ this.expression = 'expression' in elements ? elements.expression : undefined;
1159
1171
  this.indexType = extras?.type;
1160
1172
  this.options = extras?.options;
1173
+ this.where = extras?.where;
1174
+ this.unique = extras?.unique === true;
1161
1175
  this.label = `Create index "${indexName}" on "${tableName}"`;
1162
1176
  this.freeze();
1163
1177
  }
@@ -1170,14 +1184,23 @@ export class CreateIndexCall extends PostgresOpFactoryCallNode {
1170
1184
  { meta: { factory: 'CreateIndexCall' } },
1171
1185
  );
1172
1186
  }
1173
- const extras: { type?: string; options?: Record<string, unknown> } = {};
1187
+ const extras: {
1188
+ type?: string;
1189
+ options?: Record<string, unknown>;
1190
+ where?: string;
1191
+ unique?: boolean;
1192
+ } = {};
1174
1193
  if (this.indexType !== undefined) extras.type = this.indexType;
1175
1194
  if (this.options !== undefined) extras.options = this.options;
1195
+ if (this.where !== undefined) extras.where = this.where;
1196
+ if (this.unique) extras.unique = true;
1176
1197
  return createIndex(
1177
1198
  this.schemaName,
1178
1199
  this.tableName,
1179
1200
  this.indexName,
1180
- this.columns,
1201
+ this.columns !== undefined
1202
+ ? { columns: this.columns }
1203
+ : { expression: this.expression ?? '' },
1181
1204
  lowerer,
1182
1205
  extras,
1183
1206
  );
@@ -1190,11 +1213,22 @@ export class CreateIndexCall extends PostgresOpFactoryCallNode {
1190
1213
  }
1191
1214
  opts.push(`table: ${jsonToTsSource(this.tableName)}`);
1192
1215
  opts.push(`index: ${jsonToTsSource(this.indexName)}`);
1193
- opts.push(`columns: ${jsonToTsSource(this.columns)}`);
1194
- if (this.indexType !== undefined || this.options !== undefined) {
1216
+ if (this.columns !== undefined) {
1217
+ opts.push(`columns: ${jsonToTsSource(this.columns)}`);
1218
+ } else {
1219
+ opts.push(`expression: ${jsonToTsSource(this.expression ?? '')}`);
1220
+ }
1221
+ if (
1222
+ this.indexType !== undefined ||
1223
+ this.options !== undefined ||
1224
+ this.where !== undefined ||
1225
+ this.unique
1226
+ ) {
1195
1227
  const extrasParts: string[] = [];
1196
1228
  if (this.indexType !== undefined) extrasParts.push(`type: ${jsonToTsSource(this.indexType)}`);
1197
1229
  if (this.options !== undefined) extrasParts.push(`options: ${jsonToTsSource(this.options)}`);
1230
+ if (this.where !== undefined) extrasParts.push(`where: ${jsonToTsSource(this.where)}`);
1231
+ if (this.unique) extrasParts.push('unique: true');
1198
1232
  opts.push(`extras: { ${extrasParts.join(', ')} }`);
1199
1233
  }
1200
1234
  return `this.createIndex({ ${opts.join(', ')} })`;
@@ -1205,6 +1239,62 @@ export class CreateIndexCall extends PostgresOpFactoryCallNode {
1205
1239
  }
1206
1240
  }
1207
1241
 
1242
+ export class RenameIndexCall extends PostgresOpFactoryCallNode {
1243
+ readonly factoryName = 'renameIndex' as const;
1244
+ // `widening` is chosen so the rename plans under every allowance set except
1245
+ // additive-only init — a rename is neither additive-creation nor
1246
+ // destructive, and the class vocabulary has no neutral middle class. It is
1247
+ // NOT that a rename widens anything; this is the accepted typology tradeoff.
1248
+ readonly operationClass = 'widening' as const;
1249
+ readonly schemaName: string;
1250
+ readonly tableName: string;
1251
+ readonly oldIndexName: string;
1252
+ readonly newIndexName: string;
1253
+ readonly label: string;
1254
+
1255
+ constructor(schemaName: string, tableName: string, oldIndexName: string, newIndexName: string) {
1256
+ super();
1257
+ this.schemaName = schemaName;
1258
+ this.tableName = tableName;
1259
+ this.oldIndexName = oldIndexName;
1260
+ this.newIndexName = newIndexName;
1261
+ this.label = `Rename index "${oldIndexName}" to "${newIndexName}" on "${tableName}"`;
1262
+ this.freeze();
1263
+ }
1264
+
1265
+ async toOp(lowerer?: ExecuteRequestLowerer): Promise<Op> {
1266
+ if (lowerer === undefined) {
1267
+ throw postgresError(
1268
+ 'MIGRATION.POSTGRES_CONTROL_STACK_MISSING',
1269
+ `RenameIndexCall.toOp: a lowerer is required on the Postgres planner path (index "${this.oldIndexName}" on table "${this.tableName}"). Pass the control adapter to createPostgresMigrationPlanner.`,
1270
+ { meta: { factory: 'RenameIndexCall' } },
1271
+ );
1272
+ }
1273
+ return renameIndex(
1274
+ this.schemaName,
1275
+ this.tableName,
1276
+ this.oldIndexName,
1277
+ this.newIndexName,
1278
+ lowerer,
1279
+ );
1280
+ }
1281
+
1282
+ renderTypeScript(): string {
1283
+ const opts: string[] = [];
1284
+ if (this.schemaName !== UNBOUND_NAMESPACE_ID) {
1285
+ opts.push(`schema: ${jsonToTsSource(this.schemaName)}`);
1286
+ }
1287
+ opts.push(`table: ${jsonToTsSource(this.tableName)}`);
1288
+ opts.push(`from: ${jsonToTsSource(this.oldIndexName)}`);
1289
+ opts.push(`to: ${jsonToTsSource(this.newIndexName)}`);
1290
+ return `this.renameIndex({ ${opts.join(', ')} })`;
1291
+ }
1292
+
1293
+ override importRequirements(): readonly ImportRequirement[] {
1294
+ return [];
1295
+ }
1296
+ }
1297
+
1208
1298
  export class DropIndexCall extends PostgresOpFactoryCallNode {
1209
1299
  readonly factoryName = 'dropIndex' as const;
1210
1300
  readonly operationClass = 'destructive' as const;
@@ -1824,6 +1914,7 @@ export type PostgresOpFactoryCall =
1824
1914
  | AddCheckConstraintCall
1825
1915
  | DropCheckConstraintCall
1826
1916
  | CreateIndexCall
1917
+ | RenameIndexCall
1827
1918
  | DropIndexCall
1828
1919
  | DropConstraintCall
1829
1920
  | RawSqlCall
@@ -1,8 +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 { postgresError } from '../../errors';
4
- import { escapeLiteral, quoteIdentifier } from '../../sql-utils';
5
- 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';
6
9
  import { type Op, step, targetDetails } from './shared';
7
10
 
8
11
  type CheckStep = { sql: string; params?: readonly unknown[] };
@@ -18,42 +21,51 @@ async function indexExistsSteps(
18
21
  return { present, absent };
19
22
  }
20
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
+
21
29
  export interface CreateIndexExtras {
22
30
  readonly type?: string;
23
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;
24
39
  }
25
40
 
26
- function renderIndexOptionValue(key: string, value: unknown): string {
27
- if (typeof value === 'string') return `'${escapeLiteral(value)}'`;
28
- if (typeof value === 'number' && Number.isFinite(value)) return String(value);
29
- if (typeof value === 'boolean') return value ? 'true' : 'false';
30
- throw postgresError(
31
- 'CONTRACT.INDEX_INVALID',
32
- `Index option "${key}" must be a string, finite number, or boolean; got ${typeof value}`,
33
- { meta: { key, valueType: typeof value } },
34
- );
35
- }
36
-
37
- function renderIndexOptions(options: Record<string, unknown>): string {
38
- return Object.entries(options)
39
- .map(([key, value]) => `${quoteIdentifier(key)} = ${renderIndexOptionValue(key, value)}`)
40
- .join(', ');
41
- }
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 };
42
49
 
43
50
  export async function createIndex(
44
51
  schemaName: string,
45
52
  tableName: string,
46
53
  indexName: string,
47
- columns: readonly string[],
54
+ elements: CreateIndexElements,
48
55
  lowerer: ExecuteRequestLowerer,
49
56
  extras?: CreateIndexExtras,
50
57
  ): Promise<Op> {
51
- const qualified = qualifyTableName(schemaName, tableName);
52
- const columnList = columns.map(quoteIdentifier).join(', ');
53
- const using = extras?.type ? ` USING ${quoteIdentifier(extras.type)}` : '';
54
- const options = extras?.options;
55
- const withClause =
56
- 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);
57
69
  const { present, absent } = await indexExistsSteps(lowerer, schemaName, indexName);
58
70
  return {
59
71
  id: `index.${tableName}.${indexName}`,
@@ -61,22 +73,57 @@ export async function createIndex(
61
73
  operationClass: 'additive',
62
74
  target: targetDetails('index', indexName, schemaName, tableName),
63
75
  precheck: [step(`ensure index "${indexName}" does not exist`, absent.sql, absent.params)],
64
- execute: [
65
- step(
66
- `create index "${indexName}"`,
67
- `CREATE INDEX ${quoteIdentifier(indexName)} ON ${qualified}${using} (${columnList})${withClause}`,
68
- ),
69
- ],
76
+ execute: [step(`create index "${indexName}"`, execute.sql, execute.params)],
70
77
  postcheck: [step(`verify index "${indexName}" exists`, present.sql, present.params)],
71
78
  };
72
79
  }
73
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
+
74
119
  export async function dropIndex(
75
120
  schemaName: string,
76
121
  tableName: string,
77
122
  indexName: string,
78
123
  lowerer: ExecuteRequestLowerer,
79
124
  ): Promise<Op> {
125
+ const ddlNode = dropIndexDdl({ schema: ddlSchemaOf(schemaName), name: indexName });
126
+ const execute = await lowerer.lowerToExecuteRequest(ddlNode);
80
127
  const { present, absent } = await indexExistsSteps(lowerer, schemaName, indexName);
81
128
  return {
82
129
  id: `dropIndex.${tableName}.${indexName}`,
@@ -84,9 +131,7 @@ export async function dropIndex(
84
131
  operationClass: 'destructive',
85
132
  target: targetDetails('index', indexName, schemaName, tableName),
86
133
  precheck: [step(`ensure index "${indexName}" exists`, present.sql, present.params)],
87
- execute: [
88
- step(`drop index "${indexName}"`, `DROP INDEX ${qualifyTableName(schemaName, indexName)}`),
89
- ],
134
+ execute: [step(`drop index "${indexName}"`, execute.sql, execute.params)],
90
135
  postcheck: [step(`verify index "${indexName}" does not exist`, absent.sql, absent.params)],
91
136
  };
92
137
  }
@@ -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';
@@ -236,6 +238,18 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
236
238
  });
237
239
  const schemaIssues = [...namespaceIssues, ...gated];
238
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
+
239
253
  const codecHooks = extractCodecControlHooks(options.frameworkComponents);
240
254
  const storageTypes = options.contract.storage.types ?? {};
241
255
  // The strategy layer reads the live schema by bare table name for existence
@@ -253,7 +267,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
253
267
  // user-visible message survives even when the planner would have failed
254
268
  // to model the subject's live shape.
255
269
  const issuePartition = partitionIssuesByControlPolicy({
256
- issues: schemaIssues,
270
+ issues: plannableIssues,
257
271
  contract: options.contract,
258
272
  resolveControlPolicySubject: (issue) =>
259
273
  resolvePostgresNodeIssueControlPolicySubject(issue, options.contract),
@@ -282,6 +296,14 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
282
296
  return plannerFailure(result.failure);
283
297
  }
284
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
+
285
307
  const schemaDiffCalls = this.planPostgresSchemaDiff(options, policyDiffIssues);
286
308
  const schemaDiffPartition = partitionCallsByControlPolicy({
287
309
  calls: schemaDiffCalls,
@@ -314,9 +336,15 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
314
336
  resolvePostgresCallControlPolicySubject(call, options.contract),
315
337
  resolveFactoryName: (call) => call.factoryName,
316
338
  });
317
- 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
+ ];
318
345
  const warnings: SqlPlannerConflict[] = [
319
346
  ...issuePartition.suppressions,
347
+ ...indexRenamePartition.suppressions,
320
348
  ...schemaDiffPartition.suppressions,
321
349
  ...fieldEventPartition.suppressions,
322
350
  ].map((record) => renderPostgresSuppression(record, options.contract));
@@ -337,6 +365,128 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
337
365
  });
338
366
  }
339
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
+
340
490
  /**
341
491
  * Maps the RLS policy presence findings of the one combined tree diff
342
492
  * (`buildPostgresPlanDiff`, already ownership-filtered) into
@@ -412,7 +562,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
412
562
  if (allowsWidening) {
413
563
  const extrasByGroup = new Map<string, PolicyFinding[]>();
414
564
  for (const finding of extra) {
415
- const parsed = parseRlsPolicyWireName(finding.node.name);
565
+ const parsed = parseWireName(finding.node.name);
416
566
  if (parsed === undefined) continue;
417
567
  const key = pairingKey(finding, parsed.hash);
418
568
  const group = extrasByGroup.get(key) ?? [];
@@ -427,7 +577,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
427
577
  a.node.name < b.node.name ? -1 : a.node.name > b.node.name ? 1 : 0,
428
578
  );
429
579
  for (const missingFinding of sortedMissing) {
430
- const parsed = parseRlsPolicyWireName(missingFinding.node.name);
580
+ const parsed = parseWireName(missingFinding.node.name);
431
581
  if (parsed === undefined) continue;
432
582
  const candidates = extrasByGroup.get(pairingKey(missingFinding, parsed.hash));
433
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;
@@ -748,8 +748,15 @@ function buildModel(
748
748
  }
749
749
 
750
750
  for (const index of table.indexes) {
751
+ // Expression indexes (no column tuple) and partial indexes (a where
752
+ // predicate) are not emitted yet — no authoring surface can carry their
753
+ // bodies, so adopting them name-only would fail its own verify (the
754
+ // exact-mode body compare). Their inference lands when the authoring
755
+ // surfaces for those bodies exist.
756
+ if (index.columns === undefined || index.where !== undefined) continue;
751
757
  if (!index.unique) {
752
- const indexFieldNames = index.columns.map((columnName) =>
758
+ const columns = index.columns;
759
+ const indexFieldNames = columns.map((columnName) =>
753
760
  resolveColumnFieldName(fieldNamesByTable, table.name, columnName),
754
761
  );
755
762
  modelAttributes.push(