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.
Files changed (104) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +304 -0
  3. package/package.json +52 -0
  4. package/src/behavioral/behavioral-applier.js +374 -0
  5. package/src/behavioral/behavioral-extractor.js +266 -0
  6. package/src/behavioral/behavioral-puller.js +35 -0
  7. package/src/behavioral/index.js +4 -0
  8. package/src/behavioral/phase-sorter.js +24 -0
  9. package/src/constants.js +97 -0
  10. package/src/ddl-generator/alter-generator.js +1512 -0
  11. package/src/ddl-generator/comment-generator.js +95 -0
  12. package/src/ddl-generator/create-generator.js +1426 -0
  13. package/src/ddl-generator/drop-generator.js +194 -0
  14. package/src/ddl-generator/generator.js +78 -0
  15. package/src/ddl-generator/grant-generator.js +31 -0
  16. package/src/ddl-generator/index.js +3 -0
  17. package/src/ddl-generator/pg-version.js +15 -0
  18. package/src/ddl-generator/rename-generator.js +88 -0
  19. package/src/ddl-generator/safe-patterns.js +241 -0
  20. package/src/differ/change-classifier.js +192 -0
  21. package/src/differ/dependency-resolver.js +523 -0
  22. package/src/differ/index.js +12 -0
  23. package/src/differ/object-matcher.js +510 -0
  24. package/src/differ/property-differ.js +1134 -0
  25. package/src/differ/risk-tagger.js +561 -0
  26. package/src/differ/schema-differ.js +309 -0
  27. package/src/differ/utils/levenshtein.js +210 -0
  28. package/src/differ/utils/path-builder.js +198 -0
  29. package/src/differ/utils/type-compatibility.js +472 -0
  30. package/src/errors.js +147 -0
  31. package/src/executor/drift-detector.js +221 -0
  32. package/src/executor/index.js +7 -0
  33. package/src/executor/lock-manager.js +308 -0
  34. package/src/executor/migration-executor.js +1249 -0
  35. package/src/executor/progress-tracker.js +81 -0
  36. package/src/executor/recovery-manager.js +47 -0
  37. package/src/executor/snapshot-manager.js +22 -0
  38. package/src/executor/sql-splitter.js +120 -0
  39. package/src/executor/transaction-manager.js +288 -0
  40. package/src/index.js +483 -0
  41. package/src/introspection/index.js +4 -0
  42. package/src/introspection/introspector.js +250 -0
  43. package/src/introspection/queries/access-methods.js +29 -0
  44. package/src/introspection/queries/casts.js +38 -0
  45. package/src/introspection/queries/collations.js +51 -0
  46. package/src/introspection/queries/comments.js +66 -0
  47. package/src/introspection/queries/constraints.js +65 -0
  48. package/src/introspection/queries/conversions.js +39 -0
  49. package/src/introspection/queries/databases.js +43 -0
  50. package/src/introspection/queries/default-privileges.js +37 -0
  51. package/src/introspection/queries/event-triggers.js +43 -0
  52. package/src/introspection/queries/extensions.js +21 -0
  53. package/src/introspection/queries/foreign-data.js +136 -0
  54. package/src/introspection/queries/functions.js +75 -0
  55. package/src/introspection/queries/grants.js +38 -0
  56. package/src/introspection/queries/index.js +33 -0
  57. package/src/introspection/queries/indexes.js +99 -0
  58. package/src/introspection/queries/inheritance.js +24 -0
  59. package/src/introspection/queries/multiranges.js +37 -0
  60. package/src/introspection/queries/operators.js +180 -0
  61. package/src/introspection/queries/partitions.js +42 -0
  62. package/src/introspection/queries/pg18-19.js +43 -0
  63. package/src/introspection/queries/policies.js +35 -0
  64. package/src/introspection/queries/procedural-languages.js +36 -0
  65. package/src/introspection/queries/publications.js +114 -0
  66. package/src/introspection/queries/roles.js +66 -0
  67. package/src/introspection/queries/rules.js +49 -0
  68. package/src/introspection/queries/sequences.js +37 -0
  69. package/src/introspection/queries/statistics.js +74 -0
  70. package/src/introspection/queries/subscriptions.js +99 -0
  71. package/src/introspection/queries/tables.js +151 -0
  72. package/src/introspection/queries/tablespaces.js +36 -0
  73. package/src/introspection/queries/text-search.js +146 -0
  74. package/src/introspection/queries/triggers.js +61 -0
  75. package/src/introspection/queries/types.js +106 -0
  76. package/src/introspection/queries/views.js +146 -0
  77. package/src/introspection/translator.js +1283 -0
  78. package/src/introspection/version-detector.js +25 -0
  79. package/src/planner/backfill-planner.js +36 -0
  80. package/src/planner/dry-run.js +21 -0
  81. package/src/planner/index.js +6 -0
  82. package/src/planner/migration-planner.js +356 -0
  83. package/src/planner/smart-migrator.js +82 -0
  84. package/src/planner/step-sequencer.js +61 -0
  85. package/src/planner/type-registry.js +47 -0
  86. package/src/risk/compatibility-checker.js +29 -0
  87. package/src/risk/data-loss-checker.js +32 -0
  88. package/src/risk/destructive-checker.js +38 -0
  89. package/src/risk/index.js +6 -0
  90. package/src/risk/lock-analyzer.js +39 -0
  91. package/src/risk/recommendations.js +44 -0
  92. package/src/risk/risk-engine.js +25 -0
  93. package/src/storage/index.js +5 -0
  94. package/src/storage/memory-storage.js +87 -0
  95. package/src/storage/migration-table.js +423 -0
  96. package/src/storage/rollback-generator.js +344 -0
  97. package/src/types/changes.js +136 -0
  98. package/src/types/connection.js +17 -0
  99. package/src/types/execution.js +107 -0
  100. package/src/types/index.js +1 -0
  101. package/src/types/migration.js +67 -0
  102. package/src/types/risk.js +32 -0
  103. package/src/types/schema.d.ts +1004 -0
  104. 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
+ }