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,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavioral Applier - Apply behavioral changes after structural changes
|
|
3
|
+
* Order: Functions → Views → Materialized Views → Triggers → Policies → Rules → Event Triggers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class BehavioralApplier {
|
|
7
|
+
/**
|
|
8
|
+
* @param {import('pg').Pool} pool
|
|
9
|
+
*/
|
|
10
|
+
constructor(pool) {
|
|
11
|
+
this.pool = pool;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get ordered changes for behavioral objects
|
|
16
|
+
* @param {Array} changes
|
|
17
|
+
* @returns {Array} Ordered behavioral changes
|
|
18
|
+
*/
|
|
19
|
+
getOrder(changes) {
|
|
20
|
+
const order = [
|
|
21
|
+
'CREATE_FUNCTION',
|
|
22
|
+
'REPLACE_FUNCTION',
|
|
23
|
+
'RECREATE_FUNCTION',
|
|
24
|
+
'CREATE_PROCEDURE',
|
|
25
|
+
'REPLACE_PROCEDURE',
|
|
26
|
+
'RECREATE_PROCEDURE',
|
|
27
|
+
'CREATE_VIEW',
|
|
28
|
+
'REPLACE_VIEW',
|
|
29
|
+
'RECREATE_VIEW',
|
|
30
|
+
'CREATE_MATERIALIZED_VIEW',
|
|
31
|
+
'REPLACE_MATERIALIZED_VIEW',
|
|
32
|
+
'RECREATE_MATERIALIZED_VIEW',
|
|
33
|
+
'CREATE_TRIGGER',
|
|
34
|
+
'RECREATE_TRIGGER',
|
|
35
|
+
'ALTER_TRIGGER_ENABLE',
|
|
36
|
+
'REPLACE_TRIGGER',
|
|
37
|
+
'CREATE_POLICY',
|
|
38
|
+
'REPLACE_POLICY',
|
|
39
|
+
'RECREATE_POLICY',
|
|
40
|
+
'CREATE_RULE',
|
|
41
|
+
'REPLACE_RULE',
|
|
42
|
+
'RECREATE_RULE',
|
|
43
|
+
'CREATE_EVENT_TRIGGER',
|
|
44
|
+
'RECREATE_EVENT_TRIGGER',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
return changes
|
|
48
|
+
.filter(c => c.track === 2 || this.isBehavioral(c))
|
|
49
|
+
.sort((a, b) => {
|
|
50
|
+
const aIdx = order.indexOf(a.changeType);
|
|
51
|
+
const bIdx = order.indexOf(b.changeType);
|
|
52
|
+
if (aIdx === -1 && bIdx === -1) return 0;
|
|
53
|
+
if (aIdx === -1) return 1;
|
|
54
|
+
if (bIdx === -1) return -1;
|
|
55
|
+
return aIdx - bIdx;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if object type is behavioral
|
|
61
|
+
*/
|
|
62
|
+
isBehavioral(change) {
|
|
63
|
+
const behavioralTypes = [
|
|
64
|
+
'view', 'materializedView', 'function', 'procedure',
|
|
65
|
+
'trigger', 'eventTrigger', 'policy', 'rule', 'aggregate'
|
|
66
|
+
];
|
|
67
|
+
return behavioralTypes.includes(change.objectType);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate SQL for a behavioral change
|
|
72
|
+
* @param {Object} change
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
generateSQL(change) {
|
|
76
|
+
const changeType = change.changeType;
|
|
77
|
+
const objectType = change.objectType;
|
|
78
|
+
const obj = change.after || {};
|
|
79
|
+
|
|
80
|
+
switch (true) {
|
|
81
|
+
case changeType?.startsWith('CREATE') && objectType === 'view':
|
|
82
|
+
return this.generateCreateView(change, obj);
|
|
83
|
+
|
|
84
|
+
case changeType?.includes('REPLACE') && objectType === 'view':
|
|
85
|
+
return `CREATE OR REPLACE VIEW ${change.objectKey} AS ${obj.definition || change.desiredValue};`;
|
|
86
|
+
|
|
87
|
+
case changeType?.startsWith('CREATE') && objectType === 'materializedView':
|
|
88
|
+
return this.generateCreateMatView(change, obj);
|
|
89
|
+
|
|
90
|
+
case changeType?.startsWith('CREATE') && (objectType === 'function' || objectType === 'procedure'):
|
|
91
|
+
return this.generateCreateFunction(change, obj);
|
|
92
|
+
|
|
93
|
+
case changeType?.includes('REPLACE') && (objectType === 'function' || objectType === 'procedure'):
|
|
94
|
+
return this.generateReplaceFunction(change, obj);
|
|
95
|
+
|
|
96
|
+
case changeType?.startsWith('CREATE') && objectType === 'trigger':
|
|
97
|
+
return this.generateCreateTrigger(change, obj);
|
|
98
|
+
|
|
99
|
+
case changeType?.includes('RECREATE') && objectType === 'trigger':
|
|
100
|
+
return this.generateRecreateTrigger(change, obj);
|
|
101
|
+
|
|
102
|
+
case changeType === 'ALTER_TRIGGER_ENABLE':
|
|
103
|
+
return this.generateAlterTriggerEnable(change, obj);
|
|
104
|
+
|
|
105
|
+
case changeType?.startsWith('CREATE') && objectType === 'policy':
|
|
106
|
+
return this.generateCreatePolicy(change, obj);
|
|
107
|
+
|
|
108
|
+
case changeType?.includes('ALTER') && objectType === 'policy':
|
|
109
|
+
return this.generateAlterPolicy(change, obj);
|
|
110
|
+
|
|
111
|
+
case changeType?.startsWith('CREATE') && objectType === 'rule':
|
|
112
|
+
return this.generateCreateRule(change, obj);
|
|
113
|
+
|
|
114
|
+
case changeType?.startsWith('CREATE') && objectType === 'eventTrigger':
|
|
115
|
+
return this.generateCreateEventTrigger(change, obj);
|
|
116
|
+
|
|
117
|
+
default:
|
|
118
|
+
return this.getBehavioralDefaultSQL(change);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
generateCreateView(change, view) {
|
|
123
|
+
let sql = `CREATE VIEW ${change.objectKey}`;
|
|
124
|
+
if (view.columns && view.columns.length > 0) {
|
|
125
|
+
sql += ` (${view.columns.map(c => this.ident(c)).join(', ')})`;
|
|
126
|
+
}
|
|
127
|
+
sql += ` AS ${view.definition}`;
|
|
128
|
+
if (view.checkOption) {
|
|
129
|
+
sql += ` WITH ${view.checkOption} CHECK OPTION`;
|
|
130
|
+
}
|
|
131
|
+
return sql + ';';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
generateCreateMatView(change, mv) {
|
|
135
|
+
let sql = `CREATE MATERIALIZED VIEW ${change.objectKey}`;
|
|
136
|
+
sql += ` AS ${mv.definition}`;
|
|
137
|
+
if (mv.isWithData === false) {
|
|
138
|
+
sql += ' WITH NO DATA';
|
|
139
|
+
}
|
|
140
|
+
return sql + ';';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
generateCreateFunction(change, fn) {
|
|
144
|
+
const keyword = change.objectType === 'procedure' ? 'PROCEDURE' : 'FUNCTION';
|
|
145
|
+
const args = (fn.argumentTypes || []).join(', ');
|
|
146
|
+
|
|
147
|
+
let sql = `CREATE ${keyword} ${change.objectKey}(${args})`;
|
|
148
|
+
|
|
149
|
+
if (keyword === 'FUNCTION') {
|
|
150
|
+
sql += ` RETURNS ${fn.returnType || 'void'}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
sql += ` LANGUAGE ${fn.language || 'plpgsql'}`;
|
|
154
|
+
|
|
155
|
+
if (fn.volatility && fn.volatility !== 'VOLATILE') {
|
|
156
|
+
sql += ` ${fn.volatility}`;
|
|
157
|
+
}
|
|
158
|
+
if (fn.securityType === 'DEFINER') {
|
|
159
|
+
sql += ' SECURITY DEFINER';
|
|
160
|
+
}
|
|
161
|
+
if (fn.isNullCall === false) {
|
|
162
|
+
sql += ' RETURNS NULL ON NULL INPUT';
|
|
163
|
+
}
|
|
164
|
+
if (fn.parallelSafety) {
|
|
165
|
+
sql += ` PARALLEL ${fn.parallelSafety}`;
|
|
166
|
+
}
|
|
167
|
+
if (fn.cost) {
|
|
168
|
+
sql += ` COST ${fn.cost}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
sql += ` AS $$${fn.source || ''}$$;`;
|
|
172
|
+
return sql;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
generateReplaceFunction(change, fn) {
|
|
176
|
+
const keyword = change.objectType === 'procedure' ? 'PROCEDURE' : 'FUNCTION';
|
|
177
|
+
const args = (fn.argumentTypes || change.before?.argumentTypes || []).join(', ');
|
|
178
|
+
|
|
179
|
+
let sql = `CREATE OR REPLACE ${keyword} ${change.objectKey}(${args})`;
|
|
180
|
+
|
|
181
|
+
if (keyword === 'FUNCTION') {
|
|
182
|
+
sql += ` RETURNS ${fn.returnType || change.before?.returnType || 'void'}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
sql += ` LANGUAGE ${fn.language || 'plpgsql'}`;
|
|
186
|
+
sql += ` AS $$${fn.source || change.desiredValue || ''}$$;`;
|
|
187
|
+
|
|
188
|
+
return sql;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
generateCreateTrigger(change, trig) {
|
|
192
|
+
let sql = `CREATE TRIGGER ${this.ident(trig.name)}`;
|
|
193
|
+
sql += ` ${trig.timing}`;
|
|
194
|
+
sql += ` ${(trig.events || ['INSERT']).join(' OR ')}`;
|
|
195
|
+
sql += ` ON ${trig.schema ? this.ident(trig.schema) + '.' : ''}${this.ident(trig.tableName)}`;
|
|
196
|
+
|
|
197
|
+
if (trig.isConstraint) {
|
|
198
|
+
sql += ' FOR EACH ROW';
|
|
199
|
+
if (trig.constraint) {
|
|
200
|
+
sql += ` FROM ${trig.constraint}`;
|
|
201
|
+
}
|
|
202
|
+
if (trig.isDeferrable) {
|
|
203
|
+
sql += ` DEFERRABLE INITIALLY ${trig.deferred ? 'DEFERRED' : 'IMMEDIATE'}`;
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
sql += ` FOR EACH ${trig.orientation || 'ROW'}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (trig.whenCondition) {
|
|
210
|
+
sql += ` WHEN (${trig.whenCondition})`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
sql += ` EXECUTE FUNCTION ${trig.functionName}`;
|
|
214
|
+
if (trig.functionArguments) {
|
|
215
|
+
sql += `(${trig.functionArguments})`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return sql + ';';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
generateRecreateTrigger(change, trig) {
|
|
222
|
+
const lines = [];
|
|
223
|
+
const trigName = trig.name || change.name;
|
|
224
|
+
const tableName = trig.tableName || change.after?.tableName;
|
|
225
|
+
|
|
226
|
+
lines.push(`DROP TRIGGER IF EXISTS ${this.ident(trigName)} ON ${this.ident(tableName)};`);
|
|
227
|
+
lines.push(this.generateCreateTrigger({ ...change, changeType: 'CREATE_TRIGGER' }, trig));
|
|
228
|
+
|
|
229
|
+
return lines.join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
generateAlterTriggerEnable(change, trig) {
|
|
233
|
+
const trigName = trig.name || change.name;
|
|
234
|
+
const tableName = trig.tableName || change.after?.tableName;
|
|
235
|
+
const action = change.desiredValue === 'DISABLED' || change.desiredValue === false
|
|
236
|
+
? 'DISABLE'
|
|
237
|
+
: 'ENABLE';
|
|
238
|
+
|
|
239
|
+
return `ALTER TABLE ${this.ident(tableName)} ${action} TRIGGER ${this.ident(trigName)};`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
generateCreatePolicy(change, policy) {
|
|
243
|
+
let sql = `CREATE POLICY ${this.ident(policy.name)} ON ${change.objectKey.split('.').slice(0, -1).join('.')}`;
|
|
244
|
+
|
|
245
|
+
if (policy.isPermissive) {
|
|
246
|
+
sql += ' AS PERMISSIVE';
|
|
247
|
+
} else {
|
|
248
|
+
sql += ' AS RESTRICTIVE';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (policy.command) {
|
|
252
|
+
sql += ` FOR ${policy.command}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (policy.roles && policy.roles.length > 0) {
|
|
256
|
+
sql += ' TO ' + policy.roles.map(r => r === 'PUBLIC' ? 'PUBLIC' : this.ident(r)).join(', ');
|
|
257
|
+
} else {
|
|
258
|
+
sql += ' TO PUBLIC';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (policy.using) {
|
|
262
|
+
sql += ` USING (${policy.using})`;
|
|
263
|
+
}
|
|
264
|
+
if (policy.withCheck) {
|
|
265
|
+
sql += ` WITH CHECK (${policy.withCheck})`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return sql + ';';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
generateAlterPolicy(change, policy) {
|
|
272
|
+
const parts = change.objectKey.split('.');
|
|
273
|
+
const policyName = parts.pop();
|
|
274
|
+
const tableName = parts.join('.');
|
|
275
|
+
const lines = [];
|
|
276
|
+
|
|
277
|
+
if (change.property === 'roles' && change.desiredValue) {
|
|
278
|
+
const roles = change.desiredValue.map(r => r === 'PUBLIC' ? 'PUBLIC' : this.ident(r)).join(', ');
|
|
279
|
+
lines.push(`ALTER POLICY ${this.ident(policyName)} ON ${tableName} TO ${roles};`);
|
|
280
|
+
}
|
|
281
|
+
if (change.property === 'using' && change.desiredValue !== undefined) {
|
|
282
|
+
lines.push(`ALTER POLICY ${this.ident(policyName)} ON ${tableName} USING (${change.desiredValue || 'true'});`);
|
|
283
|
+
}
|
|
284
|
+
if (change.property === 'withCheck' && change.desiredValue) {
|
|
285
|
+
lines.push(`ALTER POLICY ${this.ident(policyName)} ON ${tableName} WITH CHECK (${change.desiredValue});`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return lines.length > 0 ? lines.join('\n') : `-- No ALTER_POLICY SQL for ${change.objectKey}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
generateCreateRule(change, rule) {
|
|
292
|
+
let sql = `CREATE RULE ${this.ident(rule.name)} AS`;
|
|
293
|
+
sql += rule.isInstead ? ' ON INSTEAD' : ' ON';
|
|
294
|
+
sql += ` ${rule.events?.join(' OR ') || 'INSERT'}`;
|
|
295
|
+
sql += ` TO ${change.objectKey.split('.').slice(0, -1).join('.')}`;
|
|
296
|
+
|
|
297
|
+
if (rule.condition) {
|
|
298
|
+
sql += ` WHERE (${rule.condition})`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (rule.commands && rule.commands.length > 0) {
|
|
302
|
+
sql += ' DO ';
|
|
303
|
+
if (rule.commands.length > 1) {
|
|
304
|
+
sql += `(${rule.commands.map(c => c.endsWith(';') ? c : c + ';').join(' ')})`;
|
|
305
|
+
} else {
|
|
306
|
+
sql += rule.commands[0];
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
sql += ' DO INSTEAD NOTHING';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return sql + ';';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
generateCreateEventTrigger(change, et) {
|
|
316
|
+
let sql = `CREATE EVENT TRIGGER ${this.ident(et.name)}`;
|
|
317
|
+
sql += ` ON ${et.event}`;
|
|
318
|
+
|
|
319
|
+
if (et.tags && et.tags.length > 0) {
|
|
320
|
+
sql += ` WHEN TAG IN (${et.tags.map(t => `'${t}'`).join(', ')})`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
sql += ` EXECUTE FUNCTION ${et.functionName}`;
|
|
324
|
+
if (et.functionArguments) {
|
|
325
|
+
sql += `(${et.functionArguments})`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return sql + ';';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
getBehavioralDefaultSQL(change) {
|
|
332
|
+
if (change.changeType === 'DROP' || change.changeType?.startsWith('REMOVE')) {
|
|
333
|
+
return this.generateDrop(change);
|
|
334
|
+
}
|
|
335
|
+
return `-- Unsupported behavioral change: ${change.changeType} ${change.objectType}`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
generateDrop(change) {
|
|
339
|
+
const type = change.objectType;
|
|
340
|
+
const path = change.objectKey;
|
|
341
|
+
|
|
342
|
+
switch (type) {
|
|
343
|
+
case 'view':
|
|
344
|
+
return `DROP VIEW IF EXISTS ${path} CASCADE;`;
|
|
345
|
+
case 'materializedView':
|
|
346
|
+
return `DROP MATERIALIZED VIEW IF EXISTS ${path} CASCADE;`;
|
|
347
|
+
case 'function':
|
|
348
|
+
const fnArgs = change.before?.argumentTypes ? `(${change.before.argumentTypes.join(', ')})` : '';
|
|
349
|
+
return `DROP FUNCTION IF EXISTS ${path}${fnArgs} CASCADE;`;
|
|
350
|
+
case 'procedure':
|
|
351
|
+
const procArgs = change.before?.argumentTypes ? `(${change.before.argumentTypes.join(', ')})` : '';
|
|
352
|
+
return `DROP PROCEDURE IF EXISTS ${path}${procArgs} CASCADE;`;
|
|
353
|
+
case 'trigger':
|
|
354
|
+
const tableName = change.before?.tableName;
|
|
355
|
+
return `DROP TRIGGER IF EXISTS ${this.ident(change.name)} ON ${tableName};`;
|
|
356
|
+
case 'policy':
|
|
357
|
+
const polTable = change.before?.table;
|
|
358
|
+
return `DROP POLICY IF EXISTS ${this.ident(change.name)} ON ${polTable};`;
|
|
359
|
+
case 'rule':
|
|
360
|
+
const ruleTable = change.before?.tableName;
|
|
361
|
+
return `DROP RULE IF EXISTS ${this.ident(change.name)} ON ${ruleTable};`;
|
|
362
|
+
case 'eventTrigger':
|
|
363
|
+
return `DROP EVENT TRIGGER IF EXISTS ${this.ident(change.name)};`;
|
|
364
|
+
default:
|
|
365
|
+
return `-- Unsupported DROP for ${type}`;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
ident(name) {
|
|
370
|
+
if (!name) return "''";
|
|
371
|
+
if (typeof name !== 'string') name = String(name);
|
|
372
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavioral Extractor - Extract behavioral objects from SchemaSnapshot
|
|
3
|
+
* Behavioral objects: views, functions, triggers, policies, rules, event triggers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class BehavioralExtractor {
|
|
7
|
+
/**
|
|
8
|
+
* Extract behavioral objects from a SchemaSnapshot
|
|
9
|
+
* @param {Object} snapshot - SchemaSnapshot from introspector
|
|
10
|
+
* @returns {Object} Extracted behavioral objects
|
|
11
|
+
*/
|
|
12
|
+
extractFromSnapshot(snapshot) {
|
|
13
|
+
const behavioral = {
|
|
14
|
+
views: [],
|
|
15
|
+
materializedViews: [],
|
|
16
|
+
functions: [],
|
|
17
|
+
procedures: [],
|
|
18
|
+
triggers: [],
|
|
19
|
+
policies: [],
|
|
20
|
+
rules: [],
|
|
21
|
+
eventTriggers: [],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
25
|
+
return behavioral;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const [schemaName, schema] of Object.entries(snapshot.schemas || {})) {
|
|
29
|
+
this.extractViews(schema, schemaName, behavioral);
|
|
30
|
+
this.extractFunctions(schema, schemaName, behavioral);
|
|
31
|
+
this.extractTriggers(schema, schemaName, behavioral);
|
|
32
|
+
this.extractPolicies(schema, schemaName, behavioral);
|
|
33
|
+
this.extractRules(schema, schemaName, behavioral);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const [name, et] of Object.entries(snapshot.eventTriggers || {})) {
|
|
37
|
+
behavioral.eventTriggers.push({
|
|
38
|
+
name: et.name || name,
|
|
39
|
+
event: et.event,
|
|
40
|
+
functionName: et.functionName,
|
|
41
|
+
enabled: et.enabled,
|
|
42
|
+
tags: et.tags,
|
|
43
|
+
functionDependency: et.functionName,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return behavioral;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
extractViews(schema, schemaName, behavioral) {
|
|
51
|
+
for (const view of schema.views || []) {
|
|
52
|
+
const viewObj = {
|
|
53
|
+
schema: schemaName,
|
|
54
|
+
name: view.name,
|
|
55
|
+
definition: view.definition,
|
|
56
|
+
checkOption: view.checkOption,
|
|
57
|
+
securityBarrier: view.securityBarrier,
|
|
58
|
+
securityInvoker: view.securityInvoker,
|
|
59
|
+
owner: view.owner,
|
|
60
|
+
dependencies: this.extractDependenciesFromSQL(view.definition, schemaName),
|
|
61
|
+
};
|
|
62
|
+
behavioral.views.push(viewObj);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const mv of schema.materializedViews || []) {
|
|
66
|
+
behavioral.materializedViews.push({
|
|
67
|
+
schema: schemaName,
|
|
68
|
+
name: mv.name,
|
|
69
|
+
definition: mv.definition,
|
|
70
|
+
isWithData: mv.isWithData,
|
|
71
|
+
tablespace: mv.tablespace,
|
|
72
|
+
dependencies: this.extractDependenciesFromSQL(mv.definition, schemaName),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
extractFunctions(schema, schemaName, behavioral) {
|
|
78
|
+
for (const fn of schema.functions || []) {
|
|
79
|
+
behavioral.functions.push({
|
|
80
|
+
schema: schemaName,
|
|
81
|
+
name: fn.name,
|
|
82
|
+
argumentTypes: fn.argumentTypes || [],
|
|
83
|
+
returnType: fn.returnType,
|
|
84
|
+
source: fn.source,
|
|
85
|
+
language: fn.language,
|
|
86
|
+
volatility: fn.volatility,
|
|
87
|
+
isNullCall: fn.isNullCall,
|
|
88
|
+
securityType: fn.securityType,
|
|
89
|
+
parallelSafety: fn.parallelSafety,
|
|
90
|
+
cost: fn.cost,
|
|
91
|
+
rows: fn.rows,
|
|
92
|
+
owner: fn.owner,
|
|
93
|
+
typeDependencies: this.extractTypeDependencies(fn),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const proc of schema.procedures || []) {
|
|
98
|
+
behavioral.procedures.push({
|
|
99
|
+
schema: schemaName,
|
|
100
|
+
name: proc.name,
|
|
101
|
+
argumentTypes: proc.argumentTypes || [],
|
|
102
|
+
source: proc.source,
|
|
103
|
+
language: proc.language,
|
|
104
|
+
owner: proc.owner,
|
|
105
|
+
typeDependencies: this.extractTypeDependencies(proc),
|
|
106
|
+
isProcedure: true,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
extractTriggers(schema, schemaName, behavioral) {
|
|
112
|
+
for (const trigger of schema.triggers || []) {
|
|
113
|
+
behavioral.triggers.push({
|
|
114
|
+
schema: schemaName,
|
|
115
|
+
name: trigger.name,
|
|
116
|
+
tableName: trigger.tableName,
|
|
117
|
+
timing: trigger.timing,
|
|
118
|
+
events: trigger.events,
|
|
119
|
+
functionName: trigger.functionName,
|
|
120
|
+
functionArguments: trigger.functionArguments,
|
|
121
|
+
whenCondition: trigger.whenCondition,
|
|
122
|
+
orientation: trigger.orientation,
|
|
123
|
+
enabled: trigger.enabled,
|
|
124
|
+
isConstraint: trigger.isConstraint,
|
|
125
|
+
functionDependency: trigger.functionName,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
extractPolicies(schema, schemaName, behavioral) {
|
|
131
|
+
for (const policy of schema.policies || []) {
|
|
132
|
+
behavioral.policies.push({
|
|
133
|
+
schema: schemaName,
|
|
134
|
+
name: policy.name,
|
|
135
|
+
tableName: policy.tableName,
|
|
136
|
+
command: policy.command,
|
|
137
|
+
roles: policy.roles,
|
|
138
|
+
using: policy.using,
|
|
139
|
+
withCheck: policy.withCheck,
|
|
140
|
+
isPermissive: policy.isPermissive,
|
|
141
|
+
functionDependencies: [
|
|
142
|
+
...this.extractFunctionRefs(policy.using),
|
|
143
|
+
...this.extractFunctionRefs(policy.withCheck),
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
extractRules(schema, schemaName, behavioral) {
|
|
150
|
+
for (const rule of schema.rules || []) {
|
|
151
|
+
behavioral.rules.push({
|
|
152
|
+
schema: schemaName,
|
|
153
|
+
name: rule.name,
|
|
154
|
+
tableName: rule.tableName,
|
|
155
|
+
events: rule.events,
|
|
156
|
+
isInstead: rule.isInstead,
|
|
157
|
+
commands: rule.commands,
|
|
158
|
+
condition: rule.condition,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Extract table/view/function dependencies from SQL
|
|
165
|
+
*/
|
|
166
|
+
extractDependenciesFromSQL(sql, currentSchema) {
|
|
167
|
+
if (!sql) return [];
|
|
168
|
+
|
|
169
|
+
const deps = [];
|
|
170
|
+
const normalizedSQL = sql.toLowerCase();
|
|
171
|
+
|
|
172
|
+
const fromPattern = /(?:from|join)\s+(?:(\w+)\.)?(\w+)/gi;
|
|
173
|
+
let match;
|
|
174
|
+
while ((match = fromPattern.exec(normalizedSQL)) !== null) {
|
|
175
|
+
const schema = match[1] || currentSchema;
|
|
176
|
+
const name = match[2];
|
|
177
|
+
const keywords = ['select', 'where', 'and', 'or', 'on', 'as', 'set', 'into', 'values', 'table', 'only', 'update', 'delete', 'insert'];
|
|
178
|
+
if (!keywords.includes(name)) {
|
|
179
|
+
deps.push({ schema, name, type: 'table_or_view' });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return deps;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Extract function references from an expression
|
|
188
|
+
*/
|
|
189
|
+
extractFunctionRefs(expression) {
|
|
190
|
+
if (!expression) return [];
|
|
191
|
+
|
|
192
|
+
const refs = [];
|
|
193
|
+
const fnPattern = /(\w+)\s*\(/gi;
|
|
194
|
+
let match;
|
|
195
|
+
while ((match = fnPattern.exec(expression)) !== null) {
|
|
196
|
+
const fnName = match[1].toLowerCase();
|
|
197
|
+
const keywords = ['coalesce', 'nullif', 'case', 'cast', 'exists', 'not', 'and', 'or', 'in', 'any', 'all', 'some', 'count', 'sum', 'avg', 'min', 'max', 'array', 'row', 'current_setting', 'concat', 'replace', 'substring'];
|
|
198
|
+
if (!keywords.includes(fnName)) {
|
|
199
|
+
refs.push(fnName);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return refs;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Extract type dependencies from a function
|
|
207
|
+
*/
|
|
208
|
+
extractTypeDependencies(fn) {
|
|
209
|
+
const deps = [];
|
|
210
|
+
|
|
211
|
+
for (const argType of fn.argumentTypes || []) {
|
|
212
|
+
if (!this.isBuiltInType(argType)) {
|
|
213
|
+
deps.push(argType);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (fn.returnType && !this.isBuiltInType(fn.returnType)) {
|
|
218
|
+
deps.push(fn.returnType);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return deps;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check if a type is built-in
|
|
226
|
+
*/
|
|
227
|
+
isBuiltInType(typeName) {
|
|
228
|
+
if (!typeName) return true;
|
|
229
|
+
const builtins = [
|
|
230
|
+
'integer', 'bigint', 'smallint', 'int', 'int2', 'int4', 'int8',
|
|
231
|
+
'serial', 'bigserial', 'smallserial', 'serial2', 'serial4', 'serial8',
|
|
232
|
+
'real', 'double precision', 'float', 'float4', 'float8', 'numeric', 'decimal', 'money',
|
|
233
|
+
'character varying', 'varchar', 'character', 'char', 'text',
|
|
234
|
+
'boolean', 'bool',
|
|
235
|
+
'date', 'time', 'timestamp', 'timestamptz', 'timetz', 'interval',
|
|
236
|
+
'uuid', 'json', 'jsonb', 'xml',
|
|
237
|
+
'bytea', 'bit', 'bit varying', 'varbit',
|
|
238
|
+
'point', 'line', 'lseg', 'box', 'path', 'polygon', 'circle',
|
|
239
|
+
'inet', 'cidr', 'macaddr', 'macaddr8',
|
|
240
|
+
'tsvector', 'tsquery',
|
|
241
|
+
'oid', 'regclass', 'regtype', 'regproc', 'regprocedure', 'regoper',
|
|
242
|
+
'record', 'void', 'trigger', 'event_trigger',
|
|
243
|
+
'any', 'anyelement', 'anyarray', 'anynonarray', 'anyenum', 'anyrange', 'anymultirange',
|
|
244
|
+
];
|
|
245
|
+
const normalized = typeName.toLowerCase().replace(/\[\]$/, '').replace(/\s+/g, ' ');
|
|
246
|
+
return builtins.includes(normalized) || normalized.startsWith('"pg_');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get execution order for behavioral objects
|
|
251
|
+
*/
|
|
252
|
+
getExecutionOrder(behavioral) {
|
|
253
|
+
const order = [];
|
|
254
|
+
|
|
255
|
+
order.push(...(behavioral.functions || []).filter(f => !f.isProcedure));
|
|
256
|
+
order.push(...(behavioral.procedures || []));
|
|
257
|
+
order.push(...(behavioral.views || []));
|
|
258
|
+
order.push(...(behavioral.materializedViews || []));
|
|
259
|
+
order.push(...(behavioral.triggers || []));
|
|
260
|
+
order.push(...(behavioral.policies || []));
|
|
261
|
+
order.push(...(behavioral.rules || []));
|
|
262
|
+
order.push(...(behavioral.eventTriggers || []));
|
|
263
|
+
|
|
264
|
+
return order;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class BehavioralPuller {
|
|
2
|
+
/** @param {import('pg').Pool} pool */
|
|
3
|
+
constructor(pool) {
|
|
4
|
+
this.pool = pool;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** @returns {Promise<Array<{type:string,phase:number,sql:string,name:string}>>} */
|
|
8
|
+
async pullFromDatabase() {
|
|
9
|
+
const items = [];
|
|
10
|
+
|
|
11
|
+
const extResult = await this.pool.query(`
|
|
12
|
+
SELECT 'CREATE EXTENSION IF NOT EXISTS ' || extname || ' SCHEMA ' || nspname AS sql,
|
|
13
|
+
extname AS name
|
|
14
|
+
FROM pg_extension e JOIN pg_namespace n ON n.oid = e.extnamespace
|
|
15
|
+
`);
|
|
16
|
+
for (const row of extResult.rows) {
|
|
17
|
+
items.push({ type: 'EXTENSION', phase: 0, sql: row.sql, name: row.name });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const fnResult = await this.pool.query(`
|
|
21
|
+
SELECT 'CREATE OR REPLACE FUNCTION ' || n.nspname || '.' || p.proname || '(' ||
|
|
22
|
+
pg_get_function_identity_arguments(p.oid) || ') RETURNS ' ||
|
|
23
|
+
pg_get_function_result(p.oid) || ' LANGUAGE ' || l.lanname || ' AS $$' ||
|
|
24
|
+
p.prosrc || '$$;' AS sql,
|
|
25
|
+
n.nspname || '.' || p.proname AS name
|
|
26
|
+
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace JOIN pg_language l ON l.oid = p.prolang
|
|
27
|
+
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
28
|
+
`);
|
|
29
|
+
for (const row of fnResult.rows) {
|
|
30
|
+
items.push({ type: 'FUNCTION', phase: 2, sql: row.sql, name: row.name });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return items;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class PhaseSorter {
|
|
2
|
+
/**
|
|
3
|
+
* @param {Array<{type:string,phase:number,sql:string,name:string}>} items
|
|
4
|
+
* @returns {Array<{type:string,phase:number,sql:string,name:string}>}
|
|
5
|
+
*/
|
|
6
|
+
sort(items) {
|
|
7
|
+
return [...items].sort((a, b) => {
|
|
8
|
+
if (a.phase !== b.phase) return a.phase - b.phase;
|
|
9
|
+
|
|
10
|
+
const phaseOrder = {
|
|
11
|
+
'EXTENSION': 1,
|
|
12
|
+
'TYPE': 2,
|
|
13
|
+
'TABLE': 3,
|
|
14
|
+
'FUNCTION': 4,
|
|
15
|
+
'VIEW': 5,
|
|
16
|
+
'TRIGGER': 6,
|
|
17
|
+
'POLICY': 7,
|
|
18
|
+
'GRANT': 8,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (phaseOrder[a.type] || 9) - (phaseOrder[b.type] || 9);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|