pg-migration-engine 0.1.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 +35 -0
- package/README.md +304 -0
- package/package.json +52 -0
- package/src/behavioral/behavioral-applier.js +374 -0
- package/src/behavioral/behavioral-extractor.js +266 -0
- package/src/behavioral/behavioral-puller.js +35 -0
- package/src/behavioral/index.js +4 -0
- package/src/behavioral/phase-sorter.js +24 -0
- package/src/constants.js +97 -0
- package/src/ddl-generator/alter-generator.js +1512 -0
- package/src/ddl-generator/comment-generator.js +95 -0
- package/src/ddl-generator/create-generator.js +1426 -0
- package/src/ddl-generator/drop-generator.js +194 -0
- package/src/ddl-generator/generator.js +78 -0
- package/src/ddl-generator/grant-generator.js +31 -0
- package/src/ddl-generator/index.js +3 -0
- package/src/ddl-generator/pg-version.js +15 -0
- package/src/ddl-generator/rename-generator.js +88 -0
- package/src/ddl-generator/safe-patterns.js +241 -0
- package/src/differ/change-classifier.js +192 -0
- package/src/differ/dependency-resolver.js +523 -0
- package/src/differ/index.js +12 -0
- package/src/differ/object-matcher.js +510 -0
- package/src/differ/property-differ.js +1134 -0
- package/src/differ/risk-tagger.js +561 -0
- package/src/differ/schema-differ.js +309 -0
- package/src/differ/utils/levenshtein.js +210 -0
- package/src/differ/utils/path-builder.js +198 -0
- package/src/differ/utils/type-compatibility.js +472 -0
- package/src/errors.js +147 -0
- package/src/executor/drift-detector.js +221 -0
- package/src/executor/index.js +7 -0
- package/src/executor/lock-manager.js +308 -0
- package/src/executor/migration-executor.js +1249 -0
- package/src/executor/progress-tracker.js +81 -0
- package/src/executor/recovery-manager.js +47 -0
- package/src/executor/snapshot-manager.js +22 -0
- package/src/executor/sql-splitter.js +120 -0
- package/src/executor/transaction-manager.js +288 -0
- package/src/index.js +483 -0
- package/src/introspection/index.js +4 -0
- package/src/introspection/introspector.js +250 -0
- package/src/introspection/queries/access-methods.js +29 -0
- package/src/introspection/queries/casts.js +38 -0
- package/src/introspection/queries/collations.js +51 -0
- package/src/introspection/queries/comments.js +66 -0
- package/src/introspection/queries/constraints.js +65 -0
- package/src/introspection/queries/conversions.js +39 -0
- package/src/introspection/queries/databases.js +43 -0
- package/src/introspection/queries/default-privileges.js +37 -0
- package/src/introspection/queries/event-triggers.js +43 -0
- package/src/introspection/queries/extensions.js +21 -0
- package/src/introspection/queries/foreign-data.js +136 -0
- package/src/introspection/queries/functions.js +75 -0
- package/src/introspection/queries/grants.js +38 -0
- package/src/introspection/queries/index.js +33 -0
- package/src/introspection/queries/indexes.js +99 -0
- package/src/introspection/queries/inheritance.js +24 -0
- package/src/introspection/queries/multiranges.js +37 -0
- package/src/introspection/queries/operators.js +180 -0
- package/src/introspection/queries/partitions.js +42 -0
- package/src/introspection/queries/pg18-19.js +43 -0
- package/src/introspection/queries/policies.js +35 -0
- package/src/introspection/queries/procedural-languages.js +36 -0
- package/src/introspection/queries/publications.js +114 -0
- package/src/introspection/queries/roles.js +66 -0
- package/src/introspection/queries/rules.js +49 -0
- package/src/introspection/queries/sequences.js +37 -0
- package/src/introspection/queries/statistics.js +74 -0
- package/src/introspection/queries/subscriptions.js +99 -0
- package/src/introspection/queries/tables.js +151 -0
- package/src/introspection/queries/tablespaces.js +36 -0
- package/src/introspection/queries/text-search.js +146 -0
- package/src/introspection/queries/triggers.js +61 -0
- package/src/introspection/queries/types.js +106 -0
- package/src/introspection/queries/views.js +146 -0
- package/src/introspection/translator.js +1283 -0
- package/src/introspection/version-detector.js +25 -0
- package/src/planner/backfill-planner.js +36 -0
- package/src/planner/dry-run.js +21 -0
- package/src/planner/index.js +6 -0
- package/src/planner/migration-planner.js +356 -0
- package/src/planner/smart-migrator.js +82 -0
- package/src/planner/step-sequencer.js +61 -0
- package/src/planner/type-registry.js +47 -0
- package/src/risk/compatibility-checker.js +29 -0
- package/src/risk/data-loss-checker.js +32 -0
- package/src/risk/destructive-checker.js +38 -0
- package/src/risk/index.js +6 -0
- package/src/risk/lock-analyzer.js +39 -0
- package/src/risk/recommendations.js +44 -0
- package/src/risk/risk-engine.js +25 -0
- package/src/storage/index.js +5 -0
- package/src/storage/memory-storage.js +87 -0
- package/src/storage/migration-table.js +423 -0
- package/src/storage/rollback-generator.js +344 -0
- package/src/types/changes.js +136 -0
- package/src/types/connection.js +17 -0
- package/src/types/execution.js +107 -0
- package/src/types/index.js +1 -0
- package/src/types/migration.js +67 -0
- package/src/types/risk.js +32 -0
- package/src/types/schema.d.ts +1004 -0
- package/src/types/schema.js +561 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate DROP DDL for any object type.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function generateDropSql(change) {
|
|
6
|
+
const obj = change.before;
|
|
7
|
+
const objectKey = change.objectKey;
|
|
8
|
+
const objectType = change.objectType;
|
|
9
|
+
const changeType = change.changeType;
|
|
10
|
+
|
|
11
|
+
switch (objectType) {
|
|
12
|
+
case 'schema':
|
|
13
|
+
return `DROP SCHEMA IF EXISTS ${ident(obj?.name || objectKey)} CASCADE;`;
|
|
14
|
+
|
|
15
|
+
case 'table':
|
|
16
|
+
return `DROP TABLE IF EXISTS ${objectKey} CASCADE;`;
|
|
17
|
+
|
|
18
|
+
case 'view':
|
|
19
|
+
return `DROP VIEW IF EXISTS ${objectKey} CASCADE;`;
|
|
20
|
+
|
|
21
|
+
case 'materializedView':
|
|
22
|
+
return `DROP MATERIALIZED VIEW IF EXISTS ${objectKey} CASCADE;`;
|
|
23
|
+
|
|
24
|
+
case 'function':
|
|
25
|
+
const fnArgs = obj?.argumentTypes ? `(${obj.argumentTypes.join(', ')})` : '';
|
|
26
|
+
return `DROP FUNCTION IF EXISTS ${objectKey}${fnArgs} CASCADE;`;
|
|
27
|
+
|
|
28
|
+
case 'procedure':
|
|
29
|
+
const procArgs = obj?.argumentTypes ? `(${obj.argumentTypes.join(', ')})` : '';
|
|
30
|
+
return `DROP PROCEDURE IF EXISTS ${objectKey}${procArgs} CASCADE;`;
|
|
31
|
+
|
|
32
|
+
case 'trigger':
|
|
33
|
+
const trigTable = obj?.tableName || objectKey.split('.').slice(0, -1).join('.');
|
|
34
|
+
const trigName = obj?.name || objectKey.split('.').pop();
|
|
35
|
+
return `DROP TRIGGER IF EXISTS ${ident(trigName)} ON ${trigTable};`;
|
|
36
|
+
|
|
37
|
+
case 'index':
|
|
38
|
+
if (changeType === 'DROP_INDEX_CONCURRENTLY' || obj?.isConcurrent) {
|
|
39
|
+
return `DROP INDEX CONCURRENTLY IF EXISTS ${objectKey};`;
|
|
40
|
+
}
|
|
41
|
+
return `DROP INDEX IF EXISTS ${objectKey};`;
|
|
42
|
+
|
|
43
|
+
case 'constraint':
|
|
44
|
+
const conTable = obj?._tableKey || obj?.tableKey || objectKey.split('.').slice(0, -1).join('.');
|
|
45
|
+
const conName = obj?.name || objectKey.split('.').pop();
|
|
46
|
+
return `ALTER TABLE ${conTable} DROP CONSTRAINT IF EXISTS ${ident(conName)};`;
|
|
47
|
+
|
|
48
|
+
case 'policy':
|
|
49
|
+
const polTable = obj?.table || objectKey.split('.').slice(0, -1).join('.');
|
|
50
|
+
const polName = obj?.name || objectKey.split('.').pop();
|
|
51
|
+
return `DROP POLICY IF EXISTS ${ident(polName)} ON ${polTable};`;
|
|
52
|
+
|
|
53
|
+
case 'sequence':
|
|
54
|
+
return `DROP SEQUENCE IF EXISTS ${objectKey} CASCADE;`;
|
|
55
|
+
|
|
56
|
+
case 'extension':
|
|
57
|
+
return `DROP EXTENSION IF EXISTS ${ident(obj?.name || objectKey)} CASCADE;`;
|
|
58
|
+
|
|
59
|
+
case 'type':
|
|
60
|
+
return `DROP TYPE IF EXISTS ${objectKey} CASCADE;`;
|
|
61
|
+
|
|
62
|
+
case 'domain':
|
|
63
|
+
return `DROP DOMAIN IF EXISTS ${objectKey} CASCADE;`;
|
|
64
|
+
|
|
65
|
+
case 'rule':
|
|
66
|
+
const ruleTable = obj?.tableName || objectKey.split('.').slice(0, -1).join('.');
|
|
67
|
+
const ruleName = obj?.name || objectKey.split('.').pop();
|
|
68
|
+
return `DROP RULE IF EXISTS ${ident(ruleName)} ON ${ruleTable};`;
|
|
69
|
+
|
|
70
|
+
case 'aggregate':
|
|
71
|
+
const aggArgs = obj?.argumentTypes ? `(${obj.argumentTypes.join(', ')})` : '()';
|
|
72
|
+
return `DROP AGGREGATE IF EXISTS ${objectKey}${aggArgs} CASCADE;`;
|
|
73
|
+
|
|
74
|
+
case 'eventTrigger':
|
|
75
|
+
return `DROP EVENT TRIGGER IF EXISTS ${ident(obj?.name || objectKey)};`;
|
|
76
|
+
|
|
77
|
+
case 'foreignTable':
|
|
78
|
+
return `DROP FOREIGN TABLE IF EXISTS ${objectKey} CASCADE;`;
|
|
79
|
+
|
|
80
|
+
case 'foreignServer':
|
|
81
|
+
return `DROP SERVER IF EXISTS ${ident(obj?.name || objectKey)} CASCADE;`;
|
|
82
|
+
|
|
83
|
+
case 'foreignDataWrapper':
|
|
84
|
+
return `DROP FOREIGN DATA WRAPPER IF EXISTS ${ident(obj?.name || objectKey)} CASCADE;`;
|
|
85
|
+
|
|
86
|
+
case 'userMapping':
|
|
87
|
+
return `DROP USER MAPPING IF EXISTS FOR ${ident(obj?.user || 'PUBLIC')} SERVER ${ident(obj?.server || 'FOREIGN')};`;
|
|
88
|
+
|
|
89
|
+
case 'cast':
|
|
90
|
+
return `DROP CAST IF EXISTS (${obj?.sourceType || 'type'} AS ${obj?.targetType || 'type'});`;
|
|
91
|
+
|
|
92
|
+
case 'operator':
|
|
93
|
+
const opLeft = obj?.leftType || 'NONE';
|
|
94
|
+
const opRight = obj?.rightType || 'NONE';
|
|
95
|
+
return `DROP OPERATOR IF EXISTS ${objectKey} (${opLeft}, ${opRight}) CASCADE;`;
|
|
96
|
+
|
|
97
|
+
case 'operatorClass':
|
|
98
|
+
return `DROP OPERATOR CLASS IF EXISTS ${objectKey} USING ${obj?.accessMethod || 'btree'} CASCADE;`;
|
|
99
|
+
|
|
100
|
+
case 'operatorFamily':
|
|
101
|
+
return `DROP OPERATOR FAMILY IF EXISTS ${objectKey} USING ${obj?.accessMethod || 'btree'} CASCADE;`;
|
|
102
|
+
|
|
103
|
+
case 'textSearchConfig':
|
|
104
|
+
return `DROP TEXT SEARCH CONFIGURATION IF EXISTS ${objectKey} CASCADE;`;
|
|
105
|
+
|
|
106
|
+
case 'textSearchDict':
|
|
107
|
+
return `DROP TEXT SEARCH DICTIONARY IF EXISTS ${objectKey} CASCADE;`;
|
|
108
|
+
|
|
109
|
+
case 'textSearchParser':
|
|
110
|
+
return `DROP TEXT SEARCH PARSER IF EXISTS ${objectKey} CASCADE;`;
|
|
111
|
+
|
|
112
|
+
case 'textSearchTemplate':
|
|
113
|
+
return `DROP TEXT SEARCH TEMPLATE IF EXISTS ${objectKey} CASCADE;`;
|
|
114
|
+
|
|
115
|
+
case 'statistics':
|
|
116
|
+
return `DROP STATISTICS IF EXISTS ${objectKey};`;
|
|
117
|
+
|
|
118
|
+
case 'publication':
|
|
119
|
+
return `DROP PUBLICATION IF EXISTS ${ident(obj?.name || objectKey)};`;
|
|
120
|
+
|
|
121
|
+
case 'subscription':
|
|
122
|
+
return `DROP SUBSCRIPTION IF EXISTS ${ident(obj?.name || objectKey)};`;
|
|
123
|
+
|
|
124
|
+
case 'language':
|
|
125
|
+
return `DROP LANGUAGE IF EXISTS ${ident(obj?.name || objectKey)} CASCADE;`;
|
|
126
|
+
|
|
127
|
+
case 'collation':
|
|
128
|
+
return `DROP COLLATION IF EXISTS ${objectKey} CASCADE;`;
|
|
129
|
+
|
|
130
|
+
case 'conversion':
|
|
131
|
+
return `DROP CONVERSION IF EXISTS ${objectKey} CASCADE;`;
|
|
132
|
+
|
|
133
|
+
case 'column':
|
|
134
|
+
const colTable = objectKey.split('.').slice(0, -1).join('.');
|
|
135
|
+
const colName = objectKey.split('.').pop();
|
|
136
|
+
return `ALTER TABLE ${colTable} DROP COLUMN IF EXISTS ${ident(colName)};`;
|
|
137
|
+
|
|
138
|
+
case 'database':
|
|
139
|
+
return `-- DROP DATABASE must be run outside transaction: DROP DATABASE IF EXISTS ${ident(obj?.name || objectKey)};`;
|
|
140
|
+
|
|
141
|
+
case 'tablespace':
|
|
142
|
+
return `DROP TABLESPACE IF EXISTS ${ident(obj?.name || objectKey)};`;
|
|
143
|
+
|
|
144
|
+
case 'role':
|
|
145
|
+
return `DROP ROLE IF EXISTS ${ident(obj?.name || objectKey)};`;
|
|
146
|
+
|
|
147
|
+
case 'defaultPrivileges':
|
|
148
|
+
const dpSchema = obj?.schema;
|
|
149
|
+
const dpForRole = obj?.forRole;
|
|
150
|
+
return `ALTER DEFAULT PRIVILEGES${dpSchema ? ` IN SCHEMA ${ident(dpSchema)}` : ''}${dpForRole ? ` FOR ROLE ${ident(dpForRole)}` : ''} REVOKE ALL ON ALL FROM PUBLIC;`;
|
|
151
|
+
|
|
152
|
+
case 'conversion':
|
|
153
|
+
return `DROP CONVERSION IF EXISTS ${objectKey};`;
|
|
154
|
+
|
|
155
|
+
case 'accessMethod':
|
|
156
|
+
return `DROP ACCESS METHOD IF EXISTS ${ident(obj?.name || objectKey)};`;
|
|
157
|
+
|
|
158
|
+
default:
|
|
159
|
+
if (objectType.startsWith('RECREATE')) {
|
|
160
|
+
return generateRecreateSql(change);
|
|
161
|
+
}
|
|
162
|
+
return `-- Unsupported DROP for ${objectType}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function generateRecreateSql(change) {
|
|
167
|
+
const dropSql = generateDropSql({
|
|
168
|
+
...change,
|
|
169
|
+
changeType: 'DROP',
|
|
170
|
+
before: change.before
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const createChange = {
|
|
174
|
+
...change,
|
|
175
|
+
changeType: 'CREATE',
|
|
176
|
+
after: change.before,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
switch (change.objectType) {
|
|
180
|
+
case 'materializedView':
|
|
181
|
+
return dropSql;
|
|
182
|
+
default:
|
|
183
|
+
return dropSql;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function ident(name) {
|
|
188
|
+
if (!name) return '';
|
|
189
|
+
if (typeof name !== 'string') name = String(name);
|
|
190
|
+
if (name.includes('"') || name.includes(' ')) {
|
|
191
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
192
|
+
}
|
|
193
|
+
return `"${name}"`;
|
|
194
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { generateCreateSql } from './create-generator.js';
|
|
2
|
+
import { generateAlterSql } from './alter-generator.js';
|
|
3
|
+
import { generateDropSql } from './drop-generator.js';
|
|
4
|
+
import { generateRenameSql } from './rename-generator.js';
|
|
5
|
+
import { generateCommentSql } from './comment-generator.js';
|
|
6
|
+
import { generateGrantSql } from './grant-generator.js';
|
|
7
|
+
import { generateSafePatterns } from './safe-patterns.js';
|
|
8
|
+
|
|
9
|
+
export class DdlGenerator {
|
|
10
|
+
/**
|
|
11
|
+
* @param {import('../types/changes.js').SchemaChange[]} changes
|
|
12
|
+
* @param {import('../types/execution.js').DdlOptions} [options]
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
generate(changes, options = {}) {
|
|
16
|
+
const { safeMode = false } = options;
|
|
17
|
+
const statements = [];
|
|
18
|
+
|
|
19
|
+
for (const change of changes) {
|
|
20
|
+
if (safeMode) {
|
|
21
|
+
const safeSteps = generateSafePatterns(change);
|
|
22
|
+
if (safeSteps) {
|
|
23
|
+
for (const step of safeSteps) {
|
|
24
|
+
statements.push(`-- ${step.description}`);
|
|
25
|
+
statements.push(step.sql);
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let sql = '';
|
|
32
|
+
const changeType = change.changeType;
|
|
33
|
+
const objectType = change.objectType;
|
|
34
|
+
|
|
35
|
+
if (change.sql) {
|
|
36
|
+
sql = change.sql;
|
|
37
|
+
} else if (change.requiresRecreation || changeType === 'PARTITION_STRUCTURE_CHANGE') {
|
|
38
|
+
const beforeObj = change.before || change.currentValue;
|
|
39
|
+
const afterObj = change.after || change.desiredValue;
|
|
40
|
+
|
|
41
|
+
const dropSql = generateDropSql({
|
|
42
|
+
changeType: 'DROP',
|
|
43
|
+
objectType: change.objectType,
|
|
44
|
+
objectKey: change.objectKey,
|
|
45
|
+
before: beforeObj
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const createSql = generateCreateSql({
|
|
49
|
+
changeType: 'CREATE',
|
|
50
|
+
objectType: change.objectType,
|
|
51
|
+
objectKey: change.objectKey,
|
|
52
|
+
after: afterObj
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
sql = `${dropSql}\n${createSql}`;
|
|
56
|
+
} else if (changeType === 'CREATE' || changeType.startsWith('ADD')) {
|
|
57
|
+
sql = generateCreateSql(change);
|
|
58
|
+
} else if (changeType === 'ALTER' || changeType.includes('CHANGE') || changeType.includes('CAST')) {
|
|
59
|
+
sql = generateAlterSql(change);
|
|
60
|
+
} else if (changeType === 'DROP' || changeType.startsWith('REMOVE') || changeType.includes('RECREATE')) {
|
|
61
|
+
sql = generateDropSql(change);
|
|
62
|
+
} else if (changeType === 'RENAME') {
|
|
63
|
+
sql = generateRenameSql(change);
|
|
64
|
+
} else if (changeType === 'COMMENT') {
|
|
65
|
+
sql = generateCommentSql(change);
|
|
66
|
+
} else if (changeType === 'GRANT' || changeType === 'REVOKE') {
|
|
67
|
+
sql = generateGrantSql(change);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (sql && !sql.startsWith('--')) {
|
|
71
|
+
statements.push(`-- ${changeType} ${objectType} ${change.objectKey}`);
|
|
72
|
+
statements.push(sql);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return statements.join('\n');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function ident(name) {
|
|
2
|
+
if (!name) return '';
|
|
3
|
+
if (name.includes('"') || name.includes(' ')) {
|
|
4
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
5
|
+
}
|
|
6
|
+
return `"${name}"`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {import('../types/changes.js').SchemaChange} change
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function generateGrantSql(change) {
|
|
14
|
+
const type = change.changeType || change.type;
|
|
15
|
+
if (type === 'GRANT') {
|
|
16
|
+
const g = change.after || {};
|
|
17
|
+
const grantee = g.grantee || 'PUBLIC';
|
|
18
|
+
const granteeRef = grantee.toUpperCase() === 'PUBLIC' ? 'PUBLIC' : ident(grantee);
|
|
19
|
+
let sql = `GRANT ${g.privilege || 'ALL'} ON ${g.objectType || 'TABLE'} ${ident(g.schema)}.${ident(g.object)} TO ${granteeRef}`;
|
|
20
|
+
if (g.isGrantable || g.withGrantOption) sql += ' WITH GRANT OPTION';
|
|
21
|
+
return sql + ';';
|
|
22
|
+
}
|
|
23
|
+
if (type === 'REVOKE') {
|
|
24
|
+
const g = change.before || {};
|
|
25
|
+
const grantee = g.grantee || 'PUBLIC';
|
|
26
|
+
const granteeRef = grantee.toUpperCase() === 'PUBLIC' ? 'PUBLIC' : ident(grantee);
|
|
27
|
+
let sql = `REVOKE ${g.privilege || 'ALL'} ON ${g.objectType || 'TABLE'} ${ident(g.schema)}.${ident(g.object)} FROM ${granteeRef}`;
|
|
28
|
+
return sql + ';';
|
|
29
|
+
}
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {number} [pgVersion]
|
|
3
|
+
* @returns {boolean}
|
|
4
|
+
*/
|
|
5
|
+
export function supportsPg18Features(pgVersion) {
|
|
6
|
+
return (pgVersion || 15) >= 180000;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {number} [pgVersion]
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function supportsPg15Features(pgVersion) {
|
|
14
|
+
return (pgVersion || 15) >= 150000;
|
|
15
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import('../types/changes.js').SchemaChange} change
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function generateRenameSql(change) {
|
|
6
|
+
if (!change.isRename) return '';
|
|
7
|
+
|
|
8
|
+
const objType = (change.objectType || '').toLowerCase();
|
|
9
|
+
const schemaPrefix = change.schema ? `${ident(change.schema)}.` : '';
|
|
10
|
+
|
|
11
|
+
const getTableRef = () => {
|
|
12
|
+
const t = change.after?.table || change.before?.table || change.after?.tableName || change.before?.tableName;
|
|
13
|
+
if (!t) return '';
|
|
14
|
+
return t.includes('.') ? t.split('.').map(ident).join('.') : (change.schema ? `${ident(change.schema)}.${ident(t)}` : ident(t));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
switch (objType) {
|
|
18
|
+
case 'table':
|
|
19
|
+
return `ALTER TABLE ${schemaPrefix}${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
20
|
+
|
|
21
|
+
case 'column': {
|
|
22
|
+
const table = change.objectKey.split('.').slice(0, -1).join('.');
|
|
23
|
+
const formattedTable = table.split('.').map(ident).join('.');
|
|
24
|
+
return `ALTER TABLE ${formattedTable} RENAME COLUMN ${ident(change.renameFrom)} TO ${ident(change.renameTo)};`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
case 'index':
|
|
28
|
+
return `ALTER INDEX ${schemaPrefix}${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
29
|
+
|
|
30
|
+
case 'constraint': {
|
|
31
|
+
const constrTable = change.before?.table || change.after?.table || change.before?.tableName || change.after?.tableName;
|
|
32
|
+
const formattedConstrTable = constrTable.includes('.') ? constrTable.split('.').map(ident).join('.') : (change.schema ? `${ident(change.schema)}.${ident(constrTable)}` : ident(constrTable));
|
|
33
|
+
return `ALTER TABLE ${formattedConstrTable} RENAME CONSTRAINT ${ident(change.renameFrom)} TO ${ident(change.renameTo)};`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
case 'type':
|
|
37
|
+
return `ALTER TYPE ${schemaPrefix}${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
38
|
+
|
|
39
|
+
case 'function':
|
|
40
|
+
case 'procedure':
|
|
41
|
+
return `ALTER FUNCTION ${schemaPrefix}${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
42
|
+
|
|
43
|
+
case 'trigger':
|
|
44
|
+
return `ALTER TRIGGER ${ident(change.renameFrom)} ON ${getTableRef()} RENAME TO ${ident(change.renameTo)};`;
|
|
45
|
+
|
|
46
|
+
case 'schema':
|
|
47
|
+
return `ALTER SCHEMA ${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
48
|
+
|
|
49
|
+
case 'foreigntable':
|
|
50
|
+
return `ALTER FOREIGN TABLE ${schemaPrefix}${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
51
|
+
|
|
52
|
+
case 'foreignserver':
|
|
53
|
+
return `ALTER SERVER ${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
54
|
+
|
|
55
|
+
case 'conversion':
|
|
56
|
+
return `ALTER CONVERSION ${schemaPrefix}${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
57
|
+
|
|
58
|
+
case 'collation':
|
|
59
|
+
return `ALTER COLLATION ${schemaPrefix}${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
60
|
+
|
|
61
|
+
case 'textsearchconfig':
|
|
62
|
+
return `ALTER TEXT SEARCH CONFIGURATION ${schemaPrefix}${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
63
|
+
|
|
64
|
+
case 'textsearchdict':
|
|
65
|
+
return `ALTER TEXT SEARCH DICTIONARY ${schemaPrefix}${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
66
|
+
|
|
67
|
+
case 'language':
|
|
68
|
+
return `ALTER LANGUAGE ${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
69
|
+
|
|
70
|
+
case 'policy':
|
|
71
|
+
return `ALTER POLICY ${ident(change.renameFrom)} ON ${getTableRef()} RENAME TO ${ident(change.renameTo)};`;
|
|
72
|
+
|
|
73
|
+
case 'eventtrigger':
|
|
74
|
+
return `ALTER EVENT TRIGGER ${ident(change.renameFrom)} RENAME TO ${ident(change.renameTo)};`;
|
|
75
|
+
|
|
76
|
+
default:
|
|
77
|
+
return `-- Unsupported RENAME for ${change.objectType}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ident(name) {
|
|
82
|
+
if (!name) return '';
|
|
83
|
+
if (typeof name !== 'string') name = String(name);
|
|
84
|
+
if (name.includes('"') || name.includes(' ')) {
|
|
85
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
86
|
+
}
|
|
87
|
+
return `"${name}"`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate safe patterns for potentially destructive changes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const qi = name => name === '*' ? name : '"' + name.replace(/"/g, '""') + '"';
|
|
6
|
+
|
|
7
|
+
export function generateSafePatterns(change) {
|
|
8
|
+
const steps = [];
|
|
9
|
+
const changeType = change.changeType;
|
|
10
|
+
const objectType = change.objectType;
|
|
11
|
+
const property = change.property;
|
|
12
|
+
|
|
13
|
+
// NOT NULL 3-step pattern
|
|
14
|
+
if (objectType === 'column' && property === 'isNullable' && change.currentValue === true && change.desiredValue === false) {
|
|
15
|
+
const parts = change.objectKey.split('.');
|
|
16
|
+
const col = parts.pop();
|
|
17
|
+
const table = parts.join('.');
|
|
18
|
+
const checkName = `${table.replace(/\./g, '_')}_${col}_not_null_chk`;
|
|
19
|
+
steps.push({
|
|
20
|
+
id: `step_${Date.now()}_1`,
|
|
21
|
+
type: 'constraint',
|
|
22
|
+
phase: 10,
|
|
23
|
+
description: `Add CHECK NOT NULL (NOT VALID) for ${table}.${col}`,
|
|
24
|
+
sql: `ALTER TABLE ${table} ADD CONSTRAINT ${checkName} CHECK (${col} IS NOT NULL) NOT VALID;`,
|
|
25
|
+
isTransactional: true,
|
|
26
|
+
riskLevel: 'low',
|
|
27
|
+
dependencies: [],
|
|
28
|
+
});
|
|
29
|
+
steps.push({
|
|
30
|
+
id: `step_${Date.now()}_2`,
|
|
31
|
+
type: 'constraint',
|
|
32
|
+
phase: 13,
|
|
33
|
+
description: `Validate CHECK constraint ${checkName}`,
|
|
34
|
+
sql: `ALTER TABLE ${table} VALIDATE CONSTRAINT ${checkName};`,
|
|
35
|
+
isTransactional: true,
|
|
36
|
+
riskLevel: 'low',
|
|
37
|
+
dependencies: [steps[0].id],
|
|
38
|
+
});
|
|
39
|
+
steps.push({
|
|
40
|
+
id: `step_${Date.now()}_3`,
|
|
41
|
+
type: 'structural',
|
|
42
|
+
phase: 11,
|
|
43
|
+
description: `Convert CHECK to NOT NULL for ${table}.${col}`,
|
|
44
|
+
sql: `ALTER TABLE ${table} ALTER COLUMN ${col} SET NOT NULL; ALTER TABLE ${table} DROP CONSTRAINT ${checkName};`,
|
|
45
|
+
isTransactional: true,
|
|
46
|
+
riskLevel: 'low',
|
|
47
|
+
dependencies: [steps[1].id],
|
|
48
|
+
});
|
|
49
|
+
return steps;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// FK NOT VALID pattern
|
|
53
|
+
if (objectType === 'constraint' && (change.after?.constraintType === 'FOREIGN_KEY' || change.after?.type === 'FOREIGN_KEY')) {
|
|
54
|
+
const con = change.after;
|
|
55
|
+
steps.push({
|
|
56
|
+
id: `step_${Date.now()}_1`,
|
|
57
|
+
type: 'constraint',
|
|
58
|
+
phase: 12,
|
|
59
|
+
description: `Add FK ${con.name} NOT VALID`,
|
|
60
|
+
sql: `ALTER TABLE ${con._tableKey || con.table} ADD CONSTRAINT ${con.name} FOREIGN KEY (${(con.columns || []).map(qi).join(', ')}) REFERENCES ${con.referencedTable}(${(con.referencedColumns || []).map(qi).join(', ')}) NOT VALID;`,
|
|
61
|
+
isTransactional: true,
|
|
62
|
+
riskLevel: 'low',
|
|
63
|
+
dependencies: [],
|
|
64
|
+
});
|
|
65
|
+
steps.push({
|
|
66
|
+
id: `step_${Date.now()}_2`,
|
|
67
|
+
type: 'constraint',
|
|
68
|
+
phase: 13,
|
|
69
|
+
description: `Validate FK ${con.name}`,
|
|
70
|
+
sql: `ALTER TABLE ${con._tableKey || con.table} VALIDATE CONSTRAINT ${con.name};`,
|
|
71
|
+
isTransactional: true,
|
|
72
|
+
riskLevel: 'low',
|
|
73
|
+
dependencies: [steps[0].id],
|
|
74
|
+
});
|
|
75
|
+
return steps;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// CREATE INDEX CONCURRENTLY pattern
|
|
79
|
+
if (objectType === 'index' && changeType === 'CREATE' && !change.after?.isConcurrent) {
|
|
80
|
+
const idx = change.after;
|
|
81
|
+
steps.push({
|
|
82
|
+
id: `step_${Date.now()}_1`,
|
|
83
|
+
type: 'index',
|
|
84
|
+
phase: 23,
|
|
85
|
+
description: `Create index ${idx.name} CONCURRENTLY`,
|
|
86
|
+
sql: `CREATE INDEX CONCURRENTLY ${idx.name} ON ${idx.schema}.${idx.table}${idx.accessMethod && idx.accessMethod !== 'btree' ? ` USING ${idx.accessMethod}` : ''} (${(idx.columns || []).map(c => c.expression || qi(c.name)).join(', ')});`,
|
|
87
|
+
isTransactional: false,
|
|
88
|
+
riskLevel: 'low',
|
|
89
|
+
dependencies: [],
|
|
90
|
+
});
|
|
91
|
+
return steps;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Type change with USING clause
|
|
95
|
+
if (objectType === 'column' && property === 'dataType') {
|
|
96
|
+
const currentType = change.currentValue;
|
|
97
|
+
const desiredType = change.desiredValue;
|
|
98
|
+
if (currentType && desiredType && currentType !== desiredType) {
|
|
99
|
+
const parts = change.objectKey.split('.');
|
|
100
|
+
const col = parts.pop();
|
|
101
|
+
const table = parts.join('.');
|
|
102
|
+
steps.push({
|
|
103
|
+
id: `step_${Date.now()}_1`,
|
|
104
|
+
type: 'structural',
|
|
105
|
+
phase: 11,
|
|
106
|
+
description: `Alter type of ${table}.${col} from ${currentType} to ${desiredType}`,
|
|
107
|
+
sql: `ALTER TABLE ${table} ALTER COLUMN ${col} TYPE ${desiredType} USING ${col}::${desiredType};`,
|
|
108
|
+
isTransactional: true,
|
|
109
|
+
riskLevel: 'medium',
|
|
110
|
+
dependencies: [],
|
|
111
|
+
});
|
|
112
|
+
return steps;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// UNIQUE constraint with NOT VALID pattern (for large tables)
|
|
117
|
+
if (objectType === 'constraint' && change.after?.constraintType === 'UNIQUE' && change.objectKey) {
|
|
118
|
+
const con = change.after;
|
|
119
|
+
const table = con._tableKey || con.table;
|
|
120
|
+
const tempIdxName = `${con.name}_unique_build`;
|
|
121
|
+
steps.push({
|
|
122
|
+
id: `step_${Date.now()}_1`,
|
|
123
|
+
type: 'index',
|
|
124
|
+
phase: 23,
|
|
125
|
+
description: `Create unique index CONCURRENTLY for ${con.name}`,
|
|
126
|
+
sql: `CREATE UNIQUE INDEX CONCURRENTLY ${tempIdxName} ON ${table} (${(con.columns || []).map(qi).join(', ')});`,
|
|
127
|
+
isTransactional: false,
|
|
128
|
+
riskLevel: 'low',
|
|
129
|
+
dependencies: [],
|
|
130
|
+
});
|
|
131
|
+
steps.push({
|
|
132
|
+
id: `step_${Date.now()}_2`,
|
|
133
|
+
type: 'constraint',
|
|
134
|
+
phase: 24,
|
|
135
|
+
description: `Add unique constraint using existing index`,
|
|
136
|
+
sql: `ALTER TABLE ${table} ADD CONSTRAINT ${con.name} UNIQUE USING INDEX ${tempIdxName};`,
|
|
137
|
+
isTransactional: true,
|
|
138
|
+
riskLevel: 'low',
|
|
139
|
+
dependencies: [steps[0].id],
|
|
140
|
+
});
|
|
141
|
+
return steps;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// CHECK constraint with NOT VALID pattern (for validation safety)
|
|
145
|
+
if (objectType === 'constraint' && change.after?.constraintType === 'CHECK' && change.objectKey) {
|
|
146
|
+
const con = change.after;
|
|
147
|
+
const table = con._tableKey || con.table;
|
|
148
|
+
steps.push({
|
|
149
|
+
id: `step_${Date.now()}_1`,
|
|
150
|
+
type: 'constraint',
|
|
151
|
+
phase: 10,
|
|
152
|
+
description: `Add CHECK constraint ${con.name} NOT VALID`,
|
|
153
|
+
sql: `ALTER TABLE ${table} ADD CONSTRAINT ${con.name} CHECK (${con.definition || con.check}) NOT VALID;`,
|
|
154
|
+
isTransactional: true,
|
|
155
|
+
riskLevel: 'low',
|
|
156
|
+
dependencies: [],
|
|
157
|
+
});
|
|
158
|
+
steps.push({
|
|
159
|
+
id: `step_${Date.now()}_2`,
|
|
160
|
+
type: 'constraint',
|
|
161
|
+
phase: 13,
|
|
162
|
+
description: `Validate CHECK constraint ${con.name}`,
|
|
163
|
+
sql: `ALTER TABLE ${table} VALIDATE CONSTRAINT ${con.name};`,
|
|
164
|
+
isTransactional: true,
|
|
165
|
+
riskLevel: 'low',
|
|
166
|
+
dependencies: [steps[0].id],
|
|
167
|
+
});
|
|
168
|
+
return steps;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Materialized view refresh after recreation
|
|
172
|
+
if (objectType === 'materializedView' && (changeType === 'CREATE' || changeType?.includes('RECREATE'))) {
|
|
173
|
+
const mv = change.after;
|
|
174
|
+
if (mv.isWithData !== false) {
|
|
175
|
+
steps.push({
|
|
176
|
+
id: `step_${Date.now()}_1`,
|
|
177
|
+
type: 'structural',
|
|
178
|
+
phase: 15,
|
|
179
|
+
description: `Refresh materialized view ${change.objectKey}`,
|
|
180
|
+
sql: `REFRESH MATERIALIZED VIEW ${change.objectKey};`,
|
|
181
|
+
isTransactional: true,
|
|
182
|
+
riskLevel: 'low',
|
|
183
|
+
dependencies: [],
|
|
184
|
+
});
|
|
185
|
+
return steps;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Multi-step impossible type change (add column → copy → drop old → rename)
|
|
190
|
+
if (objectType === 'column' && property === 'dataType' && change.changeType === 'IMPOSSIBLE_CAST') {
|
|
191
|
+
const parts = change.objectKey.split('.');
|
|
192
|
+
const col = parts.pop();
|
|
193
|
+
const table = parts.join('.');
|
|
194
|
+
const newColName = `${col}_new`;
|
|
195
|
+
const oldColName = `${col}_old`;
|
|
196
|
+
|
|
197
|
+
steps.push({
|
|
198
|
+
id: `step_${Date.now()}_1`,
|
|
199
|
+
type: 'structural',
|
|
200
|
+
phase: 7,
|
|
201
|
+
description: `Add temporary column ${newColName}`,
|
|
202
|
+
sql: `ALTER TABLE ${table} ADD COLUMN ${newColName} ${change.desiredValue};`,
|
|
203
|
+
isTransactional: true,
|
|
204
|
+
riskLevel: 'medium',
|
|
205
|
+
dependencies: [],
|
|
206
|
+
});
|
|
207
|
+
steps.push({
|
|
208
|
+
id: `step_${Date.now()}_2`,
|
|
209
|
+
type: 'structural',
|
|
210
|
+
phase: 11,
|
|
211
|
+
description: `Copy data with transformation`,
|
|
212
|
+
sql: `UPDATE ${table} SET ${newColName} = ${change.usingExpression || `CAST(${col} AS ${change.desiredValue})`};`,
|
|
213
|
+
isTransactional: true,
|
|
214
|
+
riskLevel: 'medium',
|
|
215
|
+
dependencies: [steps[0].id],
|
|
216
|
+
});
|
|
217
|
+
steps.push({
|
|
218
|
+
id: `step_${Date.now()}_3`,
|
|
219
|
+
type: 'structural',
|
|
220
|
+
phase: 11,
|
|
221
|
+
description: `Drop old column`,
|
|
222
|
+
sql: `ALTER TABLE ${table} DROP COLUMN ${col};`,
|
|
223
|
+
isTransactional: true,
|
|
224
|
+
riskLevel: 'high',
|
|
225
|
+
dependencies: [steps[1].id],
|
|
226
|
+
});
|
|
227
|
+
steps.push({
|
|
228
|
+
id: `step_${Date.now()}_4`,
|
|
229
|
+
type: 'structural',
|
|
230
|
+
phase: 11,
|
|
231
|
+
description: `Rename new column to original name`,
|
|
232
|
+
sql: `ALTER TABLE ${table} RENAME COLUMN ${newColName} TO ${col};`,
|
|
233
|
+
isTransactional: true,
|
|
234
|
+
riskLevel: 'low',
|
|
235
|
+
dependencies: [steps[2].id],
|
|
236
|
+
});
|
|
237
|
+
return steps;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return null;
|
|
241
|
+
}
|