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