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,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,4 @@
1
+ export { BehavioralExtractor } from './behavioral-extractor.js';
2
+ export { BehavioralApplier } from './behavioral-applier.js';
3
+ export { BehavioralPuller } from './behavioral-puller.js';
4
+ export { PhaseSorter } from './phase-sorter.js';
@@ -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
+ }