@prisma-next/target-postgres 0.3.0-dev.15 → 0.3.0-dev.162
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 +31 -29
- 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 -447
- 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 -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,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,21 +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 {
|
|
22
|
+
import { verifySqlSchema } from '@prisma-next/family-sql/schema-verify';
|
|
23
|
+
import type { SchemaIssue } from '@prisma-next/framework-components/control';
|
|
15
24
|
import type {
|
|
16
|
-
ForeignKey,
|
|
17
|
-
SqlContract,
|
|
18
|
-
SqlStorage,
|
|
19
25
|
StorageColumn,
|
|
20
26
|
StorageTable,
|
|
27
|
+
StorageTypeInstance,
|
|
21
28
|
} from '@prisma-next/sql-contract/types';
|
|
29
|
+
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
22
30
|
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
23
|
-
|
|
24
|
-
|
|
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';
|
|
25
61
|
|
|
26
62
|
type PlannerFrameworkComponents = SqlMigrationPlannerPlanOptions extends {
|
|
27
63
|
readonly frameworkComponents: infer T;
|
|
@@ -41,16 +77,8 @@ type PlannerDatabaseDependency = {
|
|
|
41
77
|
readonly id: string;
|
|
42
78
|
readonly label: string;
|
|
43
79
|
readonly install: readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[];
|
|
44
|
-
readonly verifyDatabaseDependencyInstalled: (schema: SqlSchemaIR) => readonly SchemaIssue[];
|
|
45
80
|
};
|
|
46
81
|
|
|
47
|
-
export interface PostgresPlanTargetDetails {
|
|
48
|
-
readonly schema: string;
|
|
49
|
-
readonly objectType: OperationClass;
|
|
50
|
-
readonly name: string;
|
|
51
|
-
readonly table?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
82
|
interface PlannerConfig {
|
|
55
83
|
readonly defaultSchema: string;
|
|
56
84
|
}
|
|
@@ -78,38 +106,72 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
|
|
|
78
106
|
return policyResult;
|
|
79
107
|
}
|
|
80
108
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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);
|
|
85
115
|
|
|
86
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);
|
|
87
141
|
|
|
88
142
|
// Build extension operations from component-owned database dependencies
|
|
89
143
|
operations.push(
|
|
90
144
|
...this.buildDatabaseDependencyOperations(options),
|
|
91
|
-
...
|
|
92
|
-
...
|
|
93
|
-
...this.
|
|
94
|
-
|
|
145
|
+
...storageTypePlan.operations,
|
|
146
|
+
...reconciliationPlan.operations,
|
|
147
|
+
...this.buildTableOperations(
|
|
148
|
+
sortedTables,
|
|
95
149
|
options.schema,
|
|
96
150
|
schemaName,
|
|
151
|
+
codecHooks,
|
|
152
|
+
storageTypes,
|
|
97
153
|
),
|
|
98
|
-
...this.
|
|
99
|
-
|
|
100
|
-
...this.buildForeignKeyOperations(
|
|
101
|
-
options.contract.storage.tables,
|
|
154
|
+
...this.buildColumnOperations(
|
|
155
|
+
sortedTables,
|
|
102
156
|
options.schema,
|
|
157
|
+
schemaLookups,
|
|
103
158
|
schemaName,
|
|
159
|
+
codecHooks,
|
|
160
|
+
storageTypes,
|
|
104
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),
|
|
105
167
|
);
|
|
106
168
|
|
|
107
169
|
const plan = createMigrationPlan<PostgresPlanTargetDetails>({
|
|
108
170
|
targetId: 'postgres',
|
|
109
171
|
origin: null,
|
|
110
172
|
destination: {
|
|
111
|
-
|
|
112
|
-
...(
|
|
173
|
+
storageHash: options.contract.storage.storageHash,
|
|
174
|
+
...ifDefined('profileHash', options.contract.profileHash),
|
|
113
175
|
},
|
|
114
176
|
operations,
|
|
115
177
|
});
|
|
@@ -122,8 +184,8 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
|
|
|
122
184
|
return plannerFailure([
|
|
123
185
|
{
|
|
124
186
|
kind: 'unsupportedOperation',
|
|
125
|
-
summary: '
|
|
126
|
-
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.',
|
|
127
189
|
},
|
|
128
190
|
]);
|
|
129
191
|
}
|
|
@@ -142,14 +204,15 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
|
|
|
142
204
|
const seenDependencyIds = new Set<string>();
|
|
143
205
|
const seenOperationIds = new Set<string>();
|
|
144
206
|
|
|
207
|
+
const installedIds = new Set(options.schema.dependencies.map((d) => d.id));
|
|
208
|
+
|
|
145
209
|
for (const dependency of dependencies) {
|
|
146
210
|
if (seenDependencyIds.has(dependency.id)) {
|
|
147
211
|
continue;
|
|
148
212
|
}
|
|
149
213
|
seenDependencyIds.add(dependency.id);
|
|
150
214
|
|
|
151
|
-
|
|
152
|
-
if (issues.length === 0) {
|
|
215
|
+
if (installedIds.has(dependency.id)) {
|
|
153
216
|
continue;
|
|
154
217
|
}
|
|
155
218
|
|
|
@@ -158,41 +221,77 @@ class PostgresMigrationPlanner implements SqlMigrationPlanner<PostgresPlanTarget
|
|
|
158
221
|
continue;
|
|
159
222
|
}
|
|
160
223
|
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
224
|
operations.push(installOp as SqlMigrationPlanOperation<PostgresPlanTargetDetails>);
|
|
164
225
|
}
|
|
165
226
|
}
|
|
166
227
|
|
|
167
228
|
return operations;
|
|
168
229
|
}
|
|
169
|
-
|
|
230
|
+
|
|
231
|
+
private buildStorageTypeOperations(
|
|
170
232
|
options: PlannerOptionsWithComponents,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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) {
|
|
179
254
|
continue;
|
|
180
255
|
}
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
});
|
|
184
274
|
}
|
|
185
275
|
}
|
|
186
|
-
|
|
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));
|
|
187
284
|
}
|
|
188
285
|
|
|
189
286
|
private buildTableOperations(
|
|
190
|
-
tables:
|
|
287
|
+
tables: ReadonlyArray<[string, StorageTable]>,
|
|
191
288
|
schema: SqlSchemaIR,
|
|
192
289
|
schemaName: string,
|
|
290
|
+
codecHooks: Map<string, CodecControlHooks>,
|
|
291
|
+
storageTypes: Record<string, StorageTypeInstance>,
|
|
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, storageTypes),
|
|
219
318
|
},
|
|
220
319
|
],
|
|
221
320
|
postcheck: [
|
|
@@ -230,91 +329,155 @@ 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>,
|
|
337
|
+
storageTypes: Record<string, StorageTypeInstance>,
|
|
236
338
|
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
237
339
|
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
238
|
-
for (const [tableName, table] of
|
|
340
|
+
for (const [tableName, table] of tables) {
|
|
239
341
|
const schemaTable = schema.tables[tableName];
|
|
240
342
|
if (!schemaTable) {
|
|
241
343
|
continue;
|
|
242
344
|
}
|
|
345
|
+
const schemaLookup = schemaLookups.get(tableName);
|
|
243
346
|
for (const [columnName, column] of sortedEntries(table.columns)) {
|
|
244
347
|
if (schemaTable.columns[columnName]) {
|
|
245
348
|
continue;
|
|
246
349
|
}
|
|
247
|
-
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
|
+
);
|
|
248
363
|
}
|
|
249
364
|
}
|
|
250
365
|
return operations;
|
|
251
366
|
}
|
|
252
367
|
|
|
253
|
-
private buildAddColumnOperation(
|
|
254
|
-
schema: string
|
|
255
|
-
tableName: string
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
]
|
|
293
|
-
: []),
|
|
294
|
-
];
|
|
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
|
+
});
|
|
295
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;
|
|
296
422
|
return {
|
|
297
|
-
|
|
298
|
-
label: `Add column ${columnName} to ${tableName}`,
|
|
299
|
-
summary: `Adds column ${columnName} to table ${tableName}`,
|
|
423
|
+
...buildAddColumnOperationIdentity(schema, tableName, columnName),
|
|
300
424
|
operationClass: 'additive',
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
],
|
|
308
471
|
};
|
|
309
472
|
}
|
|
310
473
|
|
|
311
474
|
private buildPrimaryKeyOperations(
|
|
312
|
-
tables:
|
|
475
|
+
tables: ReadonlyArray<[string, StorageTable]>,
|
|
313
476
|
schema: SqlSchemaIR,
|
|
314
477
|
schemaName: string,
|
|
315
478
|
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
316
479
|
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
317
|
-
for (const [tableName, table] of
|
|
480
|
+
for (const [tableName, table] of tables) {
|
|
318
481
|
if (!table.primaryKey) {
|
|
319
482
|
continue;
|
|
320
483
|
}
|
|
@@ -358,15 +521,15 @@ PRIMARY KEY (${table.primaryKey.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
358
521
|
}
|
|
359
522
|
|
|
360
523
|
private buildUniqueOperations(
|
|
361
|
-
tables:
|
|
362
|
-
|
|
524
|
+
tables: ReadonlyArray<[string, StorageTable]>,
|
|
525
|
+
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
363
526
|
schemaName: string,
|
|
364
527
|
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
365
528
|
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
366
|
-
for (const [tableName, table] of
|
|
367
|
-
const
|
|
529
|
+
for (const [tableName, table] of tables) {
|
|
530
|
+
const lookup = schemaLookups.get(tableName);
|
|
368
531
|
for (const unique of table.uniques) {
|
|
369
|
-
if (
|
|
532
|
+
if (lookup && hasUniqueConstraint(lookup, unique.columns)) {
|
|
370
533
|
continue;
|
|
371
534
|
}
|
|
372
535
|
const constraintName = unique.name ?? `${tableName}_${unique.columns.join('_')}_key`;
|
|
@@ -382,7 +545,12 @@ PRIMARY KEY (${table.primaryKey.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
382
545
|
precheck: [
|
|
383
546
|
{
|
|
384
547
|
description: `ensure unique constraint "${constraintName}" is missing`,
|
|
385
|
-
sql: constraintExistsCheck({
|
|
548
|
+
sql: constraintExistsCheck({
|
|
549
|
+
constraintName,
|
|
550
|
+
schema: schemaName,
|
|
551
|
+
table: tableName,
|
|
552
|
+
exists: false,
|
|
553
|
+
}),
|
|
386
554
|
},
|
|
387
555
|
],
|
|
388
556
|
execute: [
|
|
@@ -396,7 +564,7 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
396
564
|
postcheck: [
|
|
397
565
|
{
|
|
398
566
|
description: `verify unique constraint "${constraintName}" exists`,
|
|
399
|
-
sql: constraintExistsCheck({ constraintName, schema: schemaName }),
|
|
567
|
+
sql: constraintExistsCheck({ constraintName, schema: schemaName, table: tableName }),
|
|
400
568
|
},
|
|
401
569
|
],
|
|
402
570
|
});
|
|
@@ -406,18 +574,18 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
406
574
|
}
|
|
407
575
|
|
|
408
576
|
private buildIndexOperations(
|
|
409
|
-
tables:
|
|
410
|
-
|
|
577
|
+
tables: ReadonlyArray<[string, StorageTable]>,
|
|
578
|
+
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
411
579
|
schemaName: string,
|
|
412
580
|
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
413
581
|
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
414
|
-
for (const [tableName, table] of
|
|
415
|
-
const
|
|
582
|
+
for (const [tableName, table] of tables) {
|
|
583
|
+
const lookup = schemaLookups.get(tableName);
|
|
416
584
|
for (const index of table.indexes) {
|
|
417
|
-
if (
|
|
585
|
+
if (lookup && hasIndex(lookup, index.columns)) {
|
|
418
586
|
continue;
|
|
419
587
|
}
|
|
420
|
-
const indexName = index.name ??
|
|
588
|
+
const indexName = index.name ?? defaultIndexName(tableName, index.columns);
|
|
421
589
|
operations.push({
|
|
422
590
|
id: `index.${tableName}.${indexName}`,
|
|
423
591
|
label: `Create index ${indexName} on ${tableName}`,
|
|
@@ -454,16 +622,76 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
454
622
|
return operations;
|
|
455
623
|
}
|
|
456
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
|
+
|
|
457
684
|
private buildForeignKeyOperations(
|
|
458
|
-
tables:
|
|
459
|
-
|
|
685
|
+
tables: ReadonlyArray<[string, StorageTable]>,
|
|
686
|
+
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
460
687
|
schemaName: string,
|
|
461
688
|
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
462
689
|
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
463
|
-
for (const [tableName, table] of
|
|
464
|
-
const
|
|
690
|
+
for (const [tableName, table] of tables) {
|
|
691
|
+
const lookup = schemaLookups.get(tableName);
|
|
465
692
|
for (const foreignKey of table.foreignKeys) {
|
|
466
|
-
if (
|
|
693
|
+
if (foreignKey.constraint === false) continue;
|
|
694
|
+
if (lookup && hasForeignKey(lookup, foreignKey)) {
|
|
467
695
|
continue;
|
|
468
696
|
}
|
|
469
697
|
const fkName = foreignKey.name ?? `${tableName}_${foreignKey.columns.join('_')}_fkey`;
|
|
@@ -482,6 +710,7 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
482
710
|
sql: constraintExistsCheck({
|
|
483
711
|
constraintName: fkName,
|
|
484
712
|
schema: schemaName,
|
|
713
|
+
table: tableName,
|
|
485
714
|
exists: false,
|
|
486
715
|
}),
|
|
487
716
|
},
|
|
@@ -489,18 +718,17 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
489
718
|
execute: [
|
|
490
719
|
{
|
|
491
720
|
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(', ')})`,
|
|
721
|
+
sql: buildForeignKeySql(schemaName, tableName, fkName, foreignKey),
|
|
498
722
|
},
|
|
499
723
|
],
|
|
500
724
|
postcheck: [
|
|
501
725
|
{
|
|
502
726
|
description: `verify foreign key "${fkName}" exists`,
|
|
503
|
-
sql: constraintExistsCheck({
|
|
727
|
+
sql: constraintExistsCheck({
|
|
728
|
+
constraintName: fkName,
|
|
729
|
+
schema: schemaName,
|
|
730
|
+
table: tableName,
|
|
731
|
+
}),
|
|
504
732
|
},
|
|
505
733
|
],
|
|
506
734
|
});
|
|
@@ -515,348 +743,83 @@ REFERENCES ${qualifyTableName(schemaName, foreignKey.references.table)} (${forei
|
|
|
515
743
|
schema: string,
|
|
516
744
|
table?: string,
|
|
517
745
|
): PostgresPlanTargetDetails {
|
|
518
|
-
return
|
|
519
|
-
schema,
|
|
520
|
-
objectType,
|
|
521
|
-
name,
|
|
522
|
-
...(table ? { table } : {}),
|
|
523
|
-
};
|
|
746
|
+
return buildTargetDetails(objectType, name, schema, table);
|
|
524
747
|
}
|
|
525
748
|
|
|
526
|
-
private
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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[] {
|
|
532
762
|
const verifyOptions: VerifySqlSchemaOptionsWithComponents = {
|
|
533
763
|
contract: options.contract,
|
|
534
764
|
schema: options.schema,
|
|
535
|
-
strict
|
|
765
|
+
strict,
|
|
536
766
|
typeMetadataRegistry: new Map(),
|
|
537
767
|
frameworkComponents: options.frameworkComponents,
|
|
768
|
+
normalizeDefault: parsePostgresDefault,
|
|
769
|
+
normalizeNativeType: normalizeSchemaNativeType,
|
|
538
770
|
};
|
|
539
771
|
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
|
-
};
|
|
772
|
+
return verifyResult.schema.issues;
|
|
597
773
|
}
|
|
598
774
|
}
|
|
599
775
|
|
|
600
|
-
function
|
|
601
|
-
readonly
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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) {
|
|
606
787
|
return false;
|
|
607
788
|
}
|
|
608
|
-
const record = component as Record<string, unknown>;
|
|
609
789
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
+
}
|
|
613
797
|
}
|
|
614
798
|
|
|
615
|
-
|
|
616
|
-
|
|
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
|
+
}
|
|
617
806
|
}
|
|
618
|
-
|
|
619
|
-
return
|
|
807
|
+
|
|
808
|
+
return true;
|
|
620
809
|
}
|
|
621
810
|
|
|
622
811
|
function sortDependencies(
|
|
623
812
|
dependencies: ReadonlyArray<PlannerDatabaseDependency>,
|
|
624
813
|
): ReadonlyArray<PlannerDatabaseDependency> {
|
|
625
|
-
if (dependencies.length <= 1) {
|
|
626
|
-
return dependencies;
|
|
627
|
-
}
|
|
628
814
|
return [...dependencies].sort((a, b) => a.id.localeCompare(b.id));
|
|
629
815
|
}
|
|
630
816
|
|
|
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, "''");
|
|
817
|
+
function isPostgresPlannerDependency(
|
|
818
|
+
dependency: ComponentDatabaseDependency<unknown>,
|
|
819
|
+
): dependency is PlannerDatabaseDependency {
|
|
820
|
+
return dependency.install.every((operation) => operation.target.id === 'postgres');
|
|
671
821
|
}
|
|
672
822
|
|
|
673
823
|
function sortedEntries<V>(record: Readonly<Record<string, V>>): Array<[string, V]> {
|
|
674
824
|
return Object.entries(record).sort(([a], [b]) => a.localeCompare(b)) as Array<[string, V]>;
|
|
675
825
|
}
|
|
676
|
-
|
|
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
|
-
function tableHasPrimaryKeyCheck(
|
|
753
|
-
schema: string,
|
|
754
|
-
table: string,
|
|
755
|
-
exists: boolean,
|
|
756
|
-
constraintName?: string,
|
|
757
|
-
): string {
|
|
758
|
-
const comparison = exists ? '' : 'NOT ';
|
|
759
|
-
const constraintFilter = constraintName
|
|
760
|
-
? `AND c2.relname = '${escapeLiteral(constraintName)}'`
|
|
761
|
-
: '';
|
|
762
|
-
return `SELECT ${comparison}EXISTS (
|
|
763
|
-
SELECT 1
|
|
764
|
-
FROM pg_index i
|
|
765
|
-
JOIN pg_class c ON c.oid = i.indrelid
|
|
766
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
767
|
-
LEFT JOIN pg_class c2 ON c2.oid = i.indexrelid
|
|
768
|
-
WHERE n.nspname = '${escapeLiteral(schema)}'
|
|
769
|
-
AND c.relname = '${escapeLiteral(table)}'
|
|
770
|
-
AND i.indisprimary
|
|
771
|
-
${constraintFilter}
|
|
772
|
-
)`;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function hasUniqueConstraint(
|
|
776
|
-
table: SqlSchemaIR['tables'][string],
|
|
777
|
-
columns: readonly string[],
|
|
778
|
-
): boolean {
|
|
779
|
-
return table.uniques.some((unique) => arraysEqual(unique.columns, columns));
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function hasIndex(table: SqlSchemaIR['tables'][string], columns: readonly string[]): boolean {
|
|
783
|
-
return table.indexes.some((index) => !index.unique && arraysEqual(index.columns, columns));
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function hasForeignKey(table: SqlSchemaIR['tables'][string], fk: ForeignKey): boolean {
|
|
787
|
-
return table.foreignKeys.some(
|
|
788
|
-
(candidate) =>
|
|
789
|
-
arraysEqual(candidate.columns, fk.columns) &&
|
|
790
|
-
candidate.referencedTable === fk.references.table &&
|
|
791
|
-
arraysEqual(candidate.referencedColumns, fk.references.columns),
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function isAdditiveIssue(issue: SchemaIssue): boolean {
|
|
796
|
-
switch (issue.kind) {
|
|
797
|
-
case 'missing_table':
|
|
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;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function conflictComparator(a: SqlPlannerConflict, b: SqlPlannerConflict): number {
|
|
831
|
-
if (a.kind !== b.kind) {
|
|
832
|
-
return a.kind < b.kind ? -1 : 1;
|
|
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);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function compareStrings(a?: string, b?: string): number {
|
|
852
|
-
if (a === b) {
|
|
853
|
-
return 0;
|
|
854
|
-
}
|
|
855
|
-
if (a === undefined) {
|
|
856
|
-
return -1;
|
|
857
|
-
}
|
|
858
|
-
if (b === undefined) {
|
|
859
|
-
return 1;
|
|
860
|
-
}
|
|
861
|
-
return a < b ? -1 : 1;
|
|
862
|
-
}
|