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,309 @@
1
+ import crypto from 'crypto';
2
+ import { ObjectMatcher } from './object-matcher.js';
3
+ import { PropertyDiffer } from './property-differ.js';
4
+ import { DependencyResolver } from './dependency-resolver.js';
5
+ import { ChangeClassifier } from './change-classifier.js';
6
+ import { RiskTagger } from './risk-tagger.js';
7
+
8
+ /**
9
+ * Schema Differ - Main orchestrator for comparing two schema snapshots.
10
+ * Produces a complete SchemaDiff with all changes, properly ordered and classified.
11
+ */
12
+
13
+ export class SchemaDiffer {
14
+ constructor(options = {}) {
15
+ this.pgVersion = options.pgVersion || 150000;
16
+ this.objectMatcher = new ObjectMatcher();
17
+ this.propertyDiffer = new PropertyDiffer();
18
+ this.dependencyResolver = new DependencyResolver();
19
+ this.changeClassifier = new ChangeClassifier();
20
+ this.riskTagger = new RiskTagger(this.pgVersion);
21
+ }
22
+
23
+ /**
24
+ * Diff two schema snapshots (desired vs current).
25
+ * @param {Object} desired - The desired/target schema
26
+ * @param {Object} current - The current/live schema
27
+ * @returns {Object} SchemaDiff
28
+ */
29
+ diff(desired, current) {
30
+ const startTime = Date.now();
31
+ const allChanges = [];
32
+
33
+ // Step 1: Match objects between snapshots
34
+ const matched = this.objectMatcher.match(desired, current);
35
+
36
+ // Step 2: Handle creates (objects only in desired)
37
+ for (const create of matched.creates) {
38
+ allChanges.push(this.createChangeObject('CREATE', create.objectType, create.key, null, create.object, create));
39
+ }
40
+
41
+ // Step 3: Handle drops (objects only in current)
42
+ for (const drop of matched.drops) {
43
+ allChanges.push(this.createChangeObject('DROP', drop.objectType, drop.key, drop.object, null, drop));
44
+ }
45
+
46
+ // Step 4: Handle detected renames
47
+ for (const rename of matched.renames) {
48
+ allChanges.push(this.createChangeObject('RENAME', rename.objectType, rename.key, rename, rename, rename, rename));
49
+ }
50
+
51
+ // Step 5: Property-level diff for matched objects
52
+ const propertyResults = this.propertyDiffer.diff(matched.matches, desired, current);
53
+ allChanges.push(...propertyResults.changes.map(c => {
54
+ let pluralKey = c.objectType + 's';
55
+ if (c.objectType === 'index') pluralKey = 'indexes';
56
+ else if (c.objectType === 'materializedView') pluralKey = 'materializedViews';
57
+ else if (c.objectType === 'operatorClass') pluralKey = 'operatorClasses';
58
+ else if (c.objectType === 'operatorFamily') pluralKey = 'operatorFamilies';
59
+ else if (c.objectType === 'defaultPrivileges') pluralKey = 'defaultPrivileges';
60
+ else if (c.objectType === 'foreignTable') pluralKey = 'tables';
61
+ else if (c.objectType === 'function' && desired.procedures?.[c.path]) pluralKey = 'procedures';
62
+ else if (c.objectType === 'function' && desired.aggregates?.[c.path]) pluralKey = 'aggregates';
63
+ else if (c.objectType === 'textSearchConfig') pluralKey = 'textSearchConfigs';
64
+ else if (c.objectType === 'textSearchDict') pluralKey = 'textSearchDictionaries';
65
+ else if (c.objectType === 'textSearchParser') pluralKey = 'textSearchParsers';
66
+ else if (c.objectType === 'textSearchTemplate') pluralKey = 'textSearchTemplates';
67
+
68
+ const beforeObj = current[pluralKey]?.[c.path] || null;
69
+ const afterObj = desired[pluralKey]?.[c.path] || null;
70
+
71
+ const changeObj = this.createChangeObject(
72
+ 'ALTER',
73
+ c.objectType,
74
+ c.path,
75
+ beforeObj,
76
+ afterObj,
77
+ c,
78
+ c.property
79
+ );
80
+ changeObj.currentValue = c.currentValue;
81
+ changeObj.desiredValue = c.desiredValue;
82
+ return changeObj;
83
+ }));
84
+
85
+ // Step 6: Process warnings from property differ
86
+
87
+ // Step 7: Classify changes (track 1 vs track 2)
88
+ this.changeClassifier.classify(allChanges);
89
+
90
+ // Step 8: Resolve dependency order
91
+ const orderedChanges = this.dependencyResolver.resolve(allChanges, desired, current);
92
+
93
+ // Step 9: Tag risks
94
+ this.riskTagger.tag(orderedChanges);
95
+
96
+ // Step 10: Build output
97
+ return {
98
+ summary: this.buildSummary(orderedChanges),
99
+ changes: orderedChanges,
100
+ warnings: [...matched.renames.filter(r => !r.confirmed).map(r => ({
101
+ code: 'RENAME_UNCONFIRMED',
102
+ message: `Possible rename detected: ${r.oldName} → ${r.newName}`,
103
+ changeKey: r.key,
104
+ action: 'Confirm rename or treat as drop+add',
105
+ })), ...propertyResults.warnings],
106
+ dependencyGraph: this.dependencyResolver.getGraph(),
107
+ metadata: {
108
+ diffDuration: Date.now() - startTime,
109
+ pgVersion: this.pgVersion,
110
+ desiredChecksum: desired.checksum,
111
+ currentChecksum: current.checksum,
112
+ },
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Create a standardized change object.
118
+ */
119
+ createChangeObject(changeType, objectType, objectKey, before, after, extra = {}, property = null) {
120
+ const id = `change_${crypto.randomUUID().slice(0, 8)}`;
121
+
122
+ return {
123
+ id,
124
+ changeType,
125
+ objectType,
126
+ objectKey,
127
+ schema: extra?.schema,
128
+ name: extra?.name || objectKey?.split('.').pop(),
129
+ property,
130
+ before: before || null,
131
+ after: after || null,
132
+ track: extra?.track || 1,
133
+ phase: extra?.phase || 10,
134
+ ddlStrategy: extra?.ddlStrategy || 'ALTER',
135
+ dependencies: extra?.dependencies || [],
136
+ dependents: [],
137
+ risk: extra?.risk || { level: 'none', categories: [], warnings: [] },
138
+ requiresRecreation: extra?.requiresRecreation || false,
139
+ safePatternAvailable: extra?.safePatternAvailable || false,
140
+ isNonTransactional: extra?.isTransactional === false,
141
+ pgVersionMinimum: extra?.pgVersionMinimum,
142
+ dataLossRisk: extra?.dataLossRisk,
143
+ ...extra,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Build summary statistics.
149
+ */
150
+ buildSummary(changes) {
151
+ const summary = {
152
+ totalChanges: changes.length,
153
+ creates: 0,
154
+ drops: 0,
155
+ alters: 0,
156
+ renames: 0,
157
+ recreates: 0,
158
+ replaces: 0,
159
+
160
+ byTrack: {
161
+ track1: { count: 0, phases: {} },
162
+ track2: { count: 0, phases: {} },
163
+ },
164
+
165
+ byPhase: {},
166
+
167
+ byObjectType: {},
168
+
169
+ riskSummary: {
170
+ critical: 0,
171
+ high: 0,
172
+ medium: 0,
173
+ low: 0,
174
+ none: 0,
175
+ categories: {},
176
+ },
177
+
178
+ requiresDowntime: false,
179
+ estimatedDuration: this.estimateDuration(changes),
180
+ };
181
+
182
+ for (const change of changes) {
183
+ // Count by change type
184
+ if (change.changeType === 'CREATE') summary.creates++;
185
+ else if (change.changeType === 'DROP') summary.drops++;
186
+ else if (change.changeType === 'ALTER') summary.alters++;
187
+ else if (change.changeType === 'RENAME') summary.renames++;
188
+ else if (change.changeType?.includes('RECREATE')) summary.recreates++;
189
+ else if (change.changeType?.includes('REPLACE')) summary.replaces++;
190
+
191
+ // Count by track
192
+ const track = change.track === 2 ? 'track2' : 'track1';
193
+ summary.byTrack[track].count++;
194
+ const phase = change.phase;
195
+ if (!summary.byTrack[track].phases[phase]) {
196
+ summary.byTrack[track].phases[phase] = 0;
197
+ }
198
+ summary.byTrack[track].phases[phase]++;
199
+
200
+ // Count by phase
201
+ if (!summary.byPhase[phase]) summary.byPhase[phase] = { count: 0, name: this.getPhaseName(phase) };
202
+ summary.byPhase[phase].count++;
203
+
204
+ // Count by object type
205
+ const objType = change.objectType;
206
+ if (!summary.byObjectType[objType]) summary.byObjectType[objType] = 0;
207
+ summary.byObjectType[objType]++;
208
+
209
+ // Risk counts
210
+ const level = change.risk?.level || 'none';
211
+ summary.riskSummary[level]++;
212
+
213
+ for (const cat of (change.risk?.categories || [])) {
214
+ if (!summary.riskSummary.categories[cat]) {
215
+ summary.riskSummary.categories[cat] = 0;
216
+ }
217
+ summary.riskSummary.categories[cat]++;
218
+ }
219
+
220
+ // Check for downtime
221
+ if (change.risk?.requiresDowntime) {
222
+ summary.requiresDowntime = true;
223
+ }
224
+ }
225
+
226
+ return summary;
227
+ }
228
+
229
+ /**
230
+ * Get human-readable phase name.
231
+ */
232
+ getPhaseName(phase) {
233
+ const phases = {
234
+ 1: 'pre_check',
235
+ 2: 'advisory_lock',
236
+ 3: 'extensions',
237
+ 4: 'types',
238
+ 5: 'schemas',
239
+ 6: 'tables_create',
240
+ 7: 'columns_add',
241
+ 8: 'sequences',
242
+ 9: 'indexes_create',
243
+ 10: 'constraints_non_fk',
244
+ 11: 'data_migration',
245
+ 12: 'constraints_fk',
246
+ 13: 'validate_constraints',
247
+ 14: 'views',
248
+ 15: 'materialized_views',
249
+ 16: 'functions',
250
+ 17: 'triggers',
251
+ 18: 'policies',
252
+ 19: 'rules',
253
+ 20: 'behavioral_other',
254
+ 21: 'grants',
255
+ 22: 'comments',
256
+ 23: 'indexes_concurrent',
257
+ 24: 'cleanup',
258
+ 25: 'post_check',
259
+ };
260
+ return phases[phase] || `phase_${phase}`;
261
+ }
262
+
263
+ /**
264
+ * Estimate migration duration.
265
+ */
266
+ estimateDuration(changes) {
267
+ let seconds = 0;
268
+
269
+ for (const change of changes) {
270
+ switch (change.objectType) {
271
+ case 'index':
272
+ seconds += change.isConcurrent ? 30 : 2;
273
+ break;
274
+ case 'constraint':
275
+ seconds += change.constraintType === 'FOREIGN_KEY' ? 5 : 2;
276
+ break;
277
+ case 'table':
278
+ seconds += change.changeType === 'CREATE' ? 1 : 2;
279
+ break;
280
+ case 'column':
281
+ seconds += change.property === 'dataType' ? 10 : 1;
282
+ break;
283
+ case 'type':
284
+ seconds += 2;
285
+ break;
286
+ case 'view':
287
+ case 'materializedView':
288
+ seconds += 3;
289
+ break;
290
+ case 'function':
291
+ seconds += 2;
292
+ break;
293
+ default:
294
+ seconds += 1;
295
+ }
296
+ }
297
+
298
+ if (seconds < 60) return `${seconds} seconds`;
299
+ if (seconds < 3600) return `${Math.ceil(seconds / 60)} minutes`;
300
+ return `${Math.ceil(seconds / 3600)} hours`;
301
+ }
302
+ }
303
+
304
+ // Export all sub-modules
305
+ export { ObjectMatcher } from './object-matcher.js';
306
+ export { PropertyDiffer } from './property-differ.js';
307
+ export { DependencyResolver } from './dependency-resolver.js';
308
+ export { ChangeClassifier } from './change-classifier.js';
309
+ export { RiskTagger } from './risk-tagger.js';
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Levenshtein distance implementation for rename detection.
3
+ * Pure JS, zero dependencies.
4
+ */
5
+
6
+ /**
7
+ * Calculate the Levenshtein distance between two strings.
8
+ * @param {string} a - First string
9
+ * @param {string} b - Second string
10
+ * @returns {number} - Minimum number of edits to transform a into b
11
+ */
12
+ export function levenshtein(a, b) {
13
+ if (!a) return b ? b.length : 0;
14
+ if (!b) return a.length;
15
+
16
+ const aLen = a.length;
17
+ const bLen = b.length;
18
+
19
+ // Use the shorter string for the outer loop to minimize memory
20
+ if (aLen > bLen) {
21
+ return levenshtein(b, a);
22
+ }
23
+
24
+ // Initialize row
25
+ let prevRow = Array.from({ length: aLen + 1 }, (_, i) => i);
26
+
27
+ for (let j = 1; j <= bLen; j++) {
28
+ const currRow = [j];
29
+ const bChar = b.charCodeAt(j - 1);
30
+
31
+ for (let i = 1; i <= aLen; i++) {
32
+ const aChar = a.charCodeAt(i - 1);
33
+ const cost = aChar === bChar ? 0 : 1;
34
+
35
+ currRow[i] = Math.min(
36
+ prevRow[i] + 1, // deletion
37
+ currRow[i - 1] + 1, // insertion
38
+ prevRow[i - 1] + cost // substitution
39
+ );
40
+ }
41
+
42
+ prevRow = currRow;
43
+ }
44
+
45
+ return prevRow[aLen];
46
+ }
47
+
48
+ /**
49
+ * Calculate similarity score between two strings (0-1).
50
+ * Higher values mean more similar.
51
+ * @param {string} a - First string
52
+ * @param {string} b - Second string
53
+ * @returns {number} - Similarity score between 0 and 1
54
+ */
55
+ export function similarity(a, b) {
56
+ if (!a && !b) return 1;
57
+ if (!a || !b) return 0;
58
+
59
+ const distance = levenshtein(a.toLowerCase(), b.toLowerCase());
60
+ const maxLen = Math.max(a.length, b.length);
61
+
62
+ return maxLen === 0 ? 1 : 1 - (distance / maxLen);
63
+ }
64
+
65
+ /**
66
+ * Check if two strings are similar enough to be rename candidates.
67
+ * @param {string} a - First string
68
+ * @param {string} b - Second string
69
+ * @param {number} threshold - Minimum similarity threshold (default 0.4)
70
+ * @returns {boolean}
71
+ */
72
+ export function isSimilarEnough(a, b, threshold = 0.4) {
73
+ return similarity(a, b) >= threshold;
74
+ }
75
+
76
+ /**
77
+ * Calculate Damerau-Levenshtein distance (includes transpositions).
78
+ * More accurate for detecting typos like "teh" vs "the".
79
+ * @param {string} a - First string
80
+ * @param {string} b - Second string
81
+ * @returns {number}
82
+ */
83
+ export function damerauLevenshtein(a, b) {
84
+ if (!a) return b ? b.length : 0;
85
+ if (!b) return a.length;
86
+
87
+ const aLen = a.length;
88
+ const bLen = b.length;
89
+
90
+ const matrix = Array.from({ length: aLen + 1 }, () =>
91
+ Array.from({ length: bLen + 1 }, (_, j) => j)
92
+ );
93
+
94
+ for (let i = 0; i <= aLen; i++) {
95
+ matrix[i][0] = i;
96
+ }
97
+
98
+ for (let j = 0; j <= bLen; j++) {
99
+ matrix[0][j] = j;
100
+ }
101
+
102
+ for (let i = 1; i <= aLen; i++) {
103
+ for (let j = 1; j <= bLen; j++) {
104
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
105
+
106
+ matrix[i][j] = Math.min(
107
+ matrix[i - 1][j] + 1, // deletion
108
+ matrix[i][j - 1] + 1, // insertion
109
+ matrix[i - 1][j - 1] + cost // substitution
110
+ );
111
+
112
+ // Transposition
113
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
114
+ matrix[i][j] = Math.min(matrix[i][j], matrix[i - 2][j - 2] + cost);
115
+ }
116
+ }
117
+ }
118
+
119
+ return matrix[aLen][bLen];
120
+ }
121
+
122
+ /**
123
+ * Find the best matching string from a list.
124
+ * @param {string} target - String to match
125
+ * @param {string[]} candidates - List of candidate strings
126
+ * @param {number} threshold - Minimum similarity (default 0.4)
127
+ * @returns {{match: string|null, similarity: number}}
128
+ */
129
+ export function findBestMatch(target, candidates, threshold = 0.4) {
130
+ let bestMatch = null;
131
+ let bestScore = threshold - 1; // Start below threshold
132
+
133
+ for (const candidate of candidates) {
134
+ const score = similarity(target, candidate);
135
+ if (score > bestScore) {
136
+ bestScore = score;
137
+ bestMatch = candidate;
138
+ }
139
+ }
140
+
141
+ return { match: bestMatch, similarity: bestMatch };
142
+ }
143
+
144
+ /**
145
+ * Calculate Jaro-Winkler similarity (better for name matching).
146
+ * @param {string} a - First string
147
+ * @param {string} b - Second string
148
+ * @returns {number} - Similarity score between 0 and 1
149
+ */
150
+ export function jaroWinkler(a, b) {
151
+ if (!a && !b) return 1;
152
+ if (!a || !b) return 0;
153
+ if (a === b) return 1;
154
+
155
+ const aLower = a.toLowerCase();
156
+ const bLower = b.toLowerCase();
157
+
158
+ const aLen = aLower.length;
159
+ const bLen = bLower.length;
160
+
161
+ const matchDistance = Math.floor(Math.max(aLen, bLen) / 2) - 1;
162
+ if (matchDistance < 0) return 0;
163
+
164
+ const aMatches = new Array(aLen).fill(false);
165
+ const bMatches = new Array(bLen).fill(false);
166
+
167
+ let matches = 0;
168
+ let transpositions = 0;
169
+
170
+ // Find matches
171
+ for (let i = 0; i < aLen; i++) {
172
+ const start = Math.max(0, i - matchDistance);
173
+ const end = Math.min(i + matchDistance + 1, bLen);
174
+
175
+ for (let j = start; j < end; j++) {
176
+ if (bMatches[j] || aLower[i] !== bLower[j]) continue;
177
+ aMatches[i] = true;
178
+ bMatches[j] = true;
179
+ matches++;
180
+ break;
181
+ }
182
+ }
183
+
184
+ if (matches === 0) return 0;
185
+
186
+ // Count transpositions
187
+ let k = 0;
188
+ for (let i = 0; i < aLen; i++) {
189
+ if (!aMatches[i]) continue;
190
+ while (!bMatches[k]) k++;
191
+ if (aLower[i] !== bLower[k]) transpositions++;
192
+ k++;
193
+ }
194
+
195
+ // Jaro similarity
196
+ const jaro = (
197
+ matches / aLen +
198
+ matches / bLen +
199
+ (matches - transpositions / 2) / matches
200
+ ) / 3;
201
+
202
+ // Winkler modification (boost for common prefix)
203
+ let prefix = 0;
204
+ const maxPrefix = Math.min(4, aLen, bLen);
205
+ for (let i = 0; i < maxPrefix && aLower[i] === bLower[i]; i++) {
206
+ prefix++;
207
+ }
208
+
209
+ return jaro + prefix * 0.1 * (1 - jaro);
210
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Utility for building dotted paths like "public.users.email"
3
+ */
4
+
5
+ /**
6
+ * Build a path for an object.
7
+ * @param {Object} options
8
+ * @param {string} [options.schema]
9
+ * @param {string} [options.table]
10
+ * @param {string} [options.name]
11
+ * @param {string} [options.column]
12
+ * @param {string} [options.objectType]
13
+ * @returns {string}
14
+ */
15
+ export function buildPath(options) {
16
+ const parts = [];
17
+
18
+ if (options.schema) {
19
+ parts.push(options.schema);
20
+ }
21
+
22
+ if (options.table) {
23
+ parts.push(options.table);
24
+ } else if (options.parent) {
25
+ parts.push(options.parent);
26
+ }
27
+
28
+ if (options.column) {
29
+ parts.push(options.column);
30
+ } else if (options.name) {
31
+ parts.push(options.name);
32
+ }
33
+
34
+ return parts.join('.');
35
+ }
36
+
37
+ /**
38
+ * Build a path for a table.
39
+ * @param {string} schema
40
+ * @param {string} tableName
41
+ * @returns {string}
42
+ */
43
+ export function tablePath(schema, tableName) {
44
+ return `${schema}.${tableName}`;
45
+ }
46
+
47
+ /**
48
+ * Build a path for a column.
49
+ * @param {string} schema
50
+ * @param {string} tableName
51
+ * @param {string} columnName
52
+ * @returns {string}
53
+ */
54
+ export function columnPath(schema, tableName, columnName) {
55
+ return `${schema}.${tableName}.${columnName}`;
56
+ }
57
+
58
+ /**
59
+ * Build a path for an index.
60
+ * @param {string} schema
61
+ * @param {string} indexName
62
+ * @returns {string}
63
+ */
64
+ export function indexPath(schema, indexName) {
65
+ return `${schema}.${indexName}`;
66
+ }
67
+
68
+ /**
69
+ * Build a path for a constraint.
70
+ * @param {string} schema
71
+ * @param {string} constraintName
72
+ * @returns {string}
73
+ */
74
+ export function constraintPath(schema, constraintName) {
75
+ return `${schema}.${constraintName}`;
76
+ }
77
+
78
+ /**
79
+ * Build a path for a function.
80
+ * @param {string} schema
81
+ * @param {string} functionName
82
+ * @param {string} [args]
83
+ * @returns {string}
84
+ */
85
+ export function functionPath(schema, functionName, args) {
86
+ if (args) {
87
+ return `${schema}.${functionName}(${args})`;
88
+ }
89
+ return `${schema}.${functionName}`;
90
+ }
91
+
92
+ /**
93
+ * Build a path for a trigger.
94
+ * @param {string} schema
95
+ * @param {string} tableName
96
+ * @param {string} triggerName
97
+ * @returns {string}
98
+ */
99
+ export function triggerPath(schema, tableName, triggerName) {
100
+ return `${schema}.${tableName}.${triggerName}`;
101
+ }
102
+
103
+ /**
104
+ * Build a path for a policy.
105
+ * @param {string} schema
106
+ * @param {string} tableName
107
+ * @param {string} policyName
108
+ * @returns {string}
109
+ */
110
+ export function policyPath(schema, tableName, policyName) {
111
+ return `${schema}.${tableName}.${policyName}`;
112
+ }
113
+
114
+ /**
115
+ * Build a path for a view.
116
+ * @param {string} schema
117
+ * @param {string} viewName
118
+ * @returns {string}
119
+ */
120
+ export function viewPath(schema, viewName) {
121
+ return `${schema}.${viewName}`;
122
+ }
123
+
124
+ /**
125
+ * Build a path for a type.
126
+ * @param {string} schema
127
+ * @param {string} typeName
128
+ * @returns {string}
129
+ */
130
+ export function typePath(schema, typeName) {
131
+ return `${schema}.${typeName}`;
132
+ }
133
+
134
+ /**
135
+ * Build a path for a sequence.
136
+ * @param {string} schema
137
+ * @param {string} sequenceName
138
+ * @returns {string}
139
+ */
140
+ export function sequencePath(schema, sequenceName) {
141
+ return `${schema}.${sequenceName}`;
142
+ }
143
+
144
+ /**
145
+ * Parse a path into its components.
146
+ * @param {string} path
147
+ * @returns {{schema: string, name: string, parent: string|null}}
148
+ */
149
+ export function parsePath(path) {
150
+ const parts = path.split('.');
151
+
152
+ if (parts.length === 1) {
153
+ return { schema: null, name: parts[0], parent: null };
154
+ }
155
+
156
+ if (parts.length === 2) {
157
+ return { schema: parts[0], name: parts[1], parent: null };
158
+ }
159
+
160
+ if (parts.length >= 3) {
161
+ return { schema: parts[0], parent: `${parts[0]}.${parts[1]}`, name: parts[parts.length - 1] };
162
+ }
163
+
164
+ return { schema: null, name: path, parent: null };
165
+ }
166
+
167
+ /**
168
+ * Get the parent path (table for column, schema for table, etc.)
169
+ * @param {string} path
170
+ * @returns {string|null}
171
+ */
172
+ export function getParentPath(path) {
173
+ const parts = path.split('.');
174
+
175
+ if (parts.length <= 1) return null;
176
+
177
+ return parts.slice(0, -1).join('.');
178
+ }
179
+
180
+ /**
181
+ * Get the leaf name from a path.
182
+ * @param {string} path
183
+ * @returns {string}
184
+ */
185
+ export function getLeafName(path) {
186
+ const parts = path.split('.');
187
+ return parts[parts.length - 1];
188
+ }
189
+
190
+ /**
191
+ * Get the schema from a path.
192
+ * @param {string} path
193
+ * @returns {string|null}
194
+ */
195
+ export function getSchema(path) {
196
+ const parts = path.split('.');
197
+ return parts.length >= 1 ? parts[0] : null;
198
+ }