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,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
|
+
}
|