@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.
- package/dist/{authoring-BCzcFRay.mjs → authoring-DnO1B0N9.mjs} +7 -14
- package/dist/authoring-DnO1B0N9.mjs.map +1 -0
- package/dist/canonicalize-C7zKHw8m.mjs +59 -0
- package/dist/canonicalize-C7zKHw8m.mjs.map +1 -0
- package/dist/{canonicalize-D1F4vUnP.d.mts → canonicalize-CQlxSR4D.d.mts} +3 -18
- package/dist/canonicalize-CQlxSR4D.d.mts.map +1 -0
- package/dist/contract-free.d.mts +1 -1
- package/dist/contract-free.d.mts.map +1 -1
- package/dist/contract-free.mjs +1 -1
- package/dist/{control-policy-B-_34g05.mjs → control-policy-DEwdS3oK.mjs} +23 -24
- package/dist/control-policy-DEwdS3oK.mjs.map +1 -0
- package/dist/control.mjs +10 -7
- package/dist/control.mjs.map +1 -1
- package/dist/{ddl-DYEob9xQ.mjs → ddl-BQC6u-L9.mjs} +27 -3
- package/dist/{ddl-DYEob9xQ.mjs.map → ddl-BQC6u-L9.mjs.map} +1 -1
- package/dist/ddl.d.mts +3 -3
- package/dist/ddl.mjs +2 -2
- package/dist/{descriptor-meta-D_0ZKcj-.mjs → descriptor-meta-DGlJurgA.mjs} +2 -2
- package/dist/{descriptor-meta-D_0ZKcj-.mjs.map → descriptor-meta-DGlJurgA.mjs.map} +1 -1
- package/dist/{diff-database-schema-C9H09OD3.mjs → diff-database-schema-qRPwuYBH.mjs} +22 -12
- package/dist/diff-database-schema-qRPwuYBH.mjs.map +1 -0
- package/dist/diff-database-schema.mjs +1 -1
- package/dist/errors.d.mts +4 -2
- package/dist/errors.d.mts.map +1 -1
- package/dist/errors.mjs +2 -2
- package/dist/issue-planner.d.mts +2 -2
- package/dist/issue-planner.d.mts.map +1 -1
- package/dist/issue-planner.mjs +1 -1
- package/dist/migration.d.mts +2 -2
- package/dist/migration.mjs +2 -2
- package/dist/{nodes-DJC5B8Id.mjs → nodes-04EILM-z.mjs} +65 -2
- package/dist/nodes-04EILM-z.mjs.map +1 -0
- package/dist/{nodes-CQzM2hR0.d.mts → nodes-DjCsN-uv.d.mts} +69 -4
- package/dist/nodes-DjCsN-uv.d.mts.map +1 -0
- package/dist/{op-factory-call-DT7kdjVP.d.mts → op-factory-call-0TB5XNij.d.mts} +46 -8
- package/dist/op-factory-call-0TB5XNij.d.mts.map +1 -0
- package/dist/{op-factory-call-BVYLIojV.mjs → op-factory-call-CIaPDDUc.mjs} +104 -27
- package/dist/op-factory-call-CIaPDDUc.mjs.map +1 -0
- package/dist/op-factory-call.d.mts +2 -2
- package/dist/op-factory-call.mjs +2 -2
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/{planner-4sj5cJCY.mjs → planner-BhnkIJX-.mjs} +117 -9
- package/dist/planner-BhnkIJX-.mjs.map +1 -0
- package/dist/{planner-produced-postgres-migration-CcQPDWmX.mjs → planner-produced-postgres-migration-BE3EJIRK.mjs} +2 -2
- package/dist/{planner-produced-postgres-migration-CcQPDWmX.mjs.map → planner-produced-postgres-migration-BE3EJIRK.mjs.map} +1 -1
- package/dist/{planner-produced-postgres-migration-8QP8eMph.d.mts → planner-produced-postgres-migration-CRAHl6ak.d.mts} +2 -2
- package/dist/planner-produced-postgres-migration-CRAHl6ak.d.mts.map +1 -0
- package/dist/planner-produced-postgres-migration.d.mts +1 -1
- package/dist/planner-produced-postgres-migration.mjs +1 -1
- package/dist/{planner-schema-lookup-CiVaAQP-.mjs → planner-schema-lookup-D3Y_OL-C.mjs} +3 -3
- package/dist/planner-schema-lookup-D3Y_OL-C.mjs.map +1 -0
- package/dist/planner-schema-lookup.mjs +1 -1
- package/dist/planner.d.mts +21 -1
- package/dist/planner.d.mts.map +1 -1
- package/dist/planner.mjs +2 -2
- package/dist/{postgres-contract-view-B8yXVWYe.mjs → postgres-contract-view-BeIEyZfD.mjs} +2 -2
- package/dist/{postgres-contract-view-B8yXVWYe.mjs.map → postgres-contract-view-BeIEyZfD.mjs.map} +1 -1
- package/dist/{postgres-migration-GIOABsAa.mjs → postgres-migration--BzvpA92.mjs} +7 -4
- package/dist/postgres-migration--BzvpA92.mjs.map +1 -0
- package/dist/{postgres-migration-DLIvzHjX.d.mts → postgres-migration-BXelnqHO.d.mts} +15 -10
- package/dist/postgres-migration-BXelnqHO.d.mts.map +1 -0
- package/dist/rls-canonicalize.d.mts +2 -26
- package/dist/rls-canonicalize.mjs +2 -2
- package/dist/runtime.mjs +1 -1
- package/package.json +20 -20
- package/src/contract-free/ddl.ts +47 -0
- package/src/core/authoring.ts +10 -22
- package/src/core/ddl/nodes.ts +104 -0
- package/src/core/migrations/contract-to-postgres-database-schema-node.ts +24 -13
- package/src/core/migrations/issue-planner.ts +40 -13
- package/src/core/migrations/op-factory-call.ts +100 -9
- package/src/core/migrations/operations/indexes.ts +80 -35
- package/src/core/migrations/planner-schema-lookup.ts +2 -2
- package/src/core/migrations/planner.ts +155 -5
- package/src/core/migrations/postgres-migration.ts +26 -8
- package/src/core/psl-infer/infer-psl-contract.ts +8 -1
- package/src/core/rls/canonicalize.ts +4 -21
- package/src/exports/ddl.ts +4 -0
- package/src/exports/errors.ts +1 -1
- package/src/exports/op-factory-call.ts +1 -0
- package/src/exports/rls-canonicalize.ts +0 -6
- package/dist/authoring-BCzcFRay.mjs.map +0 -1
- package/dist/canonicalize-D1F4vUnP.d.mts.map +0 -1
- package/dist/control-policy-B-_34g05.mjs.map +0 -1
- package/dist/diff-database-schema-C9H09OD3.mjs.map +0 -1
- package/dist/nodes-CQzM2hR0.d.mts.map +0 -1
- package/dist/nodes-DJC5B8Id.mjs.map +0 -1
- package/dist/op-factory-call-BVYLIojV.mjs.map +0 -1
- package/dist/op-factory-call-DT7kdjVP.d.mts.map +0 -1
- package/dist/planner-4sj5cJCY.mjs.map +0 -1
- package/dist/planner-produced-postgres-migration-8QP8eMph.d.mts.map +0 -1
- package/dist/planner-schema-lookup-CiVaAQP-.mjs.map +0 -1
- package/dist/postgres-migration-DLIvzHjX.d.mts.map +0 -1
- package/dist/postgres-migration-GIOABsAa.mjs.map +0 -1
- package/dist/rls-canonicalize.d.mts.map +0 -1
- package/dist/wire-name-DbQ9-2RC.mjs +0 -104
- package/dist/wire-name-DbQ9-2RC.mjs.map +0 -1
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
1152
|
-
extras?:
|
|
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: {
|
|
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
|
-
|
|
1194
|
-
|
|
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 {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
54
|
+
elements: CreateIndexElements,
|
|
48
55
|
lowerer: ExecuteRequestLowerer,
|
|
49
56
|
extras?: CreateIndexExtras,
|
|
50
57
|
): Promise<Op> {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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:
|
|
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 = [
|
|
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 =
|
|
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 =
|
|
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(
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
758
|
+
const columns = index.columns;
|
|
759
|
+
const indexFieldNames = columns.map((columnName) =>
|
|
753
760
|
resolveColumnFieldName(fieldNamesByTable, table.name, columnName),
|
|
754
761
|
);
|
|
755
762
|
modelAttributes.push(
|