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