@prisma-next/target-postgres 0.3.0-dev.12 → 0.3.0-dev.122
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/LICENSE +201 -0
- package/README.md +9 -2
- package/dist/control.d.mts +19 -0
- package/dist/control.d.mts.map +1 -0
- package/dist/control.mjs +3677 -0
- package/dist/control.mjs.map +1 -0
- package/dist/descriptor-meta-DxB8oZzB.mjs +13 -0
- package/dist/descriptor-meta-DxB8oZzB.mjs.map +1 -0
- package/dist/pack.d.mts +10 -0
- package/dist/pack.d.mts.map +1 -0
- package/dist/pack.mjs +9 -0
- package/dist/pack.mjs.map +1 -0
- package/dist/runtime.d.mts +9 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +21 -0
- package/dist/runtime.mjs.map +1 -0
- package/package.json +30 -29
- package/src/core/migrations/planner-identity-values.ts +129 -0
- package/src/core/migrations/planner-recipes.ts +83 -0
- package/src/core/migrations/planner-reconciliation.ts +781 -0
- package/src/core/migrations/planner-sql.ts +437 -0
- package/src/core/migrations/planner-target-details.ts +16 -0
- package/src/core/migrations/planner.ts +424 -409
- package/src/core/migrations/runner.ts +32 -36
- package/src/core/migrations/statement-builders.ts +9 -7
- package/src/core/types.ts +5 -0
- package/src/exports/control.ts +56 -8
- package/src/exports/pack.ts +5 -2
- package/src/exports/runtime.ts +7 -12
- package/dist/chunk-RKEXRSSI.js +0 -14
- package/dist/chunk-RKEXRSSI.js.map +0 -1
- package/dist/core/descriptor-meta.d.ts +0 -9
- package/dist/core/descriptor-meta.d.ts.map +0 -1
- package/dist/core/migrations/planner.d.ts +0 -14
- package/dist/core/migrations/planner.d.ts.map +0 -1
- package/dist/core/migrations/runner.d.ts +0 -8
- package/dist/core/migrations/runner.d.ts.map +0 -1
- package/dist/core/migrations/statement-builders.d.ts +0 -30
- package/dist/core/migrations/statement-builders.d.ts.map +0 -1
- package/dist/exports/control.d.ts +0 -8
- package/dist/exports/control.d.ts.map +0 -1
- package/dist/exports/control.js +0 -1255
- package/dist/exports/control.js.map +0 -1
- package/dist/exports/pack.d.ts +0 -4
- package/dist/exports/pack.d.ts.map +0 -1
- package/dist/exports/pack.js +0 -11
- package/dist/exports/pack.js.map +0 -1
- package/dist/exports/runtime.d.ts +0 -12
- package/dist/exports/runtime.d.ts.map +0 -1
- package/dist/exports/runtime.js +0 -19
- package/dist/exports/runtime.js.map +0 -1
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeLiteral,
|
|
3
|
+
normalizeSchemaNativeType,
|
|
4
|
+
parsePostgresDefault,
|
|
5
|
+
quoteIdentifier,
|
|
6
|
+
} from '@prisma-next/adapter-postgres/control';
|
|
1
7
|
import type { SchemaIssue } from '@prisma-next/core-control-plane/types';
|
|
2
8
|
import type {
|
|
9
|
+
CodecControlHooks,
|
|
10
|
+
ComponentDatabaseDependency,
|
|
3
11
|
MigrationOperationPolicy,
|
|
4
12
|
SqlMigrationPlanner,
|
|
5
13
|
SqlMigrationPlannerPlanOptions,
|
|
@@ -7,21 +15,45 @@ import type {
|
|
|
7
15
|
SqlPlannerConflict,
|
|
8
16
|
} from '@prisma-next/family-sql/control';
|
|
9
17
|
import {
|
|
18
|
+
collectInitDependencies,
|
|
10
19
|
createMigrationPlan,
|
|
20
|
+
extractCodecControlHooks,
|
|
11
21
|
plannerFailure,
|
|
12
22
|
plannerSuccess,
|
|
13
23
|
} from '@prisma-next/family-sql/control';
|
|
14
|
-
import {
|
|
15
|
-
import type {
|
|
16
|
-
|
|
17
|
-
SqlContract,
|
|
18
|
-
SqlStorage,
|
|
19
|
-
StorageColumn,
|
|
20
|
-
StorageTable,
|
|
21
|
-
} from '@prisma-next/sql-contract/types';
|
|
24
|
+
import { verifySqlSchema } from '@prisma-next/family-sql/schema-verify';
|
|
25
|
+
import type { ForeignKey, StorageColumn, StorageTable } from '@prisma-next/sql-contract/types';
|
|
26
|
+
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
22
27
|
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
29
|
+
import { resolveIdentityValue } from './planner-identity-values';
|
|
30
|
+
import {
|
|
31
|
+
buildAddColumnOperationIdentity,
|
|
32
|
+
buildAddNotNullColumnWithTemporaryDefaultOperation,
|
|
33
|
+
} from './planner-recipes';
|
|
34
|
+
import { buildReconciliationPlan } from './planner-reconciliation';
|
|
35
|
+
import {
|
|
36
|
+
buildAddColumnSql,
|
|
37
|
+
buildCreateTableSql,
|
|
38
|
+
buildForeignKeySql,
|
|
39
|
+
columnExistsCheck,
|
|
40
|
+
columnNullabilityCheck,
|
|
41
|
+
constraintExistsCheck,
|
|
42
|
+
qualifyTableName,
|
|
43
|
+
tableIsEmptyCheck,
|
|
44
|
+
toRegclassLiteral,
|
|
45
|
+
} from './planner-sql';
|
|
46
|
+
import { buildTargetDetails } from './planner-target-details';
|
|
47
|
+
|
|
48
|
+
export type OperationClass =
|
|
49
|
+
| 'dependency'
|
|
50
|
+
| 'type'
|
|
51
|
+
| 'table'
|
|
52
|
+
| 'column'
|
|
53
|
+
| 'primaryKey'
|
|
54
|
+
| 'unique'
|
|
55
|
+
| 'index'
|
|
56
|
+
| 'foreignKey';
|
|
25
57
|
|
|
26
58
|
type PlannerFrameworkComponents = SqlMigrationPlannerPlanOptions extends {
|
|
27
59
|
readonly frameworkComponents: infer T;
|
|
@@ -41,7 +73,6 @@ type PlannerDatabaseDependency = {
|
|
|
41
73
|
readonly id: string;
|
|
42
74
|
readonly label: string;
|
|
43
75
|
readonly install: readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[];
|
|
44
|
-
readonly verifyDatabaseDependencyInstalled: (schema: SqlSchemaIR) => readonly SchemaIssue[];
|
|
45
76
|
};
|
|
46
77
|
|
|
47
78
|
export interface PostgresPlanTargetDetails {
|
|
@@ -55,6 +86,12 @@ interface PlannerConfig {
|
|
|
55
86
|
readonly defaultSchema: string;
|
|
56
87
|
}
|
|
57
88
|
|
|
89
|
+
export interface PlanningMode {
|
|
90
|
+
readonly includeExtraObjects: boolean;
|
|
91
|
+
readonly allowWidening: boolean;
|
|
92
|
+
readonly allowDestructive: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
58
95
|
const DEFAULT_PLANNER_CONFIG: PlannerConfig = {
|
|
59
96
|
defaultSchema: 'public',
|
|
60
97
|
};
|
|
@@ -78,38 +115,64 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
|
|
|
78
115
|
return policyResult;
|
|
79
116
|
}
|
|
80
117
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
118
|
+
const planningMode = this.resolvePlanningMode(options.policy);
|
|
119
|
+
const schemaIssues = this.collectSchemaIssues(options, planningMode.includeExtraObjects);
|
|
120
|
+
|
|
121
|
+
// Extract codec control hooks once at entry point for reuse across all operations.
|
|
122
|
+
// This avoids repeated iteration over frameworkComponents for each method that needs hooks.
|
|
123
|
+
const codecHooks = extractCodecControlHooks(options.frameworkComponents);
|
|
85
124
|
|
|
86
125
|
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
87
126
|
|
|
127
|
+
const reconciliationPlan = buildReconciliationPlan({
|
|
128
|
+
contract: options.contract,
|
|
129
|
+
issues: schemaIssues,
|
|
130
|
+
schemaName,
|
|
131
|
+
mode: planningMode,
|
|
132
|
+
policy: options.policy,
|
|
133
|
+
codecHooks,
|
|
134
|
+
});
|
|
135
|
+
if (reconciliationPlan.conflicts.length > 0) {
|
|
136
|
+
return plannerFailure(reconciliationPlan.conflicts);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const storageTypePlan = this.buildStorageTypeOperations(options, schemaName, codecHooks);
|
|
140
|
+
if (storageTypePlan.conflicts.length > 0) {
|
|
141
|
+
return plannerFailure(storageTypePlan.conflicts);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Sort table entries once for reuse across all additive operation builders.
|
|
145
|
+
const sortedTables = sortedEntries(options.contract.storage.tables);
|
|
146
|
+
|
|
147
|
+
// Pre-compute constraint lookups once per schema table for O(1) checks across all builders.
|
|
148
|
+
const schemaLookups = buildSchemaLookupMap(options.schema);
|
|
149
|
+
|
|
88
150
|
// Build extension operations from component-owned database dependencies
|
|
89
151
|
operations.push(
|
|
90
152
|
...this.buildDatabaseDependencyOperations(options),
|
|
91
|
-
...
|
|
92
|
-
...
|
|
93
|
-
...this.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
schemaName,
|
|
97
|
-
),
|
|
98
|
-
...this.buildUniqueOperations(options.contract.storage.tables, options.schema, schemaName),
|
|
99
|
-
...this.buildIndexOperations(options.contract.storage.tables, options.schema, schemaName),
|
|
100
|
-
...this.buildForeignKeyOperations(
|
|
101
|
-
options.contract.storage.tables,
|
|
153
|
+
...storageTypePlan.operations,
|
|
154
|
+
...reconciliationPlan.operations,
|
|
155
|
+
...this.buildTableOperations(sortedTables, options.schema, schemaName, codecHooks),
|
|
156
|
+
...this.buildColumnOperations(
|
|
157
|
+
sortedTables,
|
|
102
158
|
options.schema,
|
|
159
|
+
schemaLookups,
|
|
103
160
|
schemaName,
|
|
161
|
+
codecHooks,
|
|
104
162
|
),
|
|
163
|
+
...this.buildPrimaryKeyOperations(sortedTables, options.schema, schemaName),
|
|
164
|
+
...this.buildUniqueOperations(sortedTables, schemaLookups, schemaName),
|
|
165
|
+
...this.buildIndexOperations(sortedTables, schemaLookups, schemaName),
|
|
166
|
+
...this.buildFkBackingIndexOperations(sortedTables, schemaLookups, schemaName),
|
|
167
|
+
...this.buildForeignKeyOperations(sortedTables, schemaLookups, schemaName),
|
|
105
168
|
);
|
|
106
169
|
|
|
107
170
|
const plan = createMigrationPlan<PostgresPlanTargetDetails>({
|
|
108
171
|
targetId: 'postgres',
|
|
109
172
|
origin: null,
|
|
110
173
|
destination: {
|
|
111
|
-
|
|
112
|
-
...(
|
|
174
|
+
storageHash: options.contract.storageHash,
|
|
175
|
+
...ifDefined('profileHash', options.contract.profileHash),
|
|
113
176
|
},
|
|
114
177
|
operations,
|
|
115
178
|
});
|
|
@@ -122,8 +185,8 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
|
|
|
122
185
|
return plannerFailure([
|
|
123
186
|
{
|
|
124
187
|
kind: 'unsupportedOperation',
|
|
125
|
-
summary: '
|
|
126
|
-
why: 'The
|
|
188
|
+
summary: 'Migration planner requires additive operations be allowed',
|
|
189
|
+
why: 'The planner requires the "additive" operation class to be allowed in the policy.',
|
|
127
190
|
},
|
|
128
191
|
]);
|
|
129
192
|
}
|
|
@@ -142,14 +205,15 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
|
|
|
142
205
|
const seenDependencyIds = new Set<string>();
|
|
143
206
|
const seenOperationIds = new Set<string>();
|
|
144
207
|
|
|
208
|
+
const installedIds = new Set(options.schema.dependencies.map((d) => d.id));
|
|
209
|
+
|
|
145
210
|
for (const dependency of dependencies) {
|
|
146
211
|
if (seenDependencyIds.has(dependency.id)) {
|
|
147
212
|
continue;
|
|
148
213
|
}
|
|
149
214
|
seenDependencyIds.add(dependency.id);
|
|
150
215
|
|
|
151
|
-
|
|
152
|
-
if (issues.length === 0) {
|
|
216
|
+
if (installedIds.has(dependency.id)) {
|
|
153
217
|
continue;
|
|
154
218
|
}
|
|
155
219
|
|
|
@@ -158,41 +222,76 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
|
|
|
158
222
|
continue;
|
|
159
223
|
}
|
|
160
224
|
seenOperationIds.add(installOp.id);
|
|
161
|
-
// SQL family components are expected to provide compatible target details. This would be better if
|
|
162
|
-
// the type system could enforce it but it's not likely to occur in practice.
|
|
163
225
|
operations.push(installOp as SqlMigrationPlanOperation<PostgresPlanTargetDetails>);
|
|
164
226
|
}
|
|
165
227
|
}
|
|
166
228
|
|
|
167
229
|
return operations;
|
|
168
230
|
}
|
|
169
|
-
|
|
231
|
+
|
|
232
|
+
private buildStorageTypeOperations(
|
|
170
233
|
options: PlannerOptionsWithComponents,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
234
|
+
schemaName: string,
|
|
235
|
+
codecHooks: Map<string, CodecControlHooks>,
|
|
236
|
+
): {
|
|
237
|
+
readonly operations: readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[];
|
|
238
|
+
readonly conflicts: readonly SqlPlannerConflict[];
|
|
239
|
+
} {
|
|
240
|
+
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
241
|
+
const conflicts: SqlPlannerConflict[] = [];
|
|
242
|
+
const storageTypes = options.contract.storage.types ?? {};
|
|
243
|
+
|
|
244
|
+
for (const [typeName, typeInstance] of sortedEntries(storageTypes)) {
|
|
245
|
+
const hook = codecHooks.get(typeInstance.codecId);
|
|
246
|
+
const planResult = hook?.planTypeOperations?.({
|
|
247
|
+
typeName,
|
|
248
|
+
typeInstance,
|
|
249
|
+
contract: options.contract,
|
|
250
|
+
schema: options.schema,
|
|
251
|
+
schemaName,
|
|
252
|
+
policy: options.policy,
|
|
253
|
+
});
|
|
254
|
+
if (!planResult) {
|
|
179
255
|
continue;
|
|
180
256
|
}
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
257
|
+
for (const operation of planResult.operations) {
|
|
258
|
+
if (!options.policy.allowedOperationClasses.includes(operation.operationClass)) {
|
|
259
|
+
conflicts.push({
|
|
260
|
+
kind: 'missingButNonAdditive',
|
|
261
|
+
summary: `Storage type "${typeName}" requires "${operation.operationClass}" operation "${operation.id}"`,
|
|
262
|
+
location: {
|
|
263
|
+
type: typeName,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
operations.push({
|
|
269
|
+
...operation,
|
|
270
|
+
target: {
|
|
271
|
+
id: operation.target.id,
|
|
272
|
+
details: this.buildTargetDetails('type', typeName, schemaName),
|
|
273
|
+
},
|
|
274
|
+
});
|
|
184
275
|
}
|
|
185
276
|
}
|
|
186
|
-
|
|
277
|
+
|
|
278
|
+
return { operations, conflicts };
|
|
279
|
+
}
|
|
280
|
+
private collectDependencies(
|
|
281
|
+
options: PlannerOptionsWithComponents,
|
|
282
|
+
): ReadonlyArray<PlannerDatabaseDependency> {
|
|
283
|
+
const dependencies = collectInitDependencies(options.frameworkComponents);
|
|
284
|
+
return sortDependencies(dependencies.filter(isPostgresPlannerDependency));
|
|
187
285
|
}
|
|
188
286
|
|
|
189
287
|
private buildTableOperations(
|
|
190
|
-
tables:
|
|
288
|
+
tables: ReadonlyArray<[string, StorageTable]>,
|
|
191
289
|
schema: SqlSchemaIR,
|
|
192
290
|
schemaName: string,
|
|
291
|
+
codecHooks: Map<string, CodecControlHooks>,
|
|
193
292
|
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
194
293
|
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
195
|
-
for (const [tableName, table] of
|
|
294
|
+
for (const [tableName, table] of tables) {
|
|
196
295
|
if (schema.tables[tableName]) {
|
|
197
296
|
continue;
|
|
198
297
|
}
|
|
@@ -215,7 +314,7 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
|
|
|
215
314
|
execute: [
|
|
216
315
|
{
|
|
217
316
|
description: `create table "${tableName}"`,
|
|
218
|
-
sql: buildCreateTableSql(qualified, table),
|
|
317
|
+
sql: buildCreateTableSql(qualified, table, codecHooks),
|
|
219
318
|
},
|
|
220
319
|
],
|
|
221
320
|
postcheck: [
|
|
@@ -230,91 +329,135 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
|
|
|
230
329
|
}
|
|
231
330
|
|
|
232
331
|
private buildColumnOperations(
|
|
233
|
-
tables:
|
|
332
|
+
tables: ReadonlyArray<[string, StorageTable]>,
|
|
234
333
|
schema: SqlSchemaIR,
|
|
334
|
+
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
235
335
|
schemaName: string,
|
|
336
|
+
codecHooks: Map<string, CodecControlHooks>,
|
|
236
337
|
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
237
338
|
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
238
|
-
for (const [tableName, table] of
|
|
339
|
+
for (const [tableName, table] of tables) {
|
|
239
340
|
const schemaTable = schema.tables[tableName];
|
|
240
341
|
if (!schemaTable) {
|
|
241
342
|
continue;
|
|
242
343
|
}
|
|
344
|
+
const schemaLookup = schemaLookups.get(tableName);
|
|
243
345
|
for (const [columnName, column] of sortedEntries(table.columns)) {
|
|
244
346
|
if (schemaTable.columns[columnName]) {
|
|
245
347
|
continue;
|
|
246
348
|
}
|
|
247
|
-
operations.push(
|
|
349
|
+
operations.push(
|
|
350
|
+
this.buildAddColumnOperation({
|
|
351
|
+
schema: schemaName,
|
|
352
|
+
tableName,
|
|
353
|
+
table,
|
|
354
|
+
schemaTable,
|
|
355
|
+
schemaLookup,
|
|
356
|
+
columnName,
|
|
357
|
+
column,
|
|
358
|
+
codecHooks,
|
|
359
|
+
}),
|
|
360
|
+
);
|
|
248
361
|
}
|
|
249
362
|
}
|
|
250
363
|
return operations;
|
|
251
364
|
}
|
|
252
365
|
|
|
253
|
-
private buildAddColumnOperation(
|
|
254
|
-
schema: string
|
|
255
|
-
tableName: string
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
366
|
+
private buildAddColumnOperation(options: {
|
|
367
|
+
readonly schema: string;
|
|
368
|
+
readonly tableName: string;
|
|
369
|
+
readonly table: StorageTable;
|
|
370
|
+
readonly schemaTable: SqlSchemaIR['tables'][string];
|
|
371
|
+
readonly schemaLookup: SchemaTableLookup | undefined;
|
|
372
|
+
readonly columnName: string;
|
|
373
|
+
readonly column: StorageColumn;
|
|
374
|
+
readonly codecHooks: Map<string, CodecControlHooks>;
|
|
375
|
+
}): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
|
|
376
|
+
const { schema, tableName, table, schemaTable, schemaLookup, columnName, column, codecHooks } =
|
|
377
|
+
options;
|
|
260
378
|
const notNull = column.nullable === false;
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
{
|
|
289
|
-
description: `verify column "${columnName}" is NOT NULL`,
|
|
290
|
-
sql: columnIsNotNullCheck({ schema, table: tableName, column: columnName }),
|
|
291
|
-
},
|
|
292
|
-
]
|
|
293
|
-
: []),
|
|
294
|
-
];
|
|
379
|
+
const hasDefault = column.default !== undefined;
|
|
380
|
+
// Planner logic decides whether this column needs the coordinated multi-step
|
|
381
|
+
// strategy. The strategy recipe itself is built by a dedicated helper.
|
|
382
|
+
const needsTemporaryDefault = notNull && !hasDefault;
|
|
383
|
+
const temporaryDefault = needsTemporaryDefault
|
|
384
|
+
? resolveIdentityValue(column, codecHooks)
|
|
385
|
+
: null;
|
|
386
|
+
const canUseSharedTemporaryDefault =
|
|
387
|
+
needsTemporaryDefault &&
|
|
388
|
+
temporaryDefault !== null &&
|
|
389
|
+
canUseSharedTemporaryDefaultStrategy({
|
|
390
|
+
table,
|
|
391
|
+
schemaTable,
|
|
392
|
+
schemaLookup,
|
|
393
|
+
columnName,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (canUseSharedTemporaryDefault) {
|
|
397
|
+
return buildAddNotNullColumnWithTemporaryDefaultOperation({
|
|
398
|
+
schema,
|
|
399
|
+
tableName,
|
|
400
|
+
columnName,
|
|
401
|
+
column,
|
|
402
|
+
codecHooks,
|
|
403
|
+
temporaryDefault,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
295
406
|
|
|
407
|
+
const qualified = qualifyTableName(schema, tableName);
|
|
408
|
+
const requiresEmptyTableCheck = needsTemporaryDefault && !canUseSharedTemporaryDefault;
|
|
296
409
|
return {
|
|
297
|
-
|
|
298
|
-
label: `Add column ${columnName} to ${tableName}`,
|
|
299
|
-
summary: `Adds column ${columnName} to table ${tableName}`,
|
|
410
|
+
...buildAddColumnOperationIdentity(schema, tableName, columnName),
|
|
300
411
|
operationClass: 'additive',
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
412
|
+
precheck: [
|
|
413
|
+
{
|
|
414
|
+
description: `ensure column "${columnName}" is missing`,
|
|
415
|
+
sql: columnExistsCheck({ schema, table: tableName, column: columnName, exists: false }),
|
|
416
|
+
},
|
|
417
|
+
...(requiresEmptyTableCheck
|
|
418
|
+
? [
|
|
419
|
+
{
|
|
420
|
+
description: `ensure table "${tableName}" is empty before adding NOT NULL column without default`,
|
|
421
|
+
sql: tableIsEmptyCheck(qualified),
|
|
422
|
+
},
|
|
423
|
+
]
|
|
424
|
+
: []),
|
|
425
|
+
],
|
|
426
|
+
execute: [
|
|
427
|
+
{
|
|
428
|
+
description: `add column "${columnName}"`,
|
|
429
|
+
sql: buildAddColumnSql(qualified, columnName, column, codecHooks),
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
postcheck: [
|
|
433
|
+
{
|
|
434
|
+
description: `verify column "${columnName}" exists`,
|
|
435
|
+
sql: columnExistsCheck({ schema, table: tableName, column: columnName }),
|
|
436
|
+
},
|
|
437
|
+
...(notNull
|
|
438
|
+
? [
|
|
439
|
+
{
|
|
440
|
+
description: `verify column "${columnName}" is NOT NULL`,
|
|
441
|
+
sql: columnNullabilityCheck({
|
|
442
|
+
schema,
|
|
443
|
+
table: tableName,
|
|
444
|
+
column: columnName,
|
|
445
|
+
nullable: false,
|
|
446
|
+
}),
|
|
447
|
+
},
|
|
448
|
+
]
|
|
449
|
+
: []),
|
|
450
|
+
],
|
|
308
451
|
};
|
|
309
452
|
}
|
|
310
453
|
|
|
311
454
|
private buildPrimaryKeyOperations(
|
|
312
|
-
tables:
|
|
455
|
+
tables: ReadonlyArray<[string, StorageTable]>,
|
|
313
456
|
schema: SqlSchemaIR,
|
|
314
457
|
schemaName: string,
|
|
315
458
|
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
316
459
|
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
317
|
-
for (const [tableName, table] of
|
|
460
|
+
for (const [tableName, table] of tables) {
|
|
318
461
|
if (!table.primaryKey) {
|
|
319
462
|
continue;
|
|
320
463
|
}
|
|
@@ -358,15 +501,15 @@ PRIMARY KEY (${table.primaryKey.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
358
501
|
}
|
|
359
502
|
|
|
360
503
|
private buildUniqueOperations(
|
|
361
|
-
tables:
|
|
362
|
-
|
|
504
|
+
tables: ReadonlyArray<[string, StorageTable]>,
|
|
505
|
+
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
363
506
|
schemaName: string,
|
|
364
507
|
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
365
508
|
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
366
|
-
for (const [tableName, table] of
|
|
367
|
-
const
|
|
509
|
+
for (const [tableName, table] of tables) {
|
|
510
|
+
const lookup = schemaLookups.get(tableName);
|
|
368
511
|
for (const unique of table.uniques) {
|
|
369
|
-
if (
|
|
512
|
+
if (lookup && hasUniqueConstraint(lookup, unique.columns)) {
|
|
370
513
|
continue;
|
|
371
514
|
}
|
|
372
515
|
const constraintName = unique.name ?? `${tableName}_${unique.columns.join('_')}_key`;
|
|
@@ -382,7 +525,12 @@ PRIMARY KEY (${table.primaryKey.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
382
525
|
precheck: [
|
|
383
526
|
{
|
|
384
527
|
description: `ensure unique constraint "${constraintName}" is missing`,
|
|
385
|
-
sql: constraintExistsCheck({
|
|
528
|
+
sql: constraintExistsCheck({
|
|
529
|
+
constraintName,
|
|
530
|
+
schema: schemaName,
|
|
531
|
+
table: tableName,
|
|
532
|
+
exists: false,
|
|
533
|
+
}),
|
|
386
534
|
},
|
|
387
535
|
],
|
|
388
536
|
execute: [
|
|
@@ -396,7 +544,7 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
396
544
|
postcheck: [
|
|
397
545
|
{
|
|
398
546
|
description: `verify unique constraint "${constraintName}" exists`,
|
|
399
|
-
sql: constraintExistsCheck({ constraintName, schema: schemaName }),
|
|
547
|
+
sql: constraintExistsCheck({ constraintName, schema: schemaName, table: tableName }),
|
|
400
548
|
},
|
|
401
549
|
],
|
|
402
550
|
});
|
|
@@ -406,18 +554,18 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
406
554
|
}
|
|
407
555
|
|
|
408
556
|
private buildIndexOperations(
|
|
409
|
-
tables:
|
|
410
|
-
|
|
557
|
+
tables: ReadonlyArray<[string, StorageTable]>,
|
|
558
|
+
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
411
559
|
schemaName: string,
|
|
412
560
|
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
413
561
|
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
414
|
-
for (const [tableName, table] of
|
|
415
|
-
const
|
|
562
|
+
for (const [tableName, table] of tables) {
|
|
563
|
+
const lookup = schemaLookups.get(tableName);
|
|
416
564
|
for (const index of table.indexes) {
|
|
417
|
-
if (
|
|
565
|
+
if (lookup && hasIndex(lookup, index.columns)) {
|
|
418
566
|
continue;
|
|
419
567
|
}
|
|
420
|
-
const indexName = index.name ??
|
|
568
|
+
const indexName = index.name ?? defaultIndexName(tableName, index.columns);
|
|
421
569
|
operations.push({
|
|
422
570
|
id: `index.${tableName}.${indexName}`,
|
|
423
571
|
label: `Create index ${indexName} on ${tableName}`,
|
|
@@ -454,16 +602,76 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
454
602
|
return operations;
|
|
455
603
|
}
|
|
456
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Generates FK-backing index operations for FKs with `index: true`,
|
|
607
|
+
* but only when no matching user-declared index exists in `contractTable.indexes`.
|
|
608
|
+
*/
|
|
609
|
+
private buildFkBackingIndexOperations(
|
|
610
|
+
tables: ReadonlyArray<[string, StorageTable]>,
|
|
611
|
+
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
612
|
+
schemaName: string,
|
|
613
|
+
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
614
|
+
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
615
|
+
for (const [tableName, table] of tables) {
|
|
616
|
+
const lookup = schemaLookups.get(tableName);
|
|
617
|
+
// Collect column sets of user-declared indexes to avoid duplicates
|
|
618
|
+
const declaredIndexColumns = new Set(table.indexes.map((idx) => idx.columns.join(',')));
|
|
619
|
+
|
|
620
|
+
for (const fk of table.foreignKeys) {
|
|
621
|
+
if (fk.index === false) continue;
|
|
622
|
+
// Skip if user already declared an index with these columns
|
|
623
|
+
if (declaredIndexColumns.has(fk.columns.join(','))) continue;
|
|
624
|
+
// Skip if the index already exists in the database
|
|
625
|
+
if (lookup && hasIndex(lookup, fk.columns)) continue;
|
|
626
|
+
|
|
627
|
+
const indexName = defaultIndexName(tableName, fk.columns);
|
|
628
|
+
operations.push({
|
|
629
|
+
id: `index.${tableName}.${indexName}`,
|
|
630
|
+
label: `Create FK-backing index ${indexName} on ${tableName}`,
|
|
631
|
+
summary: `Creates FK-backing index ${indexName} on ${tableName}`,
|
|
632
|
+
operationClass: 'additive',
|
|
633
|
+
target: {
|
|
634
|
+
id: 'postgres',
|
|
635
|
+
details: this.buildTargetDetails('index', indexName, schemaName, tableName),
|
|
636
|
+
},
|
|
637
|
+
precheck: [
|
|
638
|
+
{
|
|
639
|
+
description: `ensure index "${indexName}" is missing`,
|
|
640
|
+
sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, indexName)}) IS NULL`,
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
execute: [
|
|
644
|
+
{
|
|
645
|
+
description: `create FK-backing index "${indexName}"`,
|
|
646
|
+
sql: `CREATE INDEX ${quoteIdentifier(indexName)} ON ${qualifyTableName(
|
|
647
|
+
schemaName,
|
|
648
|
+
tableName,
|
|
649
|
+
)} (${fk.columns.map(quoteIdentifier).join(', ')})`,
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
postcheck: [
|
|
653
|
+
{
|
|
654
|
+
description: `verify index "${indexName}" exists`,
|
|
655
|
+
sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, indexName)}) IS NOT NULL`,
|
|
656
|
+
},
|
|
657
|
+
],
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return operations;
|
|
662
|
+
}
|
|
663
|
+
|
|
457
664
|
private buildForeignKeyOperations(
|
|
458
|
-
tables:
|
|
459
|
-
|
|
665
|
+
tables: ReadonlyArray<[string, StorageTable]>,
|
|
666
|
+
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
460
667
|
schemaName: string,
|
|
461
668
|
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
462
669
|
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
463
|
-
for (const [tableName, table] of
|
|
464
|
-
const
|
|
670
|
+
for (const [tableName, table] of tables) {
|
|
671
|
+
const lookup = schemaLookups.get(tableName);
|
|
465
672
|
for (const foreignKey of table.foreignKeys) {
|
|
466
|
-
if (
|
|
673
|
+
if (foreignKey.constraint === false) continue;
|
|
674
|
+
if (lookup && hasForeignKey(lookup, foreignKey)) {
|
|
467
675
|
continue;
|
|
468
676
|
}
|
|
469
677
|
const fkName = foreignKey.name ?? `${tableName}_${foreignKey.columns.join('_')}_fkey`;
|
|
@@ -482,6 +690,7 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
482
690
|
sql: constraintExistsCheck({
|
|
483
691
|
constraintName: fkName,
|
|
484
692
|
schema: schemaName,
|
|
693
|
+
table: tableName,
|
|
485
694
|
exists: false,
|
|
486
695
|
}),
|
|
487
696
|
},
|
|
@@ -489,18 +698,17 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
489
698
|
execute: [
|
|
490
699
|
{
|
|
491
700
|
description: `add foreign key "${fkName}"`,
|
|
492
|
-
sql:
|
|
493
|
-
ADD CONSTRAINT ${quoteIdentifier(fkName)}
|
|
494
|
-
FOREIGN KEY (${foreignKey.columns.map(quoteIdentifier).join(', ')})
|
|
495
|
-
REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${foreignKey.references.columns
|
|
496
|
-
.map(quoteIdentifier)
|
|
497
|
-
.join(', ')})`,
|
|
701
|
+
sql: buildForeignKeySql(schemaName, tableName, fkName, foreignKey),
|
|
498
702
|
},
|
|
499
703
|
],
|
|
500
704
|
postcheck: [
|
|
501
705
|
{
|
|
502
706
|
description: `verify foreign key "${fkName}" exists`,
|
|
503
|
-
sql: constraintExistsCheck({
|
|
707
|
+
sql: constraintExistsCheck({
|
|
708
|
+
constraintName: fkName,
|
|
709
|
+
schema: schemaName,
|
|
710
|
+
table: tableName,
|
|
711
|
+
}),
|
|
504
712
|
},
|
|
505
713
|
],
|
|
506
714
|
});
|
|
@@ -515,240 +723,87 @@ REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${forei
|
|
|
515
723
|
schema: string,
|
|
516
724
|
table?: string,
|
|
517
725
|
): PostgresPlanTargetDetails {
|
|
518
|
-
return
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
726
|
+
return buildTargetDetails(objectType, name, schema, table);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private resolvePlanningMode(policy: MigrationOperationPolicy): PlanningMode {
|
|
730
|
+
const allowWidening = policy.allowedOperationClasses.includes('widening');
|
|
731
|
+
const allowDestructive = policy.allowedOperationClasses.includes('destructive');
|
|
732
|
+
// `db init` uses additive-only policy and intentionally ignores extras.
|
|
733
|
+
// Any reconciliation-capable policy should inspect extras to reconcile strict equality.
|
|
734
|
+
const includeExtraObjects = allowWidening || allowDestructive;
|
|
735
|
+
return { includeExtraObjects, allowWidening, allowDestructive };
|
|
524
736
|
}
|
|
525
737
|
|
|
526
|
-
private
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
conflicts: SqlPlannerConflict[];
|
|
531
|
-
} {
|
|
738
|
+
private collectSchemaIssues(
|
|
739
|
+
options: PlannerOptionsWithComponents,
|
|
740
|
+
strict: boolean,
|
|
741
|
+
): readonly SchemaIssue[] {
|
|
532
742
|
const verifyOptions: VerifySqlSchemaOptionsWithComponents = {
|
|
533
743
|
contract: options.contract,
|
|
534
744
|
schema: options.schema,
|
|
535
|
-
strict
|
|
745
|
+
strict,
|
|
536
746
|
typeMetadataRegistry: new Map(),
|
|
537
747
|
frameworkComponents: options.frameworkComponents,
|
|
748
|
+
normalizeDefault: parsePostgresDefault,
|
|
749
|
+
normalizeNativeType: normalizeSchemaNativeType,
|
|
538
750
|
};
|
|
539
751
|
const verifyResult = verifySqlSchema(verifyOptions);
|
|
540
|
-
|
|
541
|
-
const conflicts = this.extractConflicts(verifyResult.schema.issues);
|
|
542
|
-
if (conflicts.length > 0) {
|
|
543
|
-
return { kind: 'conflict', conflicts };
|
|
544
|
-
}
|
|
545
|
-
return { kind: 'ok' };
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
private extractConflicts(issues: readonly SchemaIssue[]): SqlPlannerConflict[] {
|
|
549
|
-
const conflicts: SqlPlannerConflict[] = [];
|
|
550
|
-
for (const issue of issues) {
|
|
551
|
-
if (isAdditiveIssue(issue)) {
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
const conflict = this.convertIssueToConflict(issue);
|
|
555
|
-
if (conflict) {
|
|
556
|
-
conflicts.push(conflict);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
return conflicts.sort(conflictComparator);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
private convertIssueToConflict(issue: SchemaIssue): SqlPlannerConflict | null {
|
|
563
|
-
switch (issue.kind) {
|
|
564
|
-
case 'type_mismatch':
|
|
565
|
-
return this.buildConflict('typeMismatch', issue);
|
|
566
|
-
case 'nullability_mismatch':
|
|
567
|
-
return this.buildConflict('nullabilityConflict', issue);
|
|
568
|
-
case 'primary_key_mismatch':
|
|
569
|
-
return this.buildConflict('indexIncompatible', issue);
|
|
570
|
-
case 'unique_constraint_mismatch':
|
|
571
|
-
return this.buildConflict('indexIncompatible', issue);
|
|
572
|
-
case 'index_mismatch':
|
|
573
|
-
return this.buildConflict('indexIncompatible', issue);
|
|
574
|
-
case 'foreign_key_mismatch':
|
|
575
|
-
return this.buildConflict('foreignKeyConflict', issue);
|
|
576
|
-
default:
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
private buildConflict(kind: SqlPlannerConflict['kind'], issue: SchemaIssue): SqlPlannerConflict {
|
|
582
|
-
const location = buildConflictLocation(issue);
|
|
583
|
-
const meta =
|
|
584
|
-
issue.expected || issue.actual
|
|
585
|
-
? Object.freeze({
|
|
586
|
-
...(issue.expected ? { expected: issue.expected } : {}),
|
|
587
|
-
...(issue.actual ? { actual: issue.actual } : {}),
|
|
588
|
-
})
|
|
589
|
-
: undefined;
|
|
590
|
-
|
|
591
|
-
return {
|
|
592
|
-
kind,
|
|
593
|
-
summary: issue.message,
|
|
594
|
-
...(location ? { location } : {}),
|
|
595
|
-
...(meta ? { meta } : {}),
|
|
596
|
-
};
|
|
752
|
+
return verifyResult.schema.issues;
|
|
597
753
|
}
|
|
598
754
|
}
|
|
599
755
|
|
|
600
|
-
function
|
|
601
|
-
readonly
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
756
|
+
function canUseSharedTemporaryDefaultStrategy(options: {
|
|
757
|
+
readonly table: StorageTable;
|
|
758
|
+
readonly schemaTable: SqlSchemaIR['tables'][string];
|
|
759
|
+
readonly schemaLookup: SchemaTableLookup | undefined;
|
|
760
|
+
readonly columnName: string;
|
|
761
|
+
}): boolean {
|
|
762
|
+
const { table, schemaTable, schemaLookup, columnName } = options;
|
|
763
|
+
|
|
764
|
+
// Shared placeholders are only safe when later plan steps do not require
|
|
765
|
+
// row-specific values for this newly added column.
|
|
766
|
+
if (table.primaryKey?.columns.includes(columnName) && !schemaTable.primaryKey) {
|
|
606
767
|
return false;
|
|
607
768
|
}
|
|
608
|
-
const record = component as Record<string, unknown>;
|
|
609
769
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
770
|
+
for (const unique of table.uniques) {
|
|
771
|
+
if (!unique.columns.includes(columnName)) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (!schemaLookup || !hasUniqueConstraint(schemaLookup, unique.columns)) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
613
777
|
}
|
|
614
778
|
|
|
615
|
-
|
|
616
|
-
|
|
779
|
+
for (const foreignKey of table.foreignKeys) {
|
|
780
|
+
if (foreignKey.constraint === false || !foreignKey.columns.includes(columnName)) {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (!schemaLookup || !hasForeignKey(schemaLookup, foreignKey)) {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
617
786
|
}
|
|
618
|
-
|
|
619
|
-
return
|
|
787
|
+
|
|
788
|
+
return true;
|
|
620
789
|
}
|
|
621
790
|
|
|
622
791
|
function sortDependencies(
|
|
623
792
|
dependencies: ReadonlyArray<PlannerDatabaseDependency>,
|
|
624
793
|
): ReadonlyArray<PlannerDatabaseDependency> {
|
|
625
|
-
if (dependencies.length <= 1) {
|
|
626
|
-
return dependencies;
|
|
627
|
-
}
|
|
628
794
|
return [...dependencies].sort((a, b) => a.id.localeCompare(b.id));
|
|
629
795
|
}
|
|
630
796
|
|
|
631
|
-
function
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
quoteIdentifier(columnName),
|
|
636
|
-
column.nativeType,
|
|
637
|
-
column.nullable ? '' : 'NOT NULL',
|
|
638
|
-
].filter(Boolean);
|
|
639
|
-
return parts.join(' ');
|
|
640
|
-
},
|
|
641
|
-
);
|
|
642
|
-
|
|
643
|
-
const constraintDefinitions: string[] = [];
|
|
644
|
-
if (table.primaryKey) {
|
|
645
|
-
constraintDefinitions.push(
|
|
646
|
-
`PRIMARY KEY (${table.primaryKey.columns.map(quoteIdentifier).join(', ')})`,
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
const allDefinitions = [...columnDefinitions, ...constraintDefinitions];
|
|
651
|
-
return `CREATE TABLE ${qualifiedTableName} (\n ${allDefinitions.join(',\n ')}\n)`;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function qualifyTableName(schema: string, table: string): string {
|
|
655
|
-
return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function toRegclassLiteral(schema: string, name: string): string {
|
|
659
|
-
const regclass = `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
|
|
660
|
-
return `'${escapeLiteral(regclass)}'`;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/** Escapes and quotes a SQL identifier (table, column, schema name). */
|
|
664
|
-
function quoteIdentifier(identifier: string): string {
|
|
665
|
-
// TypeScript enforces string type - no runtime check needed for internal callers
|
|
666
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function escapeLiteral(value: string): string {
|
|
670
|
-
return value.replace(/'/g, "''");
|
|
797
|
+
function isPostgresPlannerDependency(
|
|
798
|
+
dependency: ComponentDatabaseDependency<unknown>,
|
|
799
|
+
): dependency is PlannerDatabaseDependency {
|
|
800
|
+
return dependency.install.every((operation) => operation.target.id === 'postgres');
|
|
671
801
|
}
|
|
672
802
|
|
|
673
803
|
function sortedEntries<V>(record: Readonly<Record<string, V>>): Array<[string, V]> {
|
|
674
804
|
return Object.entries(record).sort(([a], [b]) => a.localeCompare(b)) as Array<[string, V]>;
|
|
675
805
|
}
|
|
676
806
|
|
|
677
|
-
function constraintExistsCheck({
|
|
678
|
-
constraintName,
|
|
679
|
-
schema,
|
|
680
|
-
exists = true,
|
|
681
|
-
}: {
|
|
682
|
-
constraintName: string;
|
|
683
|
-
schema: string;
|
|
684
|
-
exists?: boolean;
|
|
685
|
-
}): string {
|
|
686
|
-
const existsClause = exists ? 'EXISTS' : 'NOT EXISTS';
|
|
687
|
-
return `SELECT ${existsClause} (
|
|
688
|
-
SELECT 1 FROM pg_constraint c
|
|
689
|
-
JOIN pg_namespace n ON c.connamespace = n.oid
|
|
690
|
-
WHERE c.conname = '${escapeLiteral(constraintName)}'
|
|
691
|
-
AND n.nspname = '${escapeLiteral(schema)}'
|
|
692
|
-
)`;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function columnExistsCheck({
|
|
696
|
-
schema,
|
|
697
|
-
table,
|
|
698
|
-
column,
|
|
699
|
-
exists = true,
|
|
700
|
-
}: {
|
|
701
|
-
schema: string;
|
|
702
|
-
table: string;
|
|
703
|
-
column: string;
|
|
704
|
-
exists?: boolean;
|
|
705
|
-
}): string {
|
|
706
|
-
const existsClause = exists ? '' : 'NOT ';
|
|
707
|
-
return `SELECT ${existsClause}EXISTS (
|
|
708
|
-
SELECT 1
|
|
709
|
-
FROM information_schema.columns
|
|
710
|
-
WHERE table_schema = '${escapeLiteral(schema)}'
|
|
711
|
-
AND table_name = '${escapeLiteral(table)}'
|
|
712
|
-
AND column_name = '${escapeLiteral(column)}'
|
|
713
|
-
)`;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function columnIsNotNullCheck({
|
|
717
|
-
schema,
|
|
718
|
-
table,
|
|
719
|
-
column,
|
|
720
|
-
}: {
|
|
721
|
-
schema: string;
|
|
722
|
-
table: string;
|
|
723
|
-
column: string;
|
|
724
|
-
}): string {
|
|
725
|
-
return `SELECT EXISTS (
|
|
726
|
-
SELECT 1
|
|
727
|
-
FROM information_schema.columns
|
|
728
|
-
WHERE table_schema = '${escapeLiteral(schema)}'
|
|
729
|
-
AND table_name = '${escapeLiteral(table)}'
|
|
730
|
-
AND column_name = '${escapeLiteral(column)}'
|
|
731
|
-
AND is_nullable = 'NO'
|
|
732
|
-
)`;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function tableIsEmptyCheck(qualifiedTableName: string): string {
|
|
736
|
-
return `SELECT NOT EXISTS (SELECT 1 FROM ${qualifiedTableName} LIMIT 1)`;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
function buildAddColumnSql(
|
|
740
|
-
qualifiedTableName: string,
|
|
741
|
-
columnName: string,
|
|
742
|
-
column: StorageColumn,
|
|
743
|
-
): string {
|
|
744
|
-
const parts = [
|
|
745
|
-
`ALTER TABLE ${qualifiedTableName}`,
|
|
746
|
-
`ADD COLUMN ${quoteIdentifier(columnName)} ${column.nativeType}`,
|
|
747
|
-
column.nullable ? '' : 'NOT NULL',
|
|
748
|
-
].filter(Boolean);
|
|
749
|
-
return parts.join(' ');
|
|
750
|
-
}
|
|
751
|
-
|
|
752
807
|
function tableHasPrimaryKeyCheck(
|
|
753
808
|
schema: string,
|
|
754
809
|
table: string,
|
|
@@ -772,91 +827,51 @@ function tableHasPrimaryKeyCheck(
|
|
|
772
827
|
)`;
|
|
773
828
|
}
|
|
774
829
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
830
|
+
/**
|
|
831
|
+
* Pre-computed lookup sets for a schema table's constraints.
|
|
832
|
+
* Converts O(n*m) linear scans to O(1) Set lookups per constraint check.
|
|
833
|
+
*/
|
|
834
|
+
interface SchemaTableLookup {
|
|
835
|
+
readonly uniqueKeys: Set<string>;
|
|
836
|
+
readonly indexKeys: Set<string>;
|
|
837
|
+
readonly uniqueIndexKeys: Set<string>;
|
|
838
|
+
readonly fkKeys: Set<string>;
|
|
780
839
|
}
|
|
781
840
|
|
|
782
|
-
function
|
|
783
|
-
|
|
841
|
+
function buildSchemaLookupMap(schema: SqlSchemaIR): ReadonlyMap<string, SchemaTableLookup> {
|
|
842
|
+
const map = new Map<string, SchemaTableLookup>();
|
|
843
|
+
for (const [tableName, table] of Object.entries(schema.tables)) {
|
|
844
|
+
map.set(tableName, buildSchemaTableLookup(table));
|
|
845
|
+
}
|
|
846
|
+
return map;
|
|
784
847
|
}
|
|
785
848
|
|
|
786
|
-
function
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
arraysEqual(candidate.referencedColumns, fk.references.columns),
|
|
849
|
+
function buildSchemaTableLookup(table: SqlSchemaIR['tables'][string]): SchemaTableLookup {
|
|
850
|
+
const uniqueKeys = new Set(table.uniques.map((u) => u.columns.join(',')));
|
|
851
|
+
const indexKeys = new Set(table.indexes.map((i) => i.columns.join(',')));
|
|
852
|
+
const uniqueIndexKeys = new Set(
|
|
853
|
+
table.indexes.filter((i) => i.unique).map((i) => i.columns.join(',')),
|
|
792
854
|
);
|
|
855
|
+
const fkKeys = new Set(
|
|
856
|
+
table.foreignKeys.map(
|
|
857
|
+
(fk) => `${fk.columns.join(',')}|${fk.referencedTable}|${fk.referencedColumns.join(',')}`,
|
|
858
|
+
),
|
|
859
|
+
);
|
|
860
|
+
return { uniqueKeys, indexKeys, uniqueIndexKeys, fkKeys };
|
|
793
861
|
}
|
|
794
862
|
|
|
795
|
-
function
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
case 'missing_column':
|
|
799
|
-
case 'extension_missing':
|
|
800
|
-
return true;
|
|
801
|
-
case 'primary_key_mismatch':
|
|
802
|
-
return issue.actual === undefined;
|
|
803
|
-
case 'unique_constraint_mismatch':
|
|
804
|
-
case 'index_mismatch':
|
|
805
|
-
case 'foreign_key_mismatch':
|
|
806
|
-
return issue.indexOrConstraint === undefined;
|
|
807
|
-
default:
|
|
808
|
-
return false;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
function buildConflictLocation(issue: SchemaIssue) {
|
|
813
|
-
const location: {
|
|
814
|
-
table?: string;
|
|
815
|
-
column?: string;
|
|
816
|
-
constraint?: string;
|
|
817
|
-
} = {};
|
|
818
|
-
if (issue.table) {
|
|
819
|
-
location.table = issue.table;
|
|
820
|
-
}
|
|
821
|
-
if (issue.column) {
|
|
822
|
-
location.column = issue.column;
|
|
823
|
-
}
|
|
824
|
-
if (issue.indexOrConstraint) {
|
|
825
|
-
location.constraint = issue.indexOrConstraint;
|
|
826
|
-
}
|
|
827
|
-
return Object.keys(location).length > 0 ? location : undefined;
|
|
863
|
+
function hasUniqueConstraint(lookup: SchemaTableLookup, columns: readonly string[]): boolean {
|
|
864
|
+
const key = columns.join(',');
|
|
865
|
+
return lookup.uniqueKeys.has(key) || lookup.uniqueIndexKeys.has(key);
|
|
828
866
|
}
|
|
829
867
|
|
|
830
|
-
function
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
834
|
-
const aLocation = a.location ?? {};
|
|
835
|
-
const bLocation = b.location ?? {};
|
|
836
|
-
const tableCompare = compareStrings(aLocation.table, bLocation.table);
|
|
837
|
-
if (tableCompare !== 0) {
|
|
838
|
-
return tableCompare;
|
|
839
|
-
}
|
|
840
|
-
const columnCompare = compareStrings(aLocation.column, bLocation.column);
|
|
841
|
-
if (columnCompare !== 0) {
|
|
842
|
-
return columnCompare;
|
|
843
|
-
}
|
|
844
|
-
const constraintCompare = compareStrings(aLocation.constraint, bLocation.constraint);
|
|
845
|
-
if (constraintCompare !== 0) {
|
|
846
|
-
return constraintCompare;
|
|
847
|
-
}
|
|
848
|
-
return compareStrings(a.summary, b.summary);
|
|
868
|
+
function hasIndex(lookup: SchemaTableLookup, columns: readonly string[]): boolean {
|
|
869
|
+
const key = columns.join(',');
|
|
870
|
+
return lookup.indexKeys.has(key) || lookup.uniqueKeys.has(key);
|
|
849
871
|
}
|
|
850
872
|
|
|
851
|
-
function
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
if (a === undefined) {
|
|
856
|
-
return -1;
|
|
857
|
-
}
|
|
858
|
-
if (b === undefined) {
|
|
859
|
-
return 1;
|
|
860
|
-
}
|
|
861
|
-
return a < b ? -1 : 1;
|
|
873
|
+
function hasForeignKey(lookup: SchemaTableLookup, fk: ForeignKey): boolean {
|
|
874
|
+
return lookup.fkKeys.has(
|
|
875
|
+
`${fk.columns.join(',')}|${fk.references.table}|${fk.references.columns.join(',')}`,
|
|
876
|
+
);
|
|
862
877
|
}
|