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,12 @@
1
+ export {
2
+ SchemaDiffer,
3
+ ObjectMatcher,
4
+ PropertyDiffer,
5
+ DependencyResolver,
6
+ ChangeClassifier,
7
+ RiskTagger,
8
+ } from './schema-differ.js';
9
+
10
+ export { levenshtein, similarity } from './utils/levenshtein.js';
11
+ export { buildPath } from './utils/path-builder.js';
12
+ export { getCastInfo } from './utils/type-compatibility.js';
@@ -0,0 +1,510 @@
1
+ import { similarity, isSimilarEnough } from './utils/levenshtein.js';
2
+ import { sameTypeFamily, typesEqual, isImplicitCast } from './utils/type-compatibility.js';
3
+ import { buildPath } from './utils/path-builder.js';
4
+
5
+ /**
6
+ * Object matcher handles matching objects between two snapshots
7
+ * and detecting potential renames.
8
+ */
9
+
10
+ const RENAME_SIMILARITY_THRESHOLD = 0.15;
11
+ const HIGH_CONFIDENCE_THRESHOLD = 0.80;
12
+ const MEDIUM_CONFIDENCE_THRESHOLD = 0.55;
13
+ const LOW_CONFIDENCE_THRESHOLD = 0.35;
14
+
15
+ export class ObjectMatcher {
16
+ constructor() {
17
+ this.logger = console;
18
+ }
19
+
20
+ /**
21
+ * Match objects between desired and current snapshots.
22
+ * @param {Object} desired - Desired schema snapshot
23
+ * @param {Object} current - Current schema snapshot
24
+ * @returns {Object} Match result with creates, drops, matches, and renames
25
+ */
26
+ match(desired, current) {
27
+ const result = {
28
+ matches: [], // Objects that exist in both (same key)
29
+ creates: [], // Objects only in desired
30
+ drops: [], // Objects only in current
31
+ renames: [], // Detected renames
32
+ };
33
+
34
+ // Match all object types
35
+ this.matchObjects(desired.tables, current.tables, 'table', result, desired, current);
36
+ this.matchObjects(desired.views, current.views, 'view', result, desired, current);
37
+ this.matchObjects(desired.materializedViews, current.materializedViews, 'materializedView', result, desired, current);
38
+ this.matchObjects(desired.functions, current.functions, 'function', result, desired, current);
39
+ this.matchObjects(desired.procedures, current.procedures, 'procedure', result, desired, current);
40
+ this.matchObjects(desired.aggregates, current.aggregates, 'aggregate', result, desired, current);
41
+ this.matchObjects(desired.triggers, current.triggers, 'trigger', result, desired, current);
42
+ this.matchObjects(desired.policies, current.policies, 'policy', result, desired, current);
43
+ this.matchObjects(desired.sequences, current.sequences, 'sequence', result, desired, current);
44
+ this.matchObjects(desired.types, current.types, 'type', result, desired, current);
45
+ this.matchObjects(desired.extensions, current.extensions, 'extension', result, desired, current);
46
+ this.matchObjects(desired.indexes, current.indexes, 'index', result, desired, current);
47
+ this.matchObjects(desired.constraints, current.constraints, 'constraint', result, desired, current);
48
+ this.matchObjects(desired.statistics, current.statistics, 'statistics', result, desired, current);
49
+ this.matchObjects(desired.collations, current.collations, 'collation', result, desired, current);
50
+ this.matchObjects(desired.operators, current.operators, 'operator', result, desired, current);
51
+ this.matchObjects(desired.foreignServers, current.foreignServers, 'foreignServer', result, desired, current);
52
+ this.matchObjects(desired.eventTriggers, current.eventTriggers, 'eventTrigger', result, desired, current);
53
+ this.matchObjects(desired.rules, current.rules, 'rule', result, desired, current);
54
+ this.matchObjects(desired.publications, current.publications, 'publication', result, desired, current);
55
+ this.matchObjects(desired.subscriptions, current.subscriptions, 'subscription', result, desired, current);
56
+ this.matchObjects(desired.textSearchConfigs, current.textSearchConfigs, 'textSearchConfig', result, desired, current);
57
+ this.matchObjects(desired.textSearchDictionaries, current.textSearchDictionaries, 'textSearchDict', result, desired, current);
58
+ this.matchObjects(desired.languages, current.languages, 'language', result, desired, current);
59
+ // 11 missing object types
60
+ this.matchObjects(desired.casts, current.casts, 'cast', result, desired, current);
61
+ this.matchObjects(desired.conversions, current.conversions, 'conversion', result, desired, current);
62
+ this.matchObjects(desired.operatorClasses, current.operatorClasses, 'operatorClass', result, desired, current);
63
+ this.matchObjects(desired.operatorFamilies, current.operatorFamilies, 'operatorFamily', result, desired, current);
64
+ this.matchObjects(desired.textSearchParsers, current.textSearchParsers, 'textSearchParser', result, desired, current);
65
+ this.matchObjects(desired.textSearchTemplates, current.textSearchTemplates, 'textSearchTemplate', result, desired, current);
66
+ this.matchObjects(desired.foreignDataWrappers, current.foreignDataWrappers, 'foreignDataWrapper', result, desired, current);
67
+ this.matchObjects(desired.userMappings, current.userMappings, 'userMapping', result, desired, current);
68
+ this.matchObjects(desired.foreignTables, current.foreignTables, 'foreignTable', result, desired, current);
69
+ this.matchObjects(desired.defaultPrivileges, current.defaultPrivileges, 'defaultPrivileges', result, desired, current);
70
+ this.matchObjects(desired.accessMethods, current.accessMethods, 'accessMethod', result, desired, current);
71
+
72
+ // Match columns for matched tables
73
+ const tableMatches = result.matches.filter(m => m.objectType === 'table');
74
+ for (const tableMatch of tableMatches) {
75
+ const tableKey = tableMatch.key;
76
+ const desiredCols = {};
77
+ const currentCols = {};
78
+
79
+ if (Array.isArray(tableMatch.desired.columns)) {
80
+ tableMatch.desired.columns.forEach(c => {
81
+ desiredCols[`${tableKey}.${c.name}`] = { ...c, schema: tableMatch.desired.schema, table: tableKey };
82
+ });
83
+ }
84
+ if (Array.isArray(tableMatch.current.columns)) {
85
+ tableMatch.current.columns.forEach(c => {
86
+ currentCols[`${tableKey}.${c.name}`] = { ...c, schema: tableMatch.current.schema, table: tableKey };
87
+ });
88
+ }
89
+
90
+ this.matchObjects(desiredCols, currentCols, 'column', result, desired, current);
91
+ }
92
+
93
+ // Detect potential renames from unmatched drops and creates
94
+ this.detectRenames(result, desired, current);
95
+
96
+ return result;
97
+ }
98
+
99
+ /**
100
+ * Match objects of a specific type.
101
+ */
102
+ matchObjects(desiredMap, currentMap, objectType, result, desired, current) {
103
+ if (!desiredMap) desiredMap = {};
104
+ if (!currentMap) currentMap = {};
105
+
106
+ const desiredKeys = new Set(Object.keys(desiredMap));
107
+ const currentKeys = new Set(Object.keys(currentMap));
108
+
109
+ // Find matches (same key in both)
110
+ for (const key of desiredKeys) {
111
+ if (currentKeys.has(key)) {
112
+ result.matches.push({
113
+ key,
114
+ objectType,
115
+ desired: desiredMap[key],
116
+ current: currentMap[key],
117
+ });
118
+ } else {
119
+ // Only in desired - potential CREATE or rename target
120
+ result.creates.push({
121
+ key,
122
+ objectType,
123
+ object: desiredMap[key],
124
+ schema: this.extractSchema(desiredMap[key], objectType),
125
+ name: this.extractName(desiredMap[key], objectType, key),
126
+ parent: this.extractParent(desiredMap[key], objectType),
127
+ });
128
+ }
129
+ }
130
+
131
+ // Find drops (in current but not in desired)
132
+ for (const key of currentKeys) {
133
+ if (!desiredKeys.has(key)) {
134
+ result.drops.push({
135
+ key,
136
+ objectType,
137
+ object: currentMap[key],
138
+ schema: this.extractSchema(currentMap[key], objectType),
139
+ name: this.extractName(currentMap[key], objectType, key),
140
+ parent: this.extractParent(currentMap[key], objectType),
141
+ });
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Detect potential renames from unmatched drops and creates.
148
+ */
149
+ detectRenames(result, desired, current) {
150
+ const dropsByType = this.groupBy(result.drops, 'objectType');
151
+ const createsByType = this.groupBy(result.creates, 'objectType');
152
+
153
+ const detectedRenames = [];
154
+
155
+ console.log('\n[RENAME DETECTION] Analyzing potential renames:');
156
+
157
+ for (const [objectType, drops] of Object.entries(dropsByType)) {
158
+ const creates = createsByType[objectType] || [];
159
+
160
+ for (const drop of drops) {
161
+ const candidates = this.findRenameCandidates(drop, creates, desired, current);
162
+
163
+ if (candidates.length === 1) {
164
+ const candidate = candidates[0];
165
+ const rename = this.createRenameChange(drop, candidate, objectType);
166
+
167
+ if (rename.confidence >= LOW_CONFIDENCE_THRESHOLD) {
168
+ detectedRenames.push(rename);
169
+
170
+ console.log(` ✓ Detected: ${drop.name} → ${candidate.name}`);
171
+ console.log(` Type: ${objectType}, Confidence: ${(rename.confidence * 100).toFixed(1)}% (${rename.confidenceLevel})`);
172
+
173
+ result.creates = result.creates.filter(c => c.key !== candidate.key);
174
+ result.drops = result.drops.filter(d => d.key !== drop.key);
175
+ }
176
+ } else if (candidates.length > 1) {
177
+ const sortedCandidates = candidates.map(c => ({
178
+ candidate: c,
179
+ score: this.computeRenameScore(drop, c, desired, current)
180
+ })).sort((a, b) => b.score - a.score);
181
+
182
+ const best = sortedCandidates[0];
183
+ const rename = this.createRenameChange(drop, best.candidate, objectType);
184
+
185
+ const isHighConfidence = best.score >= HIGH_CONFIDENCE_THRESHOLD;
186
+
187
+ if (isHighConfidence) {
188
+ detectedRenames.push(rename);
189
+ console.log(` ✓ Best match: ${drop.name} → ${best.candidate.name} (${(best.score * 100).toFixed(1)}%) among ${candidates.length} candidates`);
190
+
191
+ result.creates = result.creates.filter(c => c.key !== best.candidate.key);
192
+ result.drops = result.drops.filter(d => d.key !== drop.key);
193
+ } else {
194
+ rename.ambiguous = true;
195
+ rename.candidates = candidates;
196
+ rename.warnings = [`Multiple rename candidates detected. Best match: "${best.candidate.name}" (confidence: ${rename.confidence.toFixed(2)})`];
197
+ detectedRenames.push(rename);
198
+
199
+ console.log(` ? Ambiguous: ${drop.name} has ${candidates.length} candidates:`);
200
+ sortedCandidates.slice(0, 3).forEach(c => {
201
+ console.log(` - ${c.candidate.name} (${(c.score * 100).toFixed(1)}%)`);
202
+ });
203
+ }
204
+ } else {
205
+ console.log(` ✗ No match found for: ${drop.name} (${objectType})`);
206
+ }
207
+ }
208
+ }
209
+
210
+ console.log(`[RENAME DETECTION] Total detected: ${detectedRenames.length}\n`);
211
+
212
+ result.renames = detectedRenames;
213
+ }
214
+
215
+ /**
216
+ * Find potential rename candidates for a dropped object.
217
+ */
218
+ findRenameCandidates(drop, creates, desired, current) {
219
+ return creates.filter(create => {
220
+ // Must be same object type
221
+ if (drop.objectType !== create.objectType) return false;
222
+
223
+ // Must be in same schema
224
+ if (drop.schema !== create.schema) return false;
225
+
226
+ // Must have same parent (for child objects like columns, triggers)
227
+ if (drop.parent && create.parent && drop.parent !== create.parent) return false;
228
+
229
+ // Name similarity must be above threshold
230
+ if (!isSimilarEnough(drop.name, create.name, RENAME_SIMILARITY_THRESHOLD)) return false;
231
+
232
+ // Type compatibility check (for columns and types)
233
+ if (!this.typesCompatible(drop, create, desired, current)) return false;
234
+
235
+ return true;
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Create a rename change object.
241
+ */
242
+ createRenameChange(drop, create, objectType) {
243
+ const nameSimilarity = similarity(drop.name, create.name);
244
+ const confidence = this.computeRenameScore(drop, create, null, null);
245
+
246
+ return {
247
+ key: drop.key,
248
+ objectType,
249
+ schema: drop.schema,
250
+ parent: drop.parent,
251
+ oldKey: drop.key,
252
+ newKey: create.key,
253
+ oldName: drop.name,
254
+ newName: create.name,
255
+ changeType: 'RENAME',
256
+ similarity: nameSimilarity,
257
+ confidence,
258
+ confidenceLevel: this.getConfidenceLevel(confidence),
259
+ confirmed: false,
260
+ confidenceDetails: {
261
+ nameSimilarity: nameSimilarity.toFixed(3),
262
+ finalScore: confidence.toFixed(3),
263
+ threshold: RENAME_SIMILARITY_THRESHOLD.toFixed(2),
264
+ highThreshold: HIGH_CONFIDENCE_THRESHOLD.toFixed(2),
265
+ mediumThreshold: MEDIUM_CONFIDENCE_THRESHOLD.toFixed(2),
266
+ },
267
+ fallback: {
268
+ drop: { key: drop.key, objectType, changeType: 'DROP' },
269
+ create: { key: create.key, objectType, changeType: 'CREATE', object: create.object },
270
+ },
271
+ warnings: [],
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Compute rename confidence score.
277
+ */
278
+ computeRenameScore(drop, create, desired, current) {
279
+ let score = similarity(drop.name, create.name);
280
+
281
+ const nameSim = score;
282
+
283
+ if (drop.object && create.object) {
284
+ const dropType = drop.object.dataType || drop.object.type || drop.object.kind;
285
+ const createType = create.object.dataType || create.object.type || create.object.kind;
286
+
287
+ if (dropType && createType) {
288
+ if (typesEqual(dropType, createType)) {
289
+ score += 0.25;
290
+ } else if (sameTypeFamily(dropType, createType)) {
291
+ score += 0.15;
292
+ }
293
+ }
294
+ }
295
+
296
+ if (drop.parent === create.parent && drop.parent) {
297
+ score += 0.15;
298
+ }
299
+
300
+ const minLen = Math.min(drop.name.length, create.name.length);
301
+ if (minLen < 4) {
302
+ score -= 0.05;
303
+ }
304
+
305
+ if (this.isPrefixOrSuffixRename(drop.name, create.name)) {
306
+ score += 0.25;
307
+ }
308
+
309
+ if (this.isSubstringRename(drop.name, create.name)) {
310
+ score += 0.20;
311
+ }
312
+
313
+ if ((drop.objectType === 'table' || drop.objectType === 'column') && drop.object && create.object) {
314
+ const structuralSimilarity = this.computeStructuralSimilarity(drop.object, create.object);
315
+ score += 0.20 * structuralSimilarity;
316
+ }
317
+
318
+ if (this.isCommonRenamePattern(drop.name, create.name)) {
319
+ score += 0.15;
320
+ }
321
+
322
+ if (this.hasCommonWord(drop.name, create.name)) {
323
+ score += 0.10;
324
+ }
325
+
326
+ return Math.min(Math.max(score, 0), 1);
327
+ }
328
+
329
+ isSubstringRename(oldName, newName) {
330
+ const oldLower = oldName.toLowerCase();
331
+ const newLower = newName.toLowerCase();
332
+ if (oldLower.length < 4 || newLower.length < 4) return false;
333
+
334
+ if (oldLower.includes(newLower) || newLower.includes(oldLower)) {
335
+ return true;
336
+ }
337
+
338
+ const oldParts = oldLower.split(/[_\-.]/).filter(p => p.length >= 3);
339
+ const newParts = newLower.split(/[_\-.]/).filter(p => p.length >= 3);
340
+ if (oldParts.length === 0 || newParts.length === 0) return false;
341
+
342
+ for (const op of oldParts) {
343
+ for (const np of newParts) {
344
+ const shorter = op.length < np.length ? op : np;
345
+ const longer = op.length < np.length ? np : op;
346
+ if (longer.includes(shorter) && shorter.length >= 3) {
347
+ return true;
348
+ }
349
+ }
350
+ }
351
+ return false;
352
+ }
353
+
354
+ hasCommonWord(oldName, newName) {
355
+ const oldWords = oldName.toLowerCase().split(/[_\-.]/).filter(w => w.length >= 3);
356
+ const newWords = newName.toLowerCase().split(/[_\-.]/).filter(w => w.length >= 3);
357
+ for (const ow of oldWords) {
358
+ if (newWords.includes(ow)) {
359
+ return true;
360
+ }
361
+ }
362
+ return false;
363
+ }
364
+
365
+ isPrefixOrSuffixRename(oldName, newName) {
366
+ const oldLower = oldName.toLowerCase();
367
+ const newLower = newName.toLowerCase();
368
+
369
+ if (oldLower.startsWith(newLower) || newLower.startsWith(oldLower)) {
370
+ return true;
371
+ }
372
+ if (oldLower.endsWith(newLower) || newLower.endsWith(oldLower)) {
373
+ return true;
374
+ }
375
+
376
+ const oldParts = oldLower.split(/[_\-.]/);
377
+ const newParts = newLower.split(/[_\-.]/);
378
+ if (oldParts.length > 1 && newParts.length > 1) {
379
+ const sharedParts = oldParts.filter(p => newParts.includes(p) && p.length > 2);
380
+ if (sharedParts.length >= Math.min(oldParts.length, newParts.length) - 1) {
381
+ return true;
382
+ }
383
+ }
384
+
385
+ return false;
386
+ }
387
+
388
+ isCommonRenamePattern(oldName, newName) {
389
+ const patterns = [
390
+ [/^(.+)s$/, '$1'], // plural to singular
391
+ [/^(.+?)_(.+)$/, '$2_$1'], // Swap parts
392
+ [/^v_(.+)$/, 'vw_$1'], // View naming
393
+ [/^idx_(.+)$/, 'ix_$1'], // Index naming
394
+ [/^fk_(.+)$/, 'fk_$1_ref'], // FK naming
395
+ [/^get_(.+)$/, 'fetch_$1'], // Function naming
396
+ [/^get_(.+)$/, 'get_$1_data'], // Function with suffix
397
+ [/(.+)s$/, '$1_list'], // plural to _list
398
+ [/(.+)_id$/, '$1_ref'], // _id to _ref
399
+ ];
400
+
401
+ for (const [pattern] of patterns) {
402
+ if (pattern.test(oldName) && pattern.test(newName)) {
403
+ return true;
404
+ }
405
+ }
406
+
407
+ return false;
408
+ }
409
+
410
+ /**
411
+ * Compute structural similarity between two objects.
412
+ */
413
+ computeStructuralSimilarity(objA, objB) {
414
+ // For columns
415
+ if (objA.dataType && objB.dataType) {
416
+ let score = sameTypeFamily(objA.dataType, objB.dataType) ? 1 : 0;
417
+ if (objA.isNullable === objB.isNullable) score += 0.25;
418
+ if (objA.defaultValue === objB.defaultValue) score += 0.25;
419
+ return Math.min(score, 1);
420
+ }
421
+
422
+ // For tables (column overlap)
423
+ if (objA.columns && objB.columns) {
424
+ const colsA = new Set(objA.columns.map(c => this.extractName(c, 'column', '')).filter(Boolean));
425
+ const colsB = new Set(objB.columns.map(c => c.name));
426
+ let shared = 0;
427
+ for (const col of colsB) {
428
+ if (colsA.has(col)) shared++;
429
+ }
430
+ const total = Math.max(colsA.size, colsB.size);
431
+ return total > 0 ? shared / total : 0;
432
+ }
433
+
434
+ return 0;
435
+ }
436
+
437
+ /**
438
+ * Check if types are compatible for rename detection.
439
+ */
440
+ typesCompatible(drop, create, desired, current) {
441
+ // For columns
442
+ if (drop.object?.dataType && create.object?.dataType) {
443
+ return sameTypeFamily(drop.object.dataType, create.object.dataType) ||
444
+ isImplicitCast(drop.object.dataType, create.object.dataType);
445
+ }
446
+
447
+ // For other object types, always consider compatible
448
+ return true;
449
+ }
450
+
451
+ /**
452
+ * Get confidence level label.
453
+ */
454
+ getConfidenceLevel(confidence) {
455
+ if (confidence >= HIGH_CONFIDENCE_THRESHOLD) return 'HIGH';
456
+ if (confidence >= MEDIUM_CONFIDENCE_THRESHOLD) return 'MEDIUM';
457
+ return 'LOW';
458
+ }
459
+
460
+ /**
461
+ * Extract schema from object.
462
+ */
463
+ extractSchema(obj, objectType) {
464
+ if (!obj) return null;
465
+ return obj.schema || null;
466
+ }
467
+
468
+ /**
469
+ * Extract name from object.
470
+ */
471
+ extractName(obj, objectType, key) {
472
+ if (!obj) {
473
+ // Extract from key
474
+ const parts = key.split('.');
475
+ return parts[parts.length - 1];
476
+ }
477
+ return obj.name || obj.relname || obj.proname || obj.typname || key.split('.').pop();
478
+ }
479
+
480
+ /**
481
+ * Extract parent path from object.
482
+ */
483
+ extractParent(obj, objectType) {
484
+ if (!obj) return null;
485
+
486
+ // For columns
487
+ if (obj.table) return obj.table;
488
+
489
+ // For triggers, policies, rules
490
+ if (obj.table) return obj.table;
491
+
492
+ // For indexes
493
+ if (obj.table) return obj.table;
494
+
495
+ return null;
496
+ }
497
+
498
+ /**
499
+ * Group array by property.
500
+ */
501
+ groupBy(array, prop) {
502
+ const result = {};
503
+ for (const item of array) {
504
+ const key = item[prop];
505
+ if (!result[key]) result[key] = [];
506
+ result[key].push(item);
507
+ }
508
+ return result;
509
+ }
510
+ }