@prisma-next/target-postgres 0.4.0-dev.9 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/control.d.mts +1 -9
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +1693 -4798
- package/dist/control.mjs.map +1 -1
- package/dist/migration.d.mts +164 -0
- package/dist/migration.d.mts.map +1 -0
- package/dist/migration.mjs +446 -0
- package/dist/migration.mjs.map +1 -0
- package/dist/planner-target-details-MXb3oeul.d.mts +11 -0
- package/dist/planner-target-details-MXb3oeul.d.mts.map +1 -0
- package/dist/postgres-migration-BsHJHV9O.mjs +2793 -0
- package/dist/postgres-migration-BsHJHV9O.mjs.map +1 -0
- package/package.json +20 -18
- package/src/core/migrations/issue-planner.ts +832 -0
- package/src/core/migrations/op-factory-call.ts +862 -0
- package/src/core/migrations/operations/columns.ts +285 -0
- package/src/core/migrations/operations/constraints.ts +191 -0
- package/src/core/migrations/operations/data-transform.ts +113 -0
- package/src/core/migrations/operations/dependencies.ts +36 -0
- package/src/core/migrations/operations/enums.ts +113 -0
- package/src/core/migrations/operations/indexes.ts +61 -0
- package/src/core/migrations/operations/raw.ts +15 -0
- package/src/core/migrations/operations/shared.ts +67 -0
- package/src/core/migrations/operations/tables.ts +63 -0
- package/src/core/migrations/planner-produced-postgres-migration.ts +67 -0
- package/src/core/migrations/planner-strategies.ts +592 -151
- package/src/core/migrations/planner-target-details.ts +0 -6
- package/src/core/migrations/planner.ts +63 -781
- package/src/core/migrations/postgres-migration.ts +20 -0
- package/src/core/migrations/render-ops.ts +9 -0
- package/src/core/migrations/render-typescript.ts +95 -0
- package/src/exports/control.ts +9 -142
- package/src/exports/migration.ts +40 -0
- package/dist/migration-builders.d.mts +0 -88
- package/dist/migration-builders.d.mts.map +0 -1
- package/dist/migration-builders.mjs +0 -3
- package/dist/operation-descriptors-CxymFSgK.mjs +0 -52
- package/dist/operation-descriptors-CxymFSgK.mjs.map +0 -1
- package/src/core/migrations/descriptor-planner.ts +0 -464
- package/src/core/migrations/operation-descriptors.ts +0 -166
- package/src/core/migrations/operation-resolver.ts +0 -929
- package/src/core/migrations/planner-reconciliation.ts +0 -798
- package/src/core/migrations/scaffolding.ts +0 -140
- package/src/exports/migration-builders.ts +0 -56
|
@@ -1,798 +0,0 @@
|
|
|
1
|
-
import { quoteIdentifier } from '@prisma-next/adapter-postgres/control';
|
|
2
|
-
import type { Contract } from '@prisma-next/contract/types';
|
|
3
|
-
import type {
|
|
4
|
-
CodecControlHooks,
|
|
5
|
-
MigrationOperationPolicy,
|
|
6
|
-
SqlMigrationPlanOperation,
|
|
7
|
-
SqlPlannerConflict,
|
|
8
|
-
} from '@prisma-next/family-sql/control';
|
|
9
|
-
import type { SchemaIssue } from '@prisma-next/framework-components/control';
|
|
10
|
-
import type {
|
|
11
|
-
SqlStorage,
|
|
12
|
-
StorageColumn,
|
|
13
|
-
StorageTypeInstance,
|
|
14
|
-
} from '@prisma-next/sql-contract/types';
|
|
15
|
-
import { invariant } from '@prisma-next/utils/assertions';
|
|
16
|
-
import { ifDefined } from '@prisma-next/utils/defined';
|
|
17
|
-
import { buildColumnDefaultSql, buildColumnTypeSql } from './planner-ddl-builders';
|
|
18
|
-
import {
|
|
19
|
-
buildExpectedFormatType,
|
|
20
|
-
columnDefaultExistsCheck,
|
|
21
|
-
columnExistsCheck,
|
|
22
|
-
columnNullabilityCheck,
|
|
23
|
-
columnTypeCheck,
|
|
24
|
-
constraintExistsCheck,
|
|
25
|
-
qualifyTableName,
|
|
26
|
-
toRegclassLiteral,
|
|
27
|
-
} from './planner-sql-checks';
|
|
28
|
-
import {
|
|
29
|
-
buildTargetDetails,
|
|
30
|
-
type PlanningMode,
|
|
31
|
-
type PostgresPlanTargetDetails,
|
|
32
|
-
} from './planner-target-details';
|
|
33
|
-
|
|
34
|
-
// ============================================================================
|
|
35
|
-
// Public API
|
|
36
|
-
// ============================================================================
|
|
37
|
-
|
|
38
|
-
export function buildReconciliationPlan(options: {
|
|
39
|
-
readonly contract: Contract<SqlStorage>;
|
|
40
|
-
readonly issues: readonly SchemaIssue[];
|
|
41
|
-
readonly schemaName: string;
|
|
42
|
-
readonly mode: PlanningMode;
|
|
43
|
-
readonly policy: MigrationOperationPolicy;
|
|
44
|
-
readonly codecHooks: Map<string, CodecControlHooks>;
|
|
45
|
-
}): {
|
|
46
|
-
readonly operations: readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[];
|
|
47
|
-
readonly conflicts: readonly SqlPlannerConflict[];
|
|
48
|
-
} {
|
|
49
|
-
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
50
|
-
const conflicts: SqlPlannerConflict[] = [];
|
|
51
|
-
const { mode } = options;
|
|
52
|
-
const seenOperationIds = new Set<string>();
|
|
53
|
-
|
|
54
|
-
for (const issue of sortSchemaIssues(options.issues)) {
|
|
55
|
-
if (isAdditiveIssue(issue)) {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const operation = buildReconciliationOperationFromIssue({
|
|
60
|
-
issue,
|
|
61
|
-
contract: options.contract,
|
|
62
|
-
schemaName: options.schemaName,
|
|
63
|
-
mode,
|
|
64
|
-
codecHooks: options.codecHooks,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
if (operation) {
|
|
68
|
-
// Skip duplicates: different schema issues may produce the same operation id
|
|
69
|
-
// (e.g., extra_unique_constraint and extra_index on the same object).
|
|
70
|
-
if (!seenOperationIds.has(operation.id)) {
|
|
71
|
-
seenOperationIds.add(operation.id);
|
|
72
|
-
if (options.policy.allowedOperationClasses.includes(operation.operationClass)) {
|
|
73
|
-
operations.push(operation);
|
|
74
|
-
} else {
|
|
75
|
-
const conflict = convertIssueToConflict(issue);
|
|
76
|
-
if (conflict) {
|
|
77
|
-
conflicts.push(conflict);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
const conflict = convertIssueToConflict(issue);
|
|
83
|
-
if (conflict) {
|
|
84
|
-
conflicts.push(conflict);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
operations,
|
|
91
|
-
conflicts: conflicts.sort(conflictComparator),
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ============================================================================
|
|
96
|
-
// Issue Classification
|
|
97
|
-
// ============================================================================
|
|
98
|
-
|
|
99
|
-
function isAdditiveIssue(issue: SchemaIssue): boolean {
|
|
100
|
-
switch (issue.kind) {
|
|
101
|
-
case 'type_missing':
|
|
102
|
-
case 'type_values_mismatch':
|
|
103
|
-
case 'enum_values_changed':
|
|
104
|
-
case 'missing_table':
|
|
105
|
-
case 'missing_column':
|
|
106
|
-
case 'dependency_missing':
|
|
107
|
-
return true;
|
|
108
|
-
case 'primary_key_mismatch':
|
|
109
|
-
return issue.actual === undefined;
|
|
110
|
-
case 'unique_constraint_mismatch':
|
|
111
|
-
case 'index_mismatch':
|
|
112
|
-
case 'foreign_key_mismatch':
|
|
113
|
-
return issue.indexOrConstraint === undefined;
|
|
114
|
-
default:
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ============================================================================
|
|
120
|
-
// Operation Builders
|
|
121
|
-
// ============================================================================
|
|
122
|
-
|
|
123
|
-
function buildReconciliationOperationFromIssue(options: {
|
|
124
|
-
readonly issue: SchemaIssue;
|
|
125
|
-
readonly contract: Contract<SqlStorage>;
|
|
126
|
-
readonly schemaName: string;
|
|
127
|
-
readonly mode: PlanningMode;
|
|
128
|
-
readonly codecHooks: Map<string, CodecControlHooks>;
|
|
129
|
-
}): SqlMigrationPlanOperation<PostgresPlanTargetDetails> | null {
|
|
130
|
-
const { issue, contract, schemaName, mode, codecHooks } = options;
|
|
131
|
-
const storageTypes = contract.storage.types ?? {};
|
|
132
|
-
switch (issue.kind) {
|
|
133
|
-
case 'extra_table':
|
|
134
|
-
if (!mode.allowDestructive || !issue.table) {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
return buildDropTableOperation(schemaName, issue.table);
|
|
138
|
-
|
|
139
|
-
case 'extra_column':
|
|
140
|
-
if (!mode.allowDestructive || !issue.table || !issue.column) {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
return buildDropColumnOperation(schemaName, issue.table, issue.column);
|
|
144
|
-
|
|
145
|
-
case 'extra_index':
|
|
146
|
-
if (!mode.allowDestructive || !issue.table || !issue.indexOrConstraint) {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
return buildDropIndexOperation(schemaName, issue.table, issue.indexOrConstraint);
|
|
150
|
-
|
|
151
|
-
case 'extra_foreign_key':
|
|
152
|
-
case 'extra_unique_constraint': {
|
|
153
|
-
if (!mode.allowDestructive || !issue.table || !issue.indexOrConstraint) {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
const constraintKind = issue.kind === 'extra_foreign_key' ? 'foreignKey' : 'unique';
|
|
157
|
-
return buildDropConstraintOperation(
|
|
158
|
-
schemaName,
|
|
159
|
-
issue.table,
|
|
160
|
-
issue.indexOrConstraint,
|
|
161
|
-
constraintKind,
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
case 'extra_primary_key': {
|
|
166
|
-
if (!mode.allowDestructive || !issue.table) {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
const constraintName = issue.indexOrConstraint ?? `${issue.table}_pkey`;
|
|
170
|
-
return buildDropConstraintOperation(schemaName, issue.table, constraintName, 'primaryKey');
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
case 'nullability_mismatch': {
|
|
174
|
-
if (!issue.table || !issue.column) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
if (issue.expected === 'true') {
|
|
178
|
-
// Contract wants nullable, DB has NOT NULL → widening
|
|
179
|
-
return mode.allowWidening
|
|
180
|
-
? buildDropNotNullOperation(schemaName, issue.table, issue.column)
|
|
181
|
-
: null;
|
|
182
|
-
}
|
|
183
|
-
// Contract wants NOT NULL, DB has nullable → destructive
|
|
184
|
-
return mode.allowDestructive
|
|
185
|
-
? buildSetNotNullOperation(schemaName, issue.table, issue.column)
|
|
186
|
-
: null;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
case 'type_mismatch': {
|
|
190
|
-
if (!mode.allowDestructive || !issue.table || !issue.column) {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
const contractColumn = getContractColumn(contract, issue.table, issue.column);
|
|
194
|
-
if (!contractColumn) {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
return buildAlterColumnTypeOperation(
|
|
198
|
-
schemaName,
|
|
199
|
-
issue.table,
|
|
200
|
-
issue.column,
|
|
201
|
-
contractColumn,
|
|
202
|
-
codecHooks,
|
|
203
|
-
storageTypes,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
case 'default_missing': {
|
|
208
|
-
if (!issue.table || !issue.column) {
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
const contractColMissing = getContractColumn(contract, issue.table, issue.column);
|
|
212
|
-
if (!contractColMissing) {
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
// NOTE: Being in the `default_missing` case means the verifier found the contract expects a default, so it should exist here. We must still narrow.
|
|
216
|
-
invariant(
|
|
217
|
-
contractColMissing.default !== undefined,
|
|
218
|
-
`default_missing issue for "${issue.table}"."${issue.column}" but contract column has no default`,
|
|
219
|
-
);
|
|
220
|
-
return buildDefaultOperation(
|
|
221
|
-
schemaName,
|
|
222
|
-
issue.table,
|
|
223
|
-
issue.column,
|
|
224
|
-
contractColMissing,
|
|
225
|
-
contractColMissing.default,
|
|
226
|
-
'additive',
|
|
227
|
-
'Set',
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
case 'default_mismatch': {
|
|
232
|
-
if (!issue.table || !issue.column) {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
if (!mode.allowWidening) {
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
const contractColMismatch = getContractColumn(contract, issue.table, issue.column);
|
|
239
|
-
if (!contractColMismatch) {
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
// NOTE: Being in the `default_mismatch` case means the verifier found the contract expects a different default, so it should exist here. We must still narrow.
|
|
243
|
-
invariant(
|
|
244
|
-
contractColMismatch.default !== undefined,
|
|
245
|
-
`default_mismatch issue for "${issue.table}"."${issue.column}" but contract column has no default`,
|
|
246
|
-
);
|
|
247
|
-
return buildDefaultOperation(
|
|
248
|
-
schemaName,
|
|
249
|
-
issue.table,
|
|
250
|
-
issue.column,
|
|
251
|
-
contractColMismatch,
|
|
252
|
-
contractColMismatch.default,
|
|
253
|
-
'widening',
|
|
254
|
-
'Change',
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
case 'extra_default': {
|
|
259
|
-
if (!issue.table || !issue.column) {
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
262
|
-
if (!mode.allowDestructive) {
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
return buildDropDefaultOperation(schemaName, issue.table, issue.column);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Remaining issue kinds (primary_key_mismatch, unique_constraint_mismatch,
|
|
269
|
-
// index_mismatch, foreign_key_mismatch) do not yet have reconciliation operation
|
|
270
|
-
// builders. They fall through to the caller, which converts them to conflicts via
|
|
271
|
-
// convertIssueToConflict. When a new SchemaIssue kind is added, add a case here if
|
|
272
|
-
// the planner can emit an operation for it; otherwise it becomes a conflict.
|
|
273
|
-
default:
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function getContractColumn(
|
|
279
|
-
contract: Contract<SqlStorage>,
|
|
280
|
-
tableName: string,
|
|
281
|
-
columnName: string,
|
|
282
|
-
): StorageColumn | null {
|
|
283
|
-
const table = contract.storage.tables[tableName];
|
|
284
|
-
if (!table) {
|
|
285
|
-
return null;
|
|
286
|
-
}
|
|
287
|
-
return table.columns[columnName] ?? null;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function buildDropTableOperation(
|
|
291
|
-
schemaName: string,
|
|
292
|
-
tableName: string,
|
|
293
|
-
): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
|
|
294
|
-
return {
|
|
295
|
-
id: `dropTable.${tableName}`,
|
|
296
|
-
label: `Drop table ${tableName}`,
|
|
297
|
-
summary: `Drops extra table ${tableName}`,
|
|
298
|
-
operationClass: 'destructive',
|
|
299
|
-
target: {
|
|
300
|
-
id: 'postgres',
|
|
301
|
-
details: buildTargetDetails('table', tableName, schemaName),
|
|
302
|
-
},
|
|
303
|
-
precheck: [
|
|
304
|
-
{
|
|
305
|
-
description: `ensure table "${tableName}" exists`,
|
|
306
|
-
sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, tableName)}) IS NOT NULL`,
|
|
307
|
-
},
|
|
308
|
-
],
|
|
309
|
-
execute: [
|
|
310
|
-
{
|
|
311
|
-
description: `drop table "${tableName}"`,
|
|
312
|
-
sql: `DROP TABLE ${qualifyTableName(schemaName, tableName)}`,
|
|
313
|
-
},
|
|
314
|
-
],
|
|
315
|
-
postcheck: [
|
|
316
|
-
{
|
|
317
|
-
description: `verify table "${tableName}" is removed`,
|
|
318
|
-
sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, tableName)}) IS NULL`,
|
|
319
|
-
},
|
|
320
|
-
],
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function buildDropColumnOperation(
|
|
325
|
-
schemaName: string,
|
|
326
|
-
tableName: string,
|
|
327
|
-
columnName: string,
|
|
328
|
-
): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
|
|
329
|
-
return {
|
|
330
|
-
id: `dropColumn.${tableName}.${columnName}`,
|
|
331
|
-
label: `Drop column ${columnName} from ${tableName}`,
|
|
332
|
-
summary: `Drops extra column ${columnName} from table ${tableName}`,
|
|
333
|
-
operationClass: 'destructive',
|
|
334
|
-
target: {
|
|
335
|
-
id: 'postgres',
|
|
336
|
-
details: buildTargetDetails('column', columnName, schemaName, tableName),
|
|
337
|
-
},
|
|
338
|
-
precheck: [
|
|
339
|
-
{
|
|
340
|
-
description: `ensure column "${columnName}" exists`,
|
|
341
|
-
sql: columnExistsCheck({ schema: schemaName, table: tableName, column: columnName }),
|
|
342
|
-
},
|
|
343
|
-
],
|
|
344
|
-
execute: [
|
|
345
|
-
{
|
|
346
|
-
description: `drop column "${columnName}"`,
|
|
347
|
-
sql: `ALTER TABLE ${qualifyTableName(schemaName, tableName)} DROP COLUMN ${quoteIdentifier(columnName)}`,
|
|
348
|
-
},
|
|
349
|
-
],
|
|
350
|
-
postcheck: [
|
|
351
|
-
{
|
|
352
|
-
description: `verify column "${columnName}" is removed`,
|
|
353
|
-
sql: columnExistsCheck({
|
|
354
|
-
schema: schemaName,
|
|
355
|
-
table: tableName,
|
|
356
|
-
column: columnName,
|
|
357
|
-
exists: false,
|
|
358
|
-
}),
|
|
359
|
-
},
|
|
360
|
-
],
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function buildDropIndexOperation(
|
|
365
|
-
schemaName: string,
|
|
366
|
-
tableName: string,
|
|
367
|
-
indexName: string,
|
|
368
|
-
): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
|
|
369
|
-
return {
|
|
370
|
-
id: `dropIndex.${tableName}.${indexName}`,
|
|
371
|
-
label: `Drop index ${indexName} on ${tableName}`,
|
|
372
|
-
summary: `Drops extra index ${indexName} on table ${tableName}`,
|
|
373
|
-
operationClass: 'destructive',
|
|
374
|
-
target: {
|
|
375
|
-
id: 'postgres',
|
|
376
|
-
details: buildTargetDetails('index', indexName, schemaName, tableName),
|
|
377
|
-
},
|
|
378
|
-
precheck: [
|
|
379
|
-
{
|
|
380
|
-
description: `ensure index "${indexName}" exists`,
|
|
381
|
-
sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, indexName)}) IS NOT NULL`,
|
|
382
|
-
},
|
|
383
|
-
],
|
|
384
|
-
execute: [
|
|
385
|
-
{
|
|
386
|
-
description: `drop index "${indexName}"`,
|
|
387
|
-
sql: `DROP INDEX ${qualifyTableName(schemaName, indexName)}`,
|
|
388
|
-
},
|
|
389
|
-
],
|
|
390
|
-
postcheck: [
|
|
391
|
-
{
|
|
392
|
-
description: `verify index "${indexName}" is removed`,
|
|
393
|
-
sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, indexName)}) IS NULL`,
|
|
394
|
-
},
|
|
395
|
-
],
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function buildDropConstraintOperation(
|
|
400
|
-
schemaName: string,
|
|
401
|
-
tableName: string,
|
|
402
|
-
constraintName: string,
|
|
403
|
-
constraintKind: 'foreignKey' | 'unique' | 'primaryKey',
|
|
404
|
-
): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
|
|
405
|
-
return {
|
|
406
|
-
id: `dropConstraint.${tableName}.${constraintName}`,
|
|
407
|
-
label: `Drop constraint ${constraintName} on ${tableName}`,
|
|
408
|
-
summary: `Drops extra constraint ${constraintName} on table ${tableName}`,
|
|
409
|
-
operationClass: 'destructive',
|
|
410
|
-
target: {
|
|
411
|
-
id: 'postgres',
|
|
412
|
-
details: buildTargetDetails(constraintKind, constraintName, schemaName, tableName),
|
|
413
|
-
},
|
|
414
|
-
precheck: [
|
|
415
|
-
{
|
|
416
|
-
description: `ensure constraint "${constraintName}" exists`,
|
|
417
|
-
sql: constraintExistsCheck({ constraintName, schema: schemaName, table: tableName }),
|
|
418
|
-
},
|
|
419
|
-
],
|
|
420
|
-
execute: [
|
|
421
|
-
{
|
|
422
|
-
description: `drop constraint "${constraintName}"`,
|
|
423
|
-
sql: `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
|
|
424
|
-
DROP CONSTRAINT ${quoteIdentifier(constraintName)}`,
|
|
425
|
-
},
|
|
426
|
-
],
|
|
427
|
-
postcheck: [
|
|
428
|
-
{
|
|
429
|
-
description: `verify constraint "${constraintName}" is removed`,
|
|
430
|
-
sql: constraintExistsCheck({
|
|
431
|
-
constraintName,
|
|
432
|
-
schema: schemaName,
|
|
433
|
-
table: tableName,
|
|
434
|
-
exists: false,
|
|
435
|
-
}),
|
|
436
|
-
},
|
|
437
|
-
],
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function buildDropNotNullOperation(
|
|
442
|
-
schemaName: string,
|
|
443
|
-
tableName: string,
|
|
444
|
-
columnName: string,
|
|
445
|
-
): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
|
|
446
|
-
return {
|
|
447
|
-
id: `alterNullability.${tableName}.${columnName}`,
|
|
448
|
-
label: `Relax nullability for ${columnName} on ${tableName}`,
|
|
449
|
-
summary: `Drops NOT NULL constraint for ${columnName} on table ${tableName}`,
|
|
450
|
-
operationClass: 'widening',
|
|
451
|
-
target: {
|
|
452
|
-
id: 'postgres',
|
|
453
|
-
details: buildTargetDetails('column', columnName, schemaName, tableName),
|
|
454
|
-
},
|
|
455
|
-
precheck: [
|
|
456
|
-
{
|
|
457
|
-
description: `ensure column "${columnName}" exists`,
|
|
458
|
-
sql: columnExistsCheck({ schema: schemaName, table: tableName, column: columnName }),
|
|
459
|
-
},
|
|
460
|
-
],
|
|
461
|
-
execute: [
|
|
462
|
-
{
|
|
463
|
-
description: `drop NOT NULL from "${columnName}"`,
|
|
464
|
-
sql: `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
|
|
465
|
-
ALTER COLUMN ${quoteIdentifier(columnName)} DROP NOT NULL`,
|
|
466
|
-
},
|
|
467
|
-
],
|
|
468
|
-
postcheck: [
|
|
469
|
-
{
|
|
470
|
-
description: `verify "${columnName}" is nullable`,
|
|
471
|
-
sql: columnNullabilityCheck({
|
|
472
|
-
schema: schemaName,
|
|
473
|
-
table: tableName,
|
|
474
|
-
column: columnName,
|
|
475
|
-
nullable: true,
|
|
476
|
-
}),
|
|
477
|
-
},
|
|
478
|
-
],
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function buildSetNotNullOperation(
|
|
483
|
-
schemaName: string,
|
|
484
|
-
tableName: string,
|
|
485
|
-
columnName: string,
|
|
486
|
-
): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
|
|
487
|
-
const qualified = qualifyTableName(schemaName, tableName);
|
|
488
|
-
return {
|
|
489
|
-
id: `alterNullability.${tableName}.${columnName}`,
|
|
490
|
-
label: `Enforce NOT NULL for ${columnName} on ${tableName}`,
|
|
491
|
-
summary: `Sets NOT NULL on ${columnName} for table ${tableName}`,
|
|
492
|
-
operationClass: 'destructive',
|
|
493
|
-
target: {
|
|
494
|
-
id: 'postgres',
|
|
495
|
-
details: buildTargetDetails('column', columnName, schemaName, tableName),
|
|
496
|
-
},
|
|
497
|
-
precheck: [
|
|
498
|
-
{
|
|
499
|
-
description: `ensure column "${columnName}" exists`,
|
|
500
|
-
sql: columnExistsCheck({ schema: schemaName, table: tableName, column: columnName }),
|
|
501
|
-
},
|
|
502
|
-
{
|
|
503
|
-
description: `ensure "${columnName}" has no NULL values`,
|
|
504
|
-
sql: `SELECT NOT EXISTS (
|
|
505
|
-
SELECT 1 FROM ${qualified}
|
|
506
|
-
WHERE ${quoteIdentifier(columnName)} IS NULL
|
|
507
|
-
LIMIT 1
|
|
508
|
-
)`,
|
|
509
|
-
},
|
|
510
|
-
],
|
|
511
|
-
execute: [
|
|
512
|
-
{
|
|
513
|
-
description: `set NOT NULL on "${columnName}"`,
|
|
514
|
-
sql: `ALTER TABLE ${qualified}
|
|
515
|
-
ALTER COLUMN ${quoteIdentifier(columnName)} SET NOT NULL`,
|
|
516
|
-
},
|
|
517
|
-
],
|
|
518
|
-
postcheck: [
|
|
519
|
-
{
|
|
520
|
-
description: `verify "${columnName}" is NOT NULL`,
|
|
521
|
-
sql: columnNullabilityCheck({
|
|
522
|
-
schema: schemaName,
|
|
523
|
-
table: tableName,
|
|
524
|
-
column: columnName,
|
|
525
|
-
nullable: false,
|
|
526
|
-
}),
|
|
527
|
-
},
|
|
528
|
-
],
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function buildAlterColumnTypeOperation(
|
|
533
|
-
schemaName: string,
|
|
534
|
-
tableName: string,
|
|
535
|
-
columnName: string,
|
|
536
|
-
column: StorageColumn,
|
|
537
|
-
codecHooks: Map<string, CodecControlHooks>,
|
|
538
|
-
storageTypes: Record<string, StorageTypeInstance>,
|
|
539
|
-
): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
|
|
540
|
-
const qualified = qualifyTableName(schemaName, tableName);
|
|
541
|
-
const expectedType = buildColumnTypeSql(column, codecHooks, storageTypes, false);
|
|
542
|
-
return {
|
|
543
|
-
id: `alterType.${tableName}.${columnName}`,
|
|
544
|
-
label: `Alter type for ${columnName} on ${tableName}`,
|
|
545
|
-
summary: `Changes type of ${columnName} to ${expectedType}`,
|
|
546
|
-
operationClass: 'destructive',
|
|
547
|
-
target: {
|
|
548
|
-
id: 'postgres',
|
|
549
|
-
details: buildTargetDetails('column', columnName, schemaName, tableName),
|
|
550
|
-
},
|
|
551
|
-
meta: {
|
|
552
|
-
warning: 'TABLE_REWRITE',
|
|
553
|
-
detail:
|
|
554
|
-
'ALTER COLUMN TYPE requires a full table rewrite and acquires an ACCESS EXCLUSIVE lock. On large tables, this can cause significant downtime.',
|
|
555
|
-
},
|
|
556
|
-
precheck: [
|
|
557
|
-
{
|
|
558
|
-
description: `ensure column "${columnName}" exists`,
|
|
559
|
-
sql: columnExistsCheck({ schema: schemaName, table: tableName, column: columnName }),
|
|
560
|
-
},
|
|
561
|
-
],
|
|
562
|
-
execute: [
|
|
563
|
-
{
|
|
564
|
-
description: `alter type of "${columnName}"`,
|
|
565
|
-
sql: `ALTER TABLE ${qualified}
|
|
566
|
-
ALTER COLUMN ${quoteIdentifier(columnName)}
|
|
567
|
-
TYPE ${expectedType}
|
|
568
|
-
USING ${quoteIdentifier(columnName)}::${expectedType}`,
|
|
569
|
-
},
|
|
570
|
-
],
|
|
571
|
-
postcheck: [
|
|
572
|
-
{
|
|
573
|
-
description: `verify column "${columnName}" has type ${expectedType}`,
|
|
574
|
-
sql: columnTypeCheck({
|
|
575
|
-
schema: schemaName,
|
|
576
|
-
table: tableName,
|
|
577
|
-
column: columnName,
|
|
578
|
-
expectedType: buildExpectedFormatType(column, codecHooks, storageTypes),
|
|
579
|
-
}),
|
|
580
|
-
},
|
|
581
|
-
],
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function buildDefaultOperation(
|
|
586
|
-
schemaName: string,
|
|
587
|
-
tableName: string,
|
|
588
|
-
columnName: string,
|
|
589
|
-
column: Omit<StorageColumn, 'default'>,
|
|
590
|
-
columnDefault: NonNullable<StorageColumn['default']>,
|
|
591
|
-
operationClass: 'additive' | 'widening',
|
|
592
|
-
verb: 'Set' | 'Change',
|
|
593
|
-
): SqlMigrationPlanOperation<PostgresPlanTargetDetails> | null {
|
|
594
|
-
const qualified = qualifyTableName(schemaName, tableName);
|
|
595
|
-
const defaultClause = buildColumnDefaultSql(columnDefault, column);
|
|
596
|
-
// autoincrement defaults are handled by SERIAL types — buildColumnDefaultSql returns ''
|
|
597
|
-
// for them. Until the IR is enriched to distinguish autoincrement (TML-2107), skip.
|
|
598
|
-
if (!defaultClause) return null;
|
|
599
|
-
const verbLower = verb.toLowerCase();
|
|
600
|
-
return {
|
|
601
|
-
id: `setDefault.${tableName}.${columnName}`,
|
|
602
|
-
label: `${verb} default for ${columnName} on ${tableName}`,
|
|
603
|
-
summary: `${verb}s default on column ${columnName} of table ${tableName}`,
|
|
604
|
-
operationClass,
|
|
605
|
-
target: {
|
|
606
|
-
id: 'postgres',
|
|
607
|
-
details: buildTargetDetails('column', columnName, schemaName, tableName),
|
|
608
|
-
},
|
|
609
|
-
precheck: [
|
|
610
|
-
{
|
|
611
|
-
description: `ensure column "${columnName}" exists`,
|
|
612
|
-
sql: columnExistsCheck({ schema: schemaName, table: tableName, column: columnName }),
|
|
613
|
-
},
|
|
614
|
-
],
|
|
615
|
-
execute: [
|
|
616
|
-
{
|
|
617
|
-
description: `${verbLower} default on "${columnName}"`,
|
|
618
|
-
sql: `ALTER TABLE ${qualified}\nALTER COLUMN ${quoteIdentifier(columnName)} SET ${defaultClause}`,
|
|
619
|
-
},
|
|
620
|
-
],
|
|
621
|
-
postcheck: [
|
|
622
|
-
{
|
|
623
|
-
description: `verify column "${columnName}" has a default`,
|
|
624
|
-
sql: columnDefaultExistsCheck({
|
|
625
|
-
schema: schemaName,
|
|
626
|
-
table: tableName,
|
|
627
|
-
column: columnName,
|
|
628
|
-
exists: true,
|
|
629
|
-
}),
|
|
630
|
-
},
|
|
631
|
-
],
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function buildDropDefaultOperation(
|
|
636
|
-
schemaName: string,
|
|
637
|
-
tableName: string,
|
|
638
|
-
columnName: string,
|
|
639
|
-
): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
|
|
640
|
-
const qualified = qualifyTableName(schemaName, tableName);
|
|
641
|
-
return {
|
|
642
|
-
id: `dropDefault.${tableName}.${columnName}`,
|
|
643
|
-
label: `Drop default for ${columnName} on ${tableName}`,
|
|
644
|
-
summary: `Drops default on column ${columnName} of table ${tableName}`,
|
|
645
|
-
operationClass: 'destructive',
|
|
646
|
-
target: {
|
|
647
|
-
id: 'postgres',
|
|
648
|
-
details: buildTargetDetails('column', columnName, schemaName, tableName),
|
|
649
|
-
},
|
|
650
|
-
precheck: [
|
|
651
|
-
{
|
|
652
|
-
description: `ensure column "${columnName}" exists`,
|
|
653
|
-
sql: columnExistsCheck({ schema: schemaName, table: tableName, column: columnName }),
|
|
654
|
-
},
|
|
655
|
-
],
|
|
656
|
-
execute: [
|
|
657
|
-
{
|
|
658
|
-
description: `drop default on "${columnName}"`,
|
|
659
|
-
sql: `ALTER TABLE ${qualified}\nALTER COLUMN ${quoteIdentifier(columnName)} DROP DEFAULT`,
|
|
660
|
-
},
|
|
661
|
-
],
|
|
662
|
-
postcheck: [
|
|
663
|
-
{
|
|
664
|
-
description: `verify column "${columnName}" has no default`,
|
|
665
|
-
sql: columnDefaultExistsCheck({
|
|
666
|
-
schema: schemaName,
|
|
667
|
-
table: tableName,
|
|
668
|
-
column: columnName,
|
|
669
|
-
exists: false,
|
|
670
|
-
}),
|
|
671
|
-
},
|
|
672
|
-
],
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// ============================================================================
|
|
677
|
-
// Conflict Conversion
|
|
678
|
-
// ============================================================================
|
|
679
|
-
|
|
680
|
-
function convertIssueToConflict(issue: SchemaIssue): SqlPlannerConflict | null {
|
|
681
|
-
switch (issue.kind) {
|
|
682
|
-
case 'type_mismatch':
|
|
683
|
-
return buildConflict('typeMismatch', issue);
|
|
684
|
-
case 'nullability_mismatch':
|
|
685
|
-
return buildConflict('nullabilityConflict', issue);
|
|
686
|
-
case 'default_missing':
|
|
687
|
-
case 'default_mismatch':
|
|
688
|
-
case 'extra_default':
|
|
689
|
-
case 'extra_table':
|
|
690
|
-
case 'extra_column':
|
|
691
|
-
case 'extra_primary_key':
|
|
692
|
-
case 'extra_foreign_key':
|
|
693
|
-
case 'extra_unique_constraint':
|
|
694
|
-
case 'extra_index':
|
|
695
|
-
return buildConflict('missingButNonAdditive', issue);
|
|
696
|
-
case 'primary_key_mismatch':
|
|
697
|
-
case 'unique_constraint_mismatch':
|
|
698
|
-
case 'index_mismatch':
|
|
699
|
-
return buildConflict('indexIncompatible', issue);
|
|
700
|
-
case 'foreign_key_mismatch':
|
|
701
|
-
return buildConflict('foreignKeyConflict', issue);
|
|
702
|
-
// Additive issue kinds (missing_table, missing_column, type_missing, type_values_mismatch,
|
|
703
|
-
// dependency_missing) are filtered by isAdditiveIssue before reaching this method.
|
|
704
|
-
// If a new SchemaIssue kind is introduced, add a mapping here so it becomes a conflict
|
|
705
|
-
// rather than being silently ignored.
|
|
706
|
-
default:
|
|
707
|
-
return null;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function buildConflict(kind: SqlPlannerConflict['kind'], issue: SchemaIssue): SqlPlannerConflict {
|
|
712
|
-
const location = buildConflictLocation(issue);
|
|
713
|
-
const base = issue.kind !== 'enum_values_changed' ? issue : undefined;
|
|
714
|
-
const meta =
|
|
715
|
-
base?.expected || base?.actual
|
|
716
|
-
? Object.freeze({
|
|
717
|
-
...ifDefined('expected', base.expected),
|
|
718
|
-
...ifDefined('actual', base.actual),
|
|
719
|
-
})
|
|
720
|
-
: undefined;
|
|
721
|
-
|
|
722
|
-
return {
|
|
723
|
-
kind,
|
|
724
|
-
summary: issue.message,
|
|
725
|
-
...ifDefined('location', location),
|
|
726
|
-
...ifDefined('meta', meta),
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// ============================================================================
|
|
731
|
-
// Sorting and Comparison Helpers
|
|
732
|
-
// ============================================================================
|
|
733
|
-
|
|
734
|
-
function sortSchemaIssues(issues: readonly SchemaIssue[]): readonly SchemaIssue[] {
|
|
735
|
-
return [...issues].sort((a, b) => {
|
|
736
|
-
const kindCompare = a.kind.localeCompare(b.kind);
|
|
737
|
-
if (kindCompare !== 0) {
|
|
738
|
-
return kindCompare;
|
|
739
|
-
}
|
|
740
|
-
const aBase = a.kind !== 'enum_values_changed' ? a : undefined;
|
|
741
|
-
const bBase = b.kind !== 'enum_values_changed' ? b : undefined;
|
|
742
|
-
const tableCompare = compareStrings(aBase?.table, bBase?.table);
|
|
743
|
-
if (tableCompare !== 0) {
|
|
744
|
-
return tableCompare;
|
|
745
|
-
}
|
|
746
|
-
const columnCompare = compareStrings(aBase?.column, bBase?.column);
|
|
747
|
-
if (columnCompare !== 0) {
|
|
748
|
-
return columnCompare;
|
|
749
|
-
}
|
|
750
|
-
return compareStrings(aBase?.indexOrConstraint, bBase?.indexOrConstraint);
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function buildConflictLocation(issue: SchemaIssue) {
|
|
755
|
-
if (issue.kind === 'enum_values_changed') {
|
|
756
|
-
return { type: issue.typeName };
|
|
757
|
-
}
|
|
758
|
-
const location = {
|
|
759
|
-
...ifDefined('table', issue.table),
|
|
760
|
-
...ifDefined('column', issue.column),
|
|
761
|
-
...ifDefined('constraint', issue.indexOrConstraint),
|
|
762
|
-
};
|
|
763
|
-
return Object.keys(location).length > 0 ? location : undefined;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function conflictComparator(a: SqlPlannerConflict, b: SqlPlannerConflict): number {
|
|
767
|
-
if (a.kind !== b.kind) {
|
|
768
|
-
return a.kind < b.kind ? -1 : 1;
|
|
769
|
-
}
|
|
770
|
-
const aLocation = a.location ?? {};
|
|
771
|
-
const bLocation = b.location ?? {};
|
|
772
|
-
const tableCompare = compareStrings(aLocation.table, bLocation.table);
|
|
773
|
-
if (tableCompare !== 0) {
|
|
774
|
-
return tableCompare;
|
|
775
|
-
}
|
|
776
|
-
const columnCompare = compareStrings(aLocation.column, bLocation.column);
|
|
777
|
-
if (columnCompare !== 0) {
|
|
778
|
-
return columnCompare;
|
|
779
|
-
}
|
|
780
|
-
const constraintCompare = compareStrings(aLocation.constraint, bLocation.constraint);
|
|
781
|
-
if (constraintCompare !== 0) {
|
|
782
|
-
return constraintCompare;
|
|
783
|
-
}
|
|
784
|
-
return compareStrings(a.summary, b.summary);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function compareStrings(a?: string, b?: string): number {
|
|
788
|
-
if (a === b) {
|
|
789
|
-
return 0;
|
|
790
|
-
}
|
|
791
|
-
if (a === undefined) {
|
|
792
|
-
return -1;
|
|
793
|
-
}
|
|
794
|
-
if (b === undefined) {
|
|
795
|
-
return 1;
|
|
796
|
-
}
|
|
797
|
-
return a < b ? -1 : 1;
|
|
798
|
-
}
|