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,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change classifier - separates Track 1 (structural) from Track 2 (behavioral).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class ChangeClassifier {
|
|
6
|
+
/**
|
|
7
|
+
* Classify all changes.
|
|
8
|
+
*/
|
|
9
|
+
classify(changes) {
|
|
10
|
+
for (const change of changes) {
|
|
11
|
+
change.track = this.getTrack(change);
|
|
12
|
+
change.ddlStrategy = this.getDDLStrategy(change);
|
|
13
|
+
change.isTransactional = this.isTransactional(change);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Determine which track a change belongs to.
|
|
19
|
+
* Track 1: Structural (tables, columns, indexes, constraints)
|
|
20
|
+
* Track 2: Behavioral (views, functions, triggers, policies)
|
|
21
|
+
*/
|
|
22
|
+
getTrack(change) {
|
|
23
|
+
const track1Objects = [
|
|
24
|
+
'table', 'column', 'index', 'constraint', 'sequence',
|
|
25
|
+
'extension', 'schema', 'type', 'statistics', 'collation',
|
|
26
|
+
'operator', 'operatorClass', 'operatorFamily', 'cast',
|
|
27
|
+
'foreignTable', 'accessMethod', 'defaultPrivileges',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const track2Objects = [
|
|
31
|
+
'view', 'materializedView', 'function', 'procedure',
|
|
32
|
+
'trigger', 'eventTrigger', 'policy', 'rule',
|
|
33
|
+
'aggregate', 'textSearchConfig', 'textSearchDict',
|
|
34
|
+
'conversion', 'textSearchParser', 'textSearchTemplate',
|
|
35
|
+
'foreignDataWrapper', 'foreignServer', 'userMapping',
|
|
36
|
+
'publication', 'subscription', 'language',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
if (track1Objects.includes(change.objectType)) return 1;
|
|
40
|
+
if (track2Objects.includes(change.objectType)) return 2;
|
|
41
|
+
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Determine the DDL strategy for this change.
|
|
47
|
+
*/
|
|
48
|
+
getDDLStrategy(change) {
|
|
49
|
+
const objType = change.objectType;
|
|
50
|
+
const changeType = change.changeType;
|
|
51
|
+
|
|
52
|
+
// CREATE operations
|
|
53
|
+
if (changeType === 'CREATE' || changeType.includes('ADD')) {
|
|
54
|
+
return this.getCreateStrategy(change);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ALTER operations
|
|
58
|
+
if (changeType === 'ALTER' || changeType.includes('CHANGE') || changeType.includes('REPLACE')) {
|
|
59
|
+
return this.getAlterStrategy(change);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// DROP operations
|
|
63
|
+
if (changeType === 'DROP' || changeType.includes('REMOVE') || changeType.includes('RECREATE')) {
|
|
64
|
+
return this.getDropStrategy(change);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// RENAME operations
|
|
68
|
+
if (changeType === 'RENAME') {
|
|
69
|
+
return this.getRenameStrategy(change);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return 'ALTER';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getCreateStrategy(change) {
|
|
76
|
+
switch (change.objectType) {
|
|
77
|
+
case 'view':
|
|
78
|
+
return 'CREATE_OR_REPLACE';
|
|
79
|
+
case 'function':
|
|
80
|
+
case 'procedure':
|
|
81
|
+
return 'CREATE_OR_REPLACE';
|
|
82
|
+
case 'trigger':
|
|
83
|
+
return 'CREATE_TRIGGER';
|
|
84
|
+
case 'policy':
|
|
85
|
+
return 'CREATE_POLICY';
|
|
86
|
+
case 'index':
|
|
87
|
+
return change.isConcurrent ? 'CREATE_INDEX_CONCURRENTLY' : 'CREATE_INDEX';
|
|
88
|
+
default:
|
|
89
|
+
return 'CREATE';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getAlterStrategy(change) {
|
|
94
|
+
switch (change.objectType) {
|
|
95
|
+
case 'view':
|
|
96
|
+
if (change.property === 'definition') return 'CREATE_OR_REPLACE';
|
|
97
|
+
return 'ALTER_VIEW';
|
|
98
|
+
case 'materializedView':
|
|
99
|
+
return 'DROP_AND_CREATE';
|
|
100
|
+
case 'function':
|
|
101
|
+
case 'procedure':
|
|
102
|
+
if (change.property === 'argumentTypes') return 'DROP_AND_CREATE';
|
|
103
|
+
if (change.property === 'source') return 'CREATE_OR_REPLACE';
|
|
104
|
+
return 'ALTER_FUNCTION';
|
|
105
|
+
case 'trigger':
|
|
106
|
+
if (change.property === 'enabled') return 'ALTER_TRIGGER_ENABLE';
|
|
107
|
+
return 'DROP_AND_CREATE';
|
|
108
|
+
case 'policy':
|
|
109
|
+
if (change.property === 'command') return 'DROP_AND_CREATE';
|
|
110
|
+
if (change.property === 'isPermissive') return 'DROP_AND_CREATE';
|
|
111
|
+
return 'ALTER_POLICY';
|
|
112
|
+
case 'index':
|
|
113
|
+
if (change.property === 'tablespace') return 'ALTER_INDEX_SET_TABLESPACE';
|
|
114
|
+
return 'DROP_AND_CREATE';
|
|
115
|
+
case 'constraint':
|
|
116
|
+
if (change.property === 'isValidated') return 'VALIDATE_CONSTRAINT';
|
|
117
|
+
return 'DROP_AND_CREATE';
|
|
118
|
+
case 'type':
|
|
119
|
+
if (change.property === 'enumValues' && change.addedValues) return 'ALTER_TYPE_ADD_VALUE';
|
|
120
|
+
return 'DROP_AND_CREATE';
|
|
121
|
+
default:
|
|
122
|
+
return 'ALTER';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getDropStrategy(change) {
|
|
127
|
+
switch (change.objectType) {
|
|
128
|
+
case 'index':
|
|
129
|
+
return change.before?.isConcurrent ? 'DROP_INDEX_CONCURRENTLY' : 'DROP_INDEX';
|
|
130
|
+
case 'constraint':
|
|
131
|
+
return 'DROP_CONSTRAINT';
|
|
132
|
+
default:
|
|
133
|
+
return 'DROP';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getRenameStrategy(change) {
|
|
138
|
+
switch (change.objectType) {
|
|
139
|
+
case 'table':
|
|
140
|
+
return 'ALTER_TABLE_RENAME';
|
|
141
|
+
case 'column':
|
|
142
|
+
return 'ALTER_TABLE_RENAME_COLUMN';
|
|
143
|
+
case 'index':
|
|
144
|
+
return 'ALTER_INDEX_RENAME';
|
|
145
|
+
case 'constraint':
|
|
146
|
+
return 'ALTER_TABLE_RENAME_CONSTRAINT';
|
|
147
|
+
case 'type':
|
|
148
|
+
return 'ALTER_TYPE_RENAME';
|
|
149
|
+
case 'sequence':
|
|
150
|
+
return 'ALTER_SEQUENCE_RENAME';
|
|
151
|
+
case 'view':
|
|
152
|
+
return 'ALTER_VIEW_RENAME';
|
|
153
|
+
case 'function':
|
|
154
|
+
return 'DROP_AND_CREATE';
|
|
155
|
+
case 'trigger':
|
|
156
|
+
return 'DROP_AND_CREATE';
|
|
157
|
+
case 'policy':
|
|
158
|
+
return 'DROP_AND_CREATE';
|
|
159
|
+
default:
|
|
160
|
+
return 'ALTER';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Determine if the change can run in a transaction.
|
|
166
|
+
*/
|
|
167
|
+
isTransactional(change) {
|
|
168
|
+
if (change.isNonTransactional === true) return false;
|
|
169
|
+
if (change.isNonTransactional === false) return true;
|
|
170
|
+
|
|
171
|
+
// DDL strategies that cannot run in transactions
|
|
172
|
+
const nonTransactionalStrategies = [
|
|
173
|
+
'CREATE_INDEX_CONCURRENTLY',
|
|
174
|
+
'DROP_INDEX_CONCURRENTLY',
|
|
175
|
+
'VACUUM_FULL',
|
|
176
|
+
'CLUSTER',
|
|
177
|
+
'CREATE_DATABASE',
|
|
178
|
+
'DROP_DATABASE',
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
if (nonTransactionalStrategies.includes(change.ddlStrategy)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// CONCURRENTLY flag
|
|
186
|
+
if (change.isConcurrent || change.before?.isConcurrent) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency resolver for ordering migration operations.
|
|
3
|
+
* Builds dependency graph and performs topological sort.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class DependencyResolver {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.graph = new Map();
|
|
9
|
+
this.edges = [];
|
|
10
|
+
this.cycleDetected = false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve order of all changes based on dependencies.
|
|
15
|
+
*/
|
|
16
|
+
resolve(changes, desired, current) {
|
|
17
|
+
// Build dependency graph
|
|
18
|
+
this.buildGraph(changes, desired, current);
|
|
19
|
+
|
|
20
|
+
// Topological sort (Kahn's algorithm)
|
|
21
|
+
const sorted = this.topologicalSort(changes);
|
|
22
|
+
|
|
23
|
+
// Assign phases
|
|
24
|
+
return this.assignPhases(sorted);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build dependency graph from schema objects.
|
|
29
|
+
*/
|
|
30
|
+
buildGraph(changes, desired, current) {
|
|
31
|
+
this.graph.clear();
|
|
32
|
+
this.edges = [];
|
|
33
|
+
|
|
34
|
+
// Initialize all changes
|
|
35
|
+
for (const change of changes) {
|
|
36
|
+
this.graph.set(change.id, {
|
|
37
|
+
id: change.id,
|
|
38
|
+
change,
|
|
39
|
+
dependencies: new Set(),
|
|
40
|
+
dependents: new Set(),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add dependencies based on object types
|
|
45
|
+
for (const change of changes) {
|
|
46
|
+
switch (change.objectType) {
|
|
47
|
+
case 'column':
|
|
48
|
+
this.addColumnDependencies(change, changes, desired, current);
|
|
49
|
+
break;
|
|
50
|
+
case 'constraint':
|
|
51
|
+
this.addConstraintDependencies(change, changes, desired, current);
|
|
52
|
+
break;
|
|
53
|
+
case 'index':
|
|
54
|
+
this.addIndexDependencies(change, changes, desired, current);
|
|
55
|
+
break;
|
|
56
|
+
case 'trigger':
|
|
57
|
+
this.addTriggerDependencies(change, changes, desired, current);
|
|
58
|
+
break;
|
|
59
|
+
case 'policy':
|
|
60
|
+
this.addPolicyDependencies(change, changes, desired, current);
|
|
61
|
+
break;
|
|
62
|
+
case 'view':
|
|
63
|
+
case 'materializedView':
|
|
64
|
+
this.addViewDependencies(change, changes, desired, current);
|
|
65
|
+
break;
|
|
66
|
+
case 'function':
|
|
67
|
+
this.addFunctionDependencies(change, changes, desired, current);
|
|
68
|
+
break;
|
|
69
|
+
case 'type':
|
|
70
|
+
this.addTypeDependencies(change, changes, desired, current);
|
|
71
|
+
break;
|
|
72
|
+
case 'sequence':
|
|
73
|
+
this.addSequenceDependencies(change, changes, desired, current);
|
|
74
|
+
break;
|
|
75
|
+
case 'table':
|
|
76
|
+
this.addTableDependencies(change, changes, desired, current);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return this.graph;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
addColumnDependencies(change, changes, desired, current) {
|
|
85
|
+
// Column depends on its table
|
|
86
|
+
const tableKey = this.getTableKey(change);
|
|
87
|
+
const tableChange = changes.find(c => c.objectKey === tableKey && c.changeType === 'CREATE');
|
|
88
|
+
if (tableChange) {
|
|
89
|
+
this.addEdge(change.id, tableChange.id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
addConstraintDependencies(change, changes, desired, current) {
|
|
94
|
+
// Constraint depends on its table
|
|
95
|
+
const tableKey = change.objectPath?.split('.').slice(0, 2).join('.');
|
|
96
|
+
const tableChange = changes.find(c => c.objectKey === tableKey && c.changeType === 'CREATE');
|
|
97
|
+
if (tableChange) {
|
|
98
|
+
this.addEdge(change.id, tableChange.id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// FK depends on referenced table
|
|
102
|
+
if (change.changeType === 'ADD_FOREIGN_KEY' || change.constraintType === 'FOREIGN_KEY') {
|
|
103
|
+
const refTable = change.referencedTable || change.after?.referencedTable;
|
|
104
|
+
if (refTable) {
|
|
105
|
+
const refTableChange = changes.find(c => c.objectKey === refTable);
|
|
106
|
+
if (refTableChange) {
|
|
107
|
+
this.addEdge(change.id, refTableChange.id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
addIndexDependencies(change, changes, desired, current) {
|
|
114
|
+
// Index depends on its table
|
|
115
|
+
const tableKey = change.after?.table || change.before?.table;
|
|
116
|
+
if (tableKey) {
|
|
117
|
+
const tableChange = changes.find(c => c.objectKey === tableKey && c.changeType === 'CREATE');
|
|
118
|
+
if (tableChange) {
|
|
119
|
+
this.addEdge(change.id, tableChange.id);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
addTriggerDependencies(change, changes, desired, current) {
|
|
125
|
+
// Trigger depends on its table
|
|
126
|
+
const tableKey = change.after?.table || change.before?.table;
|
|
127
|
+
if (tableKey) {
|
|
128
|
+
const tableChange = changes.find(c => c.objectKey === tableKey);
|
|
129
|
+
if (tableChange) {
|
|
130
|
+
this.addEdge(change.id, tableChange.id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Trigger depends on its function
|
|
135
|
+
const fnCall = change.after?.functionCall || change.before?.functionCall;
|
|
136
|
+
if (fnCall) {
|
|
137
|
+
const fnChange = changes.find(c => c.objectKey?.startsWith(fnCall));
|
|
138
|
+
if (fnChange) {
|
|
139
|
+
this.addEdge(change.id, fnChange.id);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
addPolicyDependencies(change, changes, desired, current) {
|
|
145
|
+
// Policy depends on its table
|
|
146
|
+
const tableKey = change.after?.table || change.before?.table;
|
|
147
|
+
if (tableKey) {
|
|
148
|
+
const tableChange = changes.find(c => c.objectKey === tableKey);
|
|
149
|
+
if (tableChange) {
|
|
150
|
+
this.addEdge(change.id, tableChange.id);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
addViewDependencies(change, changes, desired, current) {
|
|
156
|
+
// View depends on tables/views/functions it references
|
|
157
|
+
const definition = change.after?.definition || '';
|
|
158
|
+
const deps = this.extractViewDependencies(definition);
|
|
159
|
+
|
|
160
|
+
for (const dep of deps) {
|
|
161
|
+
const depChange = changes.find(c => c.objectKey?.includes(dep));
|
|
162
|
+
if (depChange) {
|
|
163
|
+
this.addEdge(change.id, depChange.id);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
addFunctionDependencies(change, changes, desired, current) {
|
|
169
|
+
// Function depends on types it uses (argument types, return type)
|
|
170
|
+
const types = new Set();
|
|
171
|
+
|
|
172
|
+
// Extract from argument types
|
|
173
|
+
const args = change.after?.argumentTypes || [];
|
|
174
|
+
args.forEach(arg => {
|
|
175
|
+
const t = this.extractTypeName(arg);
|
|
176
|
+
|
|
177
|
+
if (
|
|
178
|
+
t &&
|
|
179
|
+
!['integer', 'bigint', 'text', 'boolean', 'uuid', 'timestamp', 'date'].includes(
|
|
180
|
+
t.toLowerCase()
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
types.add(t);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Extract from return type
|
|
187
|
+
const retType = change.after?.returnType;
|
|
188
|
+
if (retType) {
|
|
189
|
+
const t = this.extractTypeName(retType);
|
|
190
|
+
if (t && !this.isBuiltInType(t)) {
|
|
191
|
+
types.add(t);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Add dependencies
|
|
196
|
+
for (const typeName of types) {
|
|
197
|
+
const typeChange = changes.find(
|
|
198
|
+
c => c.objectType === 'type' && c.objectKey?.includes(typeName)
|
|
199
|
+
);
|
|
200
|
+
if (typeChange) {
|
|
201
|
+
this.addEdge(change.id, typeChange.id);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
addTypeDependencies(change, changes, desired, current) {
|
|
207
|
+
// Domain depends on base type
|
|
208
|
+
if (change.after?.kind === 'DOMAIN' && change.after?.baseType) {
|
|
209
|
+
const baseTypeChange = changes.find(
|
|
210
|
+
c =>
|
|
211
|
+
c.objectType === 'type' &&
|
|
212
|
+
c.objectKey?.includes(change.after.baseType)
|
|
213
|
+
);
|
|
214
|
+
if (baseTypeChange) {
|
|
215
|
+
this.addEdge(change.id, baseTypeChange.id);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Range depends on subtype
|
|
220
|
+
if (change.after?.kind === 'RANGE' && change.after?.subtype) {
|
|
221
|
+
const subTypeChange = changes.find(
|
|
222
|
+
c =>
|
|
223
|
+
c.objectType === 'type' &&
|
|
224
|
+
c.objectKey?.includes(change.after.subtype)
|
|
225
|
+
);
|
|
226
|
+
if (subTypeChange) {
|
|
227
|
+
this.addEdge(change.id, subTypeChange.id);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
addSequenceDependencies(change, changes, desired, current) {
|
|
233
|
+
// Sequence may be owned by a column
|
|
234
|
+
const ownedBy = change.after?.ownedBy || change.before?.ownedBy;
|
|
235
|
+
if (ownedBy) {
|
|
236
|
+
const colChange = changes.find(c => c.objectKey === ownedBy);
|
|
237
|
+
if (colChange) {
|
|
238
|
+
this.addEdge(change.id, colChange.id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
addTableDependencies(change, changes, desired, current) {
|
|
244
|
+
// Table depends on schema
|
|
245
|
+
if (change.changeType === 'CREATE_TABLE') {
|
|
246
|
+
const schemaChange = changes.find(
|
|
247
|
+
c =>
|
|
248
|
+
c.objectType === 'schema' &&
|
|
249
|
+
c.objectKey === change.schema
|
|
250
|
+
);
|
|
251
|
+
if (schemaChange) {
|
|
252
|
+
this.addEdge(change.id, schemaChange.id);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Table depends on extensions that provide types it uses
|
|
257
|
+
// (e.g., PostGIS extension provides geometry types)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
addEdge(fromId, toId) {
|
|
261
|
+
if (!this.graph.has(fromId) || !this.graph.has(toId)) return;
|
|
262
|
+
|
|
263
|
+
const from = this.graph.get(fromId);
|
|
264
|
+
const to = this.graph.get(toId);
|
|
265
|
+
|
|
266
|
+
to.dependents.add(fromId);
|
|
267
|
+
from.dependencies.add(toId);
|
|
268
|
+
|
|
269
|
+
this.edges.push({ from: fromId, to: toId, type: 'dependency' });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Topological sort (Kahn's algorithm).
|
|
274
|
+
*/
|
|
275
|
+
topologicalSort(changes) {
|
|
276
|
+
const result = [];
|
|
277
|
+
const inDegree = new Map();
|
|
278
|
+
const nodes = new Map();
|
|
279
|
+
|
|
280
|
+
// Initialize
|
|
281
|
+
for (const [id, node] of this.graph) {
|
|
282
|
+
inDegree.set(id, node.dependencies.size);
|
|
283
|
+
nodes.set(id, { ...node });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Queue for nodes with no dependencies
|
|
287
|
+
const queue = [];
|
|
288
|
+
|
|
289
|
+
for (const [id, degree] of inDegree) {
|
|
290
|
+
if (degree === 0) {
|
|
291
|
+
queue.push(id);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Sort queue by object type priority
|
|
296
|
+
this.sortQueueByPriority(queue, changes);
|
|
297
|
+
|
|
298
|
+
// Process
|
|
299
|
+
while (queue.length > 0) {
|
|
300
|
+
const currentId = queue.shift();
|
|
301
|
+
const node = this.graph.get(currentId);
|
|
302
|
+
|
|
303
|
+
if (!node) continue;
|
|
304
|
+
|
|
305
|
+
result.push(node.change);
|
|
306
|
+
|
|
307
|
+
// Update dependents
|
|
308
|
+
for (const dependentId of node.dependents) {
|
|
309
|
+
const newDegree = inDegree.get(dependentId) - 1;
|
|
310
|
+
inDegree.set(dependentId, newDegree);
|
|
311
|
+
|
|
312
|
+
if (newDegree === 0) {
|
|
313
|
+
// Insert at correct position based on priority
|
|
314
|
+
this.insertByPriority(queue, dependentId, changes);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check for cycles
|
|
320
|
+
if (result.length !== changes.length) {
|
|
321
|
+
this.cycleDetected = true;
|
|
322
|
+
// Add remaining changes with cycle warning
|
|
323
|
+
for (const change of changes) {
|
|
324
|
+
if (!result.includes(change)) {
|
|
325
|
+
change.cycleWarning = true;
|
|
326
|
+
result.push(change);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
sortQueueByPriority(queue, changes) {
|
|
335
|
+
const priority = this.getObjectTypePriority();
|
|
336
|
+
|
|
337
|
+
queue.sort((a, b) => {
|
|
338
|
+
const changeA = changes.find(c => c.id === a);
|
|
339
|
+
const changeB = changes.find(c => c.id === b);
|
|
340
|
+
|
|
341
|
+
const pA = priority[changeA?.objectType] || 99;
|
|
342
|
+
const pB = priority[changeB?.objectType] || 99;
|
|
343
|
+
|
|
344
|
+
return pA - pB;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
insertByPriority(queue, id, changes) {
|
|
349
|
+
const priority = this.getObjectTypePriority();
|
|
350
|
+
const change = changes.find(c => c.id === id);
|
|
351
|
+
const p = priority[change?.objectType] || 99;
|
|
352
|
+
|
|
353
|
+
let inserted = false;
|
|
354
|
+
for (let i = 0; i < queue.length; i++) {
|
|
355
|
+
const qChange = changes.find(c => c.id === queue[i]);
|
|
356
|
+
const qP = priority[qChange?.objectType] || 99;
|
|
357
|
+
|
|
358
|
+
if (p < qP) {
|
|
359
|
+
queue.splice(i, 0, id);
|
|
360
|
+
inserted = true;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!inserted) {
|
|
366
|
+
queue.push(id);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
getObjectTypePriority() {
|
|
371
|
+
return {
|
|
372
|
+
schema: 1,
|
|
373
|
+
extension: 2,
|
|
374
|
+
type: 3,
|
|
375
|
+
sequence: 4,
|
|
376
|
+
table: 5,
|
|
377
|
+
column: 6,
|
|
378
|
+
index: 7,
|
|
379
|
+
constraint: 8,
|
|
380
|
+
view: 9,
|
|
381
|
+
materializedView: 10,
|
|
382
|
+
function: 11,
|
|
383
|
+
procedure: 12,
|
|
384
|
+
trigger: 13,
|
|
385
|
+
policy: 14,
|
|
386
|
+
rule: 15,
|
|
387
|
+
eventTrigger: 16,
|
|
388
|
+
cast: 17,
|
|
389
|
+
collation: 18,
|
|
390
|
+
statistics: 19,
|
|
391
|
+
publication: 20,
|
|
392
|
+
subscription: 21,
|
|
393
|
+
operator: 22,
|
|
394
|
+
operatorClass: 23,
|
|
395
|
+
operatorFamily: 24,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Assign migration phases.
|
|
401
|
+
*/
|
|
402
|
+
assignPhases(changes) {
|
|
403
|
+
for (const change of changes) {
|
|
404
|
+
change.phase = this.computePhase(change);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return changes;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
computePhase(change) {
|
|
411
|
+
// Phase definitions:
|
|
412
|
+
// 1-2: Pre-flight
|
|
413
|
+
// 3: Extensions
|
|
414
|
+
// 4: Types
|
|
415
|
+
// 5: Schemas
|
|
416
|
+
// 6: Tables (CREATE)
|
|
417
|
+
// 7: Columns (ADD)
|
|
418
|
+
// 8: Sequences
|
|
419
|
+
// 9: Indexes (CREATE)
|
|
420
|
+
// 10: Non-FK constraints
|
|
421
|
+
// 11: Data migration
|
|
422
|
+
// 12: FK constraints (ADD)
|
|
423
|
+
// 13: Validate constraints
|
|
424
|
+
// 14: Views
|
|
425
|
+
// 15: Materialized views
|
|
426
|
+
// 16: Functions
|
|
427
|
+
// 17: Triggers
|
|
428
|
+
// 18: Policies
|
|
429
|
+
// 19: Rules
|
|
430
|
+
// 20: Other behavioral
|
|
431
|
+
// 21: Grants
|
|
432
|
+
// 22: Comments
|
|
433
|
+
// 23: Indexes CONCURRENTLY
|
|
434
|
+
// 24: Cleanup
|
|
435
|
+
// 25: Post-flight
|
|
436
|
+
|
|
437
|
+
const type = change.objectType;
|
|
438
|
+
const changeType = change.changeType;
|
|
439
|
+
|
|
440
|
+
if (type === 'extension') return 3;
|
|
441
|
+
if (type === 'type' && changeType === 'CREATE') return 4;
|
|
442
|
+
if (type === 'schema') return 5;
|
|
443
|
+
if (type === 'table' && changeType === 'CREATE') return 6;
|
|
444
|
+
if (type === 'column' && changeType.includes('ADD')) return 7;
|
|
445
|
+
if (type === 'sequence') return 8;
|
|
446
|
+
if (type === 'index' && changeType === 'CREATE' && !change.isConcurrent) return 9;
|
|
447
|
+
if (type === 'constraint' && change.constraintType !== 'FOREIGN_KEY') return 10;
|
|
448
|
+
if (change.property === 'dataType' && change.castRequired) return 11;
|
|
449
|
+
if (type === 'constraint' && change.constraintType === 'FOREIGN_KEY') return 12;
|
|
450
|
+
if (change.property === 'isValidated' && change.changeType.includes('VALIDATE')) return 13;
|
|
451
|
+
if (type === 'view') return 14;
|
|
452
|
+
if (type === 'materializedView') return 15;
|
|
453
|
+
if (type === 'function' || type === 'procedure') return 16;
|
|
454
|
+
if (type === 'trigger') return 17;
|
|
455
|
+
if (type === 'policy') return 18;
|
|
456
|
+
if (type === 'rule') return 19;
|
|
457
|
+
if (['eventTrigger', 'cast', 'operator', 'textSearchConfig', 'textSearchDict'].includes(type)) return 20;
|
|
458
|
+
if (type === 'grant' || change.property === 'privileges') return 21;
|
|
459
|
+
if (change.property === 'comment') return 22;
|
|
460
|
+
if (type === 'index' && change.isConcurrent) return 23;
|
|
461
|
+
if (change.property === 'notNull' && change.desiredValue === true && change.safePatternApplied) return 24;
|
|
462
|
+
if (type === 'table' && changeType === 'DROP') return 24; // Drops last
|
|
463
|
+
|
|
464
|
+
return 10; // Default
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Helper methods.
|
|
469
|
+
*/
|
|
470
|
+
|
|
471
|
+
getTableKey(change) {
|
|
472
|
+
return change.objectKey?.split('.').slice(0, 2).join('.');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
extractViewDependencies(definition) {
|
|
476
|
+
if (!definition) return [];
|
|
477
|
+
const deps = [];
|
|
478
|
+
|
|
479
|
+
// Extract table names from FROM/JOIN
|
|
480
|
+
const fromMatch = definition.match(/(?:FROM|JOIN)\s+([a-z_][a-z0-9_]*)/gi);
|
|
481
|
+
if (fromMatch) {
|
|
482
|
+
for (const m of fromMatch) {
|
|
483
|
+
const tableName = m.split(/\s+/)[1];
|
|
484
|
+
if (tableName && !this.isBuiltInTable(tableName)) {
|
|
485
|
+
deps.push(tableName);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return deps;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
extractTypeName(type) {
|
|
494
|
+
if (!type) return null;
|
|
495
|
+
return type.replace(/\[\]$/, '').replace(/\(.*\)$/, '').trim();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
isBuiltInType(type) {
|
|
499
|
+
const builtins = [
|
|
500
|
+
'integer', 'bigint', 'smallint', 'serial', 'bigserial',
|
|
501
|
+
'text', 'character varying', 'varchar', 'boolean', 'bool',
|
|
502
|
+
'date', 'timestamp', 'timestamptz', 'time', 'timetz',
|
|
503
|
+
'uuid', 'json', 'jsonb', 'bytea', 'real', 'double precision',
|
|
504
|
+
'numeric', 'decimal', 'money',
|
|
505
|
+
];
|
|
506
|
+
return builtins.includes(type.toLowerCase().trim());
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
isBuiltInTable(name) {
|
|
510
|
+
const builtins = ['pg_catalog', 'information_schema'];
|
|
511
|
+
return builtins.some(b => name.startsWith(b));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Get the dependency graph.
|
|
516
|
+
*/
|
|
517
|
+
getGraph() {
|
|
518
|
+
return {
|
|
519
|
+
nodes: Array.from(this.graph.keys()),
|
|
520
|
+
edges: this.edges,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|