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,472 @@
1
+ /**
2
+ * PostgreSQL type compatibility and cast matrix.
3
+ * Determines if type changes are safe, need USING clause, or are impossible.
4
+ */
5
+
6
+ /**
7
+ * Normalize type names for comparison.
8
+ * @param {string} type - PostgreSQL type name
9
+ * @returns {string}
10
+ */
11
+ export function normalizeType(type) {
12
+ if (!type) return 'unknown';
13
+
14
+ const lower = type.toLowerCase().trim();
15
+
16
+ // Handle array types
17
+ const isArray = lower.endsWith('[]');
18
+ const baseType = isArray ? lower.slice(0, -2) : lower;
19
+
20
+ // Normalize common aliases
21
+ const aliases = {
22
+ 'int': 'integer',
23
+ 'int4': 'integer',
24
+ 'int2': 'smallint',
25
+ 'int8': 'bigint',
26
+ 'serial': 'integer',
27
+ 'bigserial': 'bigint',
28
+ 'smallserial': 'smallint',
29
+ 'bool': 'boolean',
30
+ 'float4': 'real',
31
+ 'float8': 'double precision',
32
+ 'double': 'double precision',
33
+ 'char': 'character',
34
+ 'varchar': 'character varying',
35
+ 'timestamp': 'timestamp without time zone',
36
+ 'timestamptz': 'timestamp with time zone',
37
+ 'time': 'time without time zone',
38
+ 'timetz': 'time with time zone',
39
+ };
40
+
41
+ const normalized = aliases[baseType] || baseType;
42
+
43
+ return isArray ? `${normalized}[]` : normalized;
44
+ }
45
+
46
+ /**
47
+ * Extract type family and parameters.
48
+ * @param {string} type
49
+ * @returns {{family: string, params: Object|null}}
50
+ */
51
+ export function parseTypeParams(type) {
52
+ const normalized = normalizeType(type);
53
+
54
+ // Check for parameterized types: varchar(n), numeric(p,s), etc.
55
+ const match = normalized.match(/^([a-z_]+)\s*\(([^)]+)\)$/);
56
+
57
+ if (!match) {
58
+ return { family: normalized, params: null };
59
+ }
60
+
61
+ const family = match[1];
62
+ const paramString = match[2];
63
+
64
+ if (family === 'character varying' || family === 'varchar' || family === 'character') {
65
+ return { family, params: { length: parseInt(paramString, 10) } };
66
+ }
67
+
68
+ if (family === 'numeric' || family === 'decimal') {
69
+ const [precision, scale] = paramString.split(',').map(s => parseInt(s.trim(), 10));
70
+ return { family, params: { precision, scale } };
71
+ }
72
+
73
+ if (family === 'bit' || family === 'bit varying' || family === 'varbit') {
74
+ return { family, params: { length: parseInt(paramString, 10) } };
75
+ }
76
+
77
+ return { family: normalized, params: null };
78
+ }
79
+
80
+ /**
81
+ * PostgreSQL cast matrix.
82
+ * Format: sourceType → { targetType: { implicit: bool, safe: bool, using: string|null, dataLossRisk: string|null } }
83
+ */
84
+ const CAST_MATRIX = {
85
+ // Smallint casts
86
+ 'smallint': {
87
+ 'integer': { implicit: true, safe: true },
88
+ 'bigint': { implicit: true, safe: true },
89
+ 'real': { implicit: true, safe: true },
90
+ 'double precision': { implicit: true, safe: true },
91
+ 'numeric': { implicit: true, safe: true },
92
+ 'decimal': { implicit: true, safe: true },
93
+ 'character varying': { implicit: false, safe: true, using: '::character varying' },
94
+ 'text': { implicit: false, safe: true, using: '::text' },
95
+ },
96
+
97
+ // Integer casts
98
+ 'integer': {
99
+ 'smallint': { implicit: false, safe: false, dataLossRisk: 'overflow', minCheck: 'MIN(smallint)' },
100
+ 'bigint': { implicit: true, safe: true },
101
+ 'real': { implicit: true, safe: true },
102
+ 'double precision': { implicit: true, safe: true },
103
+ 'numeric': { implicit: true, safe: true },
104
+ 'character varying': { implicit: false, safe: true, using: '::character varying' },
105
+ 'text': { implicit: false, safe: true, using: '::text' },
106
+ },
107
+
108
+ // Bigint casts
109
+ 'bigint': {
110
+ 'smallint': { implicit: false, safe: false, dataLossRisk: 'overflow' },
111
+ 'integer': { implicit: false, safe: false, dataLossRisk: 'overflow' },
112
+ 'real': { implicit: true, safe: true },
113
+ 'double precision': { implicit: true, safe: true },
114
+ 'numeric': { implicit: true, safe: true },
115
+ 'character varying': { implicit: false, safe: true, using: '::character varying' },
116
+ 'text': { implicit: false, safe: true, using: '::text' },
117
+ },
118
+
119
+ // Real/float casts
120
+ 'real': {
121
+ 'double precision': { implicit: true, safe: true },
122
+ 'numeric': { implicit: true, safe: true },
123
+ 'integer': { implicit: false, safe: false, dataLossRisk: 'truncation' },
124
+ 'bigint': { implicit: false, safe: false, dataLossRisk: 'truncation' },
125
+ 'character varying': { implicit: false, safe: true, using: '::character varying' },
126
+ 'text': { implicit: false, safe: true, using: '::text' },
127
+ },
128
+
129
+ // Double precision casts
130
+ 'double precision': {
131
+ 'real': { implicit: true, safe: true },
132
+ 'numeric': { implicit: true, safe: true },
133
+ 'integer': { implicit: false, safe: false, dataLossRisk: 'truncation' },
134
+ 'bigint': { implicit: false, safe: false, dataLossRisk: 'truncation' },
135
+ 'character varying': { implicit: false, safe: true, using: '::character varying' },
136
+ 'text': { implicit: false, safe: true, using: '::text' },
137
+ },
138
+
139
+ // Numeric/decimal casts
140
+ 'numeric': {
141
+ 'integer': { implicit: false, safe: false, dataLossRisk: 'truncation' },
142
+ 'bigint': { implicit: false, safe: false, dataLossRisk: 'truncation' },
143
+ 'real': { implicit: true, safe: true },
144
+ 'double precision': { implicit: true, safe: true },
145
+ 'character varying': { implicit: false, safe: true, using: '::character varying' },
146
+ 'text': { implicit: false, safe: true, using: '::text' },
147
+ },
148
+
149
+ // Character varying/text casts
150
+ 'character varying': {
151
+ 'text': { implicit: true, safe: true },
152
+ 'character': { implicit: false, safe: false, dataLossRisk: 'truncation' },
153
+ 'integer': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
154
+ 'bigint': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
155
+ 'numeric': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
156
+ 'boolean': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
157
+ 'date': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
158
+ 'timestamp': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
159
+ 'timestamp with time zone': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
160
+ 'uuid': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
161
+ 'json': { implicit: false, safe: true, using: '::json' },
162
+ 'jsonb': { implicit: false, safe: true, using: '::jsonb' },
163
+ },
164
+
165
+ 'text': {
166
+ 'character varying': { implicit: true, safe: true },
167
+ 'character': { implicit: false, safe: false, dataLossRisk: 'truncation' },
168
+ 'integer': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
169
+ 'bigint': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
170
+ 'numeric': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
171
+ 'boolean': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
172
+ 'date': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
173
+ 'uuid': { implicit: false, safe: false, dataLossRisk: 'parse_error' },
174
+ 'json': { implicit: false, safe: true, using: '::json' },
175
+ 'jsonb': { implicit: false, safe: true, using: '::jsonb' },
176
+ },
177
+
178
+ // Boolean casts
179
+ 'boolean': {
180
+ 'integer': { implicit: false, safe: true, using: "CASE WHEN true THEN 1 ELSE 0 END" },
181
+ 'character varying': { implicit: false, safe: true, using: '::character varying' },
182
+ 'text': { implicit: false, safe: true, using: '::text' },
183
+ },
184
+
185
+ // Date/time casts
186
+ 'date': {
187
+ 'timestamp': { implicit: true, safe: true },
188
+ 'timestamp with time zone': { implicit: true, safe: true },
189
+ 'character varying': { implicit: false, safe: true, using: '::character varying' },
190
+ 'text': { implicit: false, safe: true, using: '::text' },
191
+ },
192
+
193
+ 'timestamp': {
194
+ 'timestamp with time zone': { implicit: true, safe: true },
195
+ 'date': { implicit: true, safe: true },
196
+ 'time': { implicit: false, safe: true, using: '::time' },
197
+ 'character varying': { implicit: false, safe: true, using: '::character varying' },
198
+ 'text': { implicit: false, safe: true, using: '::text' },
199
+ },
200
+
201
+ 'timestamp with time zone': {
202
+ 'timestamp': { implicit: true, safe: true },
203
+ 'date': { implicit: true, safe: true },
204
+ 'character varying': { implicit: false, safe: true, using: '::character varying' },
205
+ 'text': { implicit: false, safe: true, using: '::text' },
206
+ },
207
+
208
+ // JSON casts
209
+ 'json': {
210
+ 'jsonb': { implicit: true, safe: true },
211
+ 'text': { implicit: true, safe: true },
212
+ 'character varying': { implicit: true, safe: true },
213
+ },
214
+
215
+ 'jsonb': {
216
+ 'json': { implicit: true, safe: true },
217
+ 'text': { implicit: true, safe: true },
218
+ 'character varying': { implicit: true, safe: true },
219
+ },
220
+
221
+ // UUID casts
222
+ 'uuid': {
223
+ 'text': { implicit: false, safe: true, using: '::text' },
224
+ 'character varying': { implicit: false, safe: true, using: '::character varying' },
225
+ },
226
+ };
227
+
228
+ /**
229
+ * Check if a cast exists between two types.
230
+ * @param {string} fromType
231
+ * @param {string} toType
232
+ * @returns {{exists: boolean, implicit: boolean, safe: boolean, using: string|null, dataLossRisk: string|null}}
233
+ */
234
+ export function getCastInfo(fromType, toType) {
235
+ const from = normalizeType(fromType);
236
+ const to = normalizeType(toType);
237
+
238
+ if (from === to) {
239
+ return { exists: true, implicit: true, safe: true, using: null, dataLossRisk: null };
240
+ }
241
+
242
+ // Check parameterized types (same family)
243
+ const fromParsed = parseTypeParams(from);
244
+ const toParsed = parseTypeParams(to);
245
+
246
+ if (fromParsed.family === toParsed.family) {
247
+ return getSameFamilyCastInfo(fromParsed, toParsed);
248
+ }
249
+
250
+ // Check explicit cast matrix
251
+ const sourceCasts = CAST_MATRIX[from];
252
+ if (sourceCasts && sourceCasts[to]) {
253
+ return { exists: true, ...sourceCasts[to] };
254
+ }
255
+
256
+ // Try reverse lookup for assignment casts
257
+ // (Some types can be cast to even if not in the forward direction)
258
+ const targetCasts = CAST_MATRIX[to];
259
+ if (targetCasts && targetCasts[from]) {
260
+ // Reverse the cast info
261
+ const info = targetCasts[from];
262
+ return {
263
+ exists: true,
264
+ implicit: false, // Not implicit in reverse
265
+ safe: info.safe,
266
+ using: info.using ? `::${to}` : null,
267
+ dataLossRisk: info.dataLossRisk,
268
+ };
269
+ }
270
+
271
+ // No cast path found
272
+ return { exists: false, implicit: false, safe: false, using: null, dataLossRisk: 'no_cast_path' };
273
+ }
274
+
275
+ /**
276
+ * Handle same-family type changes (varchar(50) → varchar(255), etc.)
277
+ */
278
+ function getSameFamilyCastInfo(fromParsed, toParsed) {
279
+ const family = fromParsed.family;
280
+
281
+ // String types with length
282
+ if (['character varying', 'varchar', 'character', 'char'].includes(family)) {
283
+ const fromLen = fromParsed.params?.length || Infinity;
284
+ const toLen = toParsed.params?.length || Infinity;
285
+
286
+ if (toLen >= fromLen) {
287
+ // Widening (safe)
288
+ return { exists: true, implicit: true, safe: true, using: null, dataLossRisk: null };
289
+ } else {
290
+ // Narrowing (data loss)
291
+ return { exists: true, implicit: true, safe: false, using: null, dataLossRisk: 'truncation' };
292
+ }
293
+ }
294
+
295
+ // Numeric types with precision/scale
296
+ if (family === 'numeric' || family === 'decimal') {
297
+ const fromPrec = fromParsed.params?.precision || Infinity;
298
+ const toPrec = toParsed.params?.precision || Infinity;
299
+ const fromScale = fromParsed.params?.scale || 0;
300
+ const toScale = toParsed.params?.scale || 0;
301
+
302
+ // Widening precision is safe
303
+ if (toPrec >= fromPrec && toScale >= fromScale) {
304
+ return { exists: true, implicit: true, safe: true, using: null, dataLossRisk: null };
305
+ }
306
+
307
+ // Narrowing may lose data
308
+ return { exists: true, implicit: true, safe: false, using: null, dataLossRisk: 'precision_loss' };
309
+ }
310
+
311
+ // Bit types
312
+ if (family === 'bit' || family === 'bit varying' || family === 'varbit') {
313
+ const fromLen = fromParsed.params?.length || Infinity;
314
+ const toLen = toParsed.params?.length || Infinity;
315
+
316
+ if (toLen >= fromLen) {
317
+ return { exists: true, implicit: true, safe: true, using: null, dataLossRisk: null };
318
+ }
319
+ return { exists: true, implicit: true, safe: false, using: null, dataLossRisk: 'truncation' };
320
+ }
321
+
322
+ // Default: same family, no params
323
+ return { exists: true, implicit: true, safe: true, using: null, dataLossRisk: null };
324
+ }
325
+
326
+ /**
327
+ * Check if a cast is implicit (can be done without USING clause).
328
+ * @param {string} fromType
329
+ * @param {string} toType
330
+ * @returns {boolean}
331
+ */
332
+ export function isImplicitCast(fromType, toType) {
333
+ const info = getCastInfo(fromType, toType);
334
+ return info.exists && info.implicit;
335
+ }
336
+
337
+ /**
338
+ * Check if a cast is safe (no data loss).
339
+ * @param {string} fromType
340
+ * @param {string} toType
341
+ * @returns {boolean}
342
+ */
343
+ export function isSafeCast(fromType, toType) {
344
+ const info = getCastInfo(fromType, toType);
345
+ return info.exists && info.safe;
346
+ }
347
+
348
+ /**
349
+ * Check if a cast is a widening (e.g., int → bigint, varchar(50) → varchar(255)).
350
+ * @param {string} fromType
351
+ * @param {string} toType
352
+ * @returns {boolean}
353
+ */
354
+ export function isWideningCast(fromType, toType) {
355
+ const fromParsed = parseTypeParams(fromType);
356
+ const toParsed = parseTypeParams(toType);
357
+
358
+ // Same family checks
359
+ if (fromParsed.family === toParsed.family) {
360
+ const family = fromParsed.family;
361
+
362
+ // String types: longer is wider
363
+ if (['character varying', 'varchar', 'character', 'char'].includes(family)) {
364
+ const fromLen = fromParsed.params?.length || Infinity;
365
+ const toLen = toParsed.params?.length || Infinity;
366
+ return toLen >= fromLen;
367
+ }
368
+
369
+ // Numeric types: higher precision is wider
370
+ if (family === 'numeric' || family === 'decimal') {
371
+ const fromPrec = fromParsed.params?.precision || Infinity;
372
+ const toPrec = toParsed.params?.precision || Infinity;
373
+ return toPrec >= fromPrec;
374
+ }
375
+ }
376
+
377
+ // Cross-family widening
378
+ const wideningCasts = [
379
+ ['smallint', 'integer'],
380
+ ['smallint', 'bigint'],
381
+ ['smallint', 'real'],
382
+ ['smallint', 'double precision'],
383
+ ['smallint', 'numeric'],
384
+ ['integer', 'bigint'],
385
+ ['integer', 'real'],
386
+ ['integer', 'double precision'],
387
+ ['integer', 'numeric'],
388
+ ['bigint', 'real'],
389
+ ['bigint', 'double precision'],
390
+ ['bigint', 'numeric'],
391
+ ['real', 'double precision'],
392
+ ['real', 'numeric'],
393
+ ['double precision', 'numeric'],
394
+ ['date', 'timestamp'],
395
+ ['date', 'timestamp with time zone'],
396
+ ['timestamp', 'timestamp with time zone'],
397
+ ['character varying', 'text'],
398
+ ['json', 'jsonb'],
399
+ ];
400
+
401
+ const from = normalizeType(fromType);
402
+ const to = normalizeType(toType);
403
+
404
+ return wideningCasts.some(([f, t]) => f === from && t === to);
405
+ }
406
+
407
+ /**
408
+ * Check if a cast is narrowing (potential data loss).
409
+ */
410
+ export function isNarrowingCast(fromType, toType) {
411
+ return !isWideningCast(fromType, toType) && getCastInfo(fromType, toType).exists;
412
+ }
413
+
414
+ /**
415
+ * Check if two types are in the same family.
416
+ * @param {string} typeA
417
+ * @param {string} typeB
418
+ * @returns {boolean}
419
+ */
420
+ export function sameTypeFamily(typeA, typeB) {
421
+ const a = parseTypeParams(typeA);
422
+ const b = parseTypeParams(typeB);
423
+ return a.family === b.family;
424
+ }
425
+
426
+ /**
427
+ * Check if types are exactly equal (including parameters).
428
+ * @param {string} typeA
429
+ * @param {string} typeB
430
+ * @returns {boolean}
431
+ */
432
+ export function typesEqual(typeA, typeB) {
433
+ return normalizeType(typeA) === normalizeType(typeB);
434
+ }
435
+
436
+ /**
437
+ * Get the USING clause for a type cast.
438
+ * @param {string} fromType
439
+ * @param {string} toType
440
+ * @param {string} columnName
441
+ * @returns {string|null}
442
+ */
443
+ export function getCastUsingClause(fromType, toType, columnName) {
444
+ const info = getCastInfo(fromType, toType);
445
+
446
+ if (!info.exists) return null;
447
+ if (info.implicit) return `ALTER COLUMN ${columnName} TYPE ${normalizeType(toType)}`;
448
+
449
+ const to = normalizeType(toType);
450
+
451
+ if (info.using) {
452
+ return `ALTER COLUMN ${columnName} TYPE ${to} USING ${columnName}${info.using}`;
453
+ }
454
+
455
+ return `ALTER COLUMN ${columnName} TYPE ${to} USING ${columnName}::${to}`;
456
+ }
457
+
458
+ /**
459
+ * List of type families for compatibility checks.
460
+ */
461
+ export const TYPE_FAMILIES = {
462
+ integer: ['smallint', 'integer', 'bigint', 'serial', 'bigserial', 'smallserial'],
463
+ float: ['real', 'double precision', 'float4', 'float8'],
464
+ numeric: ['numeric', 'decimal'],
465
+ boolean: ['boolean', 'bool'],
466
+ string: ['text', 'character varying', 'varchar', 'character', 'char'],
467
+ datetime: ['date', 'timestamp', 'timestamp with time zone', 'time', 'time with time zone'],
468
+ json: ['json', 'jsonb'],
469
+ uuid: ['uuid'],
470
+ binary: ['bytea'],
471
+ array: [], // Arrays are detected by [] suffix
472
+ };
package/src/errors.js ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Custom error types for the Schema Weaver Migration Engine.
3
+ * All errors extend the base MigrationError class.
4
+ */
5
+
6
+ export class MigrationError extends Error {
7
+ constructor(message, details = {}) {
8
+ super(message);
9
+ this.name = 'MigrationError';
10
+ this.details = details;
11
+ this.timestamp = new Date().toISOString();
12
+ }
13
+
14
+ toJSON() {
15
+ return {
16
+ name: this.name,
17
+ message: this.message,
18
+ details: this.details,
19
+ timestamp: this.timestamp,
20
+ stack: this.stack,
21
+ };
22
+ }
23
+ }
24
+
25
+ export class IntrospectionError extends MigrationError {
26
+ constructor(message, details) {
27
+ super(message, details);
28
+ this.name = 'IntrospectionError';
29
+ }
30
+ }
31
+
32
+ export class DiffError extends MigrationError {
33
+ constructor(message, details) {
34
+ super(message, details);
35
+ this.name = 'DiffError';
36
+ }
37
+ }
38
+
39
+ export class DDLGenerationError extends MigrationError {
40
+ constructor(message, details) {
41
+ super(message, details);
42
+ this.name = 'DDLGenerationError';
43
+ }
44
+ }
45
+
46
+ export class ExecutionError extends MigrationError {
47
+ constructor(message, details) {
48
+ super(message, details);
49
+ this.name = 'ExecutionError';
50
+ this.stepId = details?.step?.id;
51
+ this.phase = details?.phase?.name;
52
+ this.sql = details?.step?.sql;
53
+ this.cause = details?.cause;
54
+ }
55
+ }
56
+
57
+ export class PreCheckFailedError extends ExecutionError {
58
+ constructor(message, details) {
59
+ super(message, details);
60
+ this.name = 'PreCheckFailedError';
61
+ }
62
+ }
63
+
64
+ export class PostCheckFailedError extends ExecutionError {
65
+ constructor(message, details) {
66
+ super(message, details);
67
+ this.name = 'PostCheckFailedError';
68
+ }
69
+ }
70
+
71
+ export class MigrationConflictError extends MigrationError {
72
+ constructor(message, details) {
73
+ super(message, details);
74
+ this.name = 'MigrationConflictError';
75
+ }
76
+ }
77
+
78
+ export class VersionIncompatibilityError extends MigrationError {
79
+ constructor(message, details) {
80
+ super(message, details);
81
+ this.name = 'VersionIncompatibilityError';
82
+ this.requiredVersion = details?.requiredVersion;
83
+ this.currentVersion = details?.currentVersion;
84
+ }
85
+ }
86
+
87
+ export class RollbackError extends MigrationError {
88
+ constructor(message, details) {
89
+ super(message, details);
90
+ this.name = 'RollbackError';
91
+ }
92
+ }
93
+
94
+ export class DriftDetectedError extends MigrationError {
95
+ constructor(message, details) {
96
+ super(message, details);
97
+ this.name = 'DriftDetectedError';
98
+ this.driftDetails = details?.drift;
99
+ }
100
+ }
101
+
102
+ export class LockAcquisitionError extends MigrationError {
103
+ constructor(message, details) {
104
+ super(message, details);
105
+ this.name = 'LockAcquisitionError';
106
+ }
107
+ }
108
+
109
+ export class TimeoutError extends MigrationError {
110
+ constructor(message, details) {
111
+ super(message, details);
112
+ this.name = 'TimeoutError';
113
+ this.timeout = details?.timeout;
114
+ }
115
+ }
116
+
117
+ export class ValidationError extends MigrationError {
118
+ constructor(message, details) {
119
+ super(message, details);
120
+ this.name = 'ValidationError';
121
+ this.validationErrors = details?.errors || [];
122
+ }
123
+ }
124
+
125
+ export class StorageError extends MigrationError {
126
+ constructor(message, details) {
127
+ super(message, details);
128
+ this.name = 'StorageError';
129
+ }
130
+ }
131
+
132
+ export class PlanBlockedError extends MigrationError {
133
+ constructor(message, details) {
134
+ super(message, details);
135
+ this.name = 'PlanBlockedError';
136
+ this.blockReason = details?.blockReason;
137
+ this.riskAssessment = details?.riskAssessment;
138
+ }
139
+ }
140
+
141
+ export class RecoveryError extends MigrationError {
142
+ constructor(message, details) {
143
+ super(message, details);
144
+ this.name = 'RecoveryError';
145
+ this.recoverySteps = details?.recoverySteps || [];
146
+ }
147
+ }