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
package/src/index.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { SchemaIntrospector } from './introspection/index.js';
|
|
2
|
+
import { SchemaDiffer } from './differ/index.js';
|
|
3
|
+
import { MigrationPlanner } from './planner/index.js';
|
|
4
|
+
import { MigrationExecutor } from './executor/migration-executor.js';
|
|
5
|
+
import { TransactionManager } from './executor/transaction-manager.js';
|
|
6
|
+
import { MigrationTable } from './storage/migration-table.js';
|
|
7
|
+
import { RollbackGenerator } from './storage/rollback-generator.js';
|
|
8
|
+
import { BehavioralExtractor } from './behavioral/behavioral-extractor.js';
|
|
9
|
+
import { BehavioralApplier } from './behavioral/behavioral-applier.js';
|
|
10
|
+
import { DdlGenerator } from './ddl-generator/index.js';
|
|
11
|
+
import { RiskEngine } from './risk/index.js';
|
|
12
|
+
import { InMemoryStorageProvider } from './storage/index.js';
|
|
13
|
+
import {
|
|
14
|
+
MigrationError,
|
|
15
|
+
ExecutionError,
|
|
16
|
+
IntrospectionError,
|
|
17
|
+
DiffError,
|
|
18
|
+
DDLGenerationError,
|
|
19
|
+
PreCheckFailedError,
|
|
20
|
+
PostCheckFailedError,
|
|
21
|
+
MigrationConflictError,
|
|
22
|
+
VersionIncompatibilityError,
|
|
23
|
+
RollbackError,
|
|
24
|
+
DriftDetectedError,
|
|
25
|
+
LockAcquisitionError,
|
|
26
|
+
TimeoutError,
|
|
27
|
+
ValidationError,
|
|
28
|
+
StorageError,
|
|
29
|
+
PlanBlockedError,
|
|
30
|
+
RecoveryError,
|
|
31
|
+
} from './errors.js';
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
SchemaIntrospector,
|
|
35
|
+
SchemaDiffer,
|
|
36
|
+
DdlGenerator,
|
|
37
|
+
RiskEngine,
|
|
38
|
+
MigrationPlanner,
|
|
39
|
+
MigrationExecutor,
|
|
40
|
+
BehavioralExtractor,
|
|
41
|
+
BehavioralApplier,
|
|
42
|
+
TransactionManager,
|
|
43
|
+
MigrationTable,
|
|
44
|
+
RollbackGenerator,
|
|
45
|
+
InMemoryStorageProvider,
|
|
46
|
+
MigrationError,
|
|
47
|
+
ExecutionError,
|
|
48
|
+
IntrospectionError,
|
|
49
|
+
DiffError,
|
|
50
|
+
DDLGenerationError,
|
|
51
|
+
PreCheckFailedError,
|
|
52
|
+
PostCheckFailedError,
|
|
53
|
+
MigrationConflictError,
|
|
54
|
+
VersionIncompatibilityError,
|
|
55
|
+
RollbackError,
|
|
56
|
+
DriftDetectedError,
|
|
57
|
+
LockAcquisitionError,
|
|
58
|
+
TimeoutError,
|
|
59
|
+
ValidationError,
|
|
60
|
+
StorageError,
|
|
61
|
+
PlanBlockedError,
|
|
62
|
+
RecoveryError,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
import { RISK_LEVELS, RISK_LEVEL_ORDER, mapExecutorStatusToDb } from './constants.js';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @typedef {Object} EngineConfig
|
|
69
|
+
* @property {string} [engineVersion='1.0.0']
|
|
70
|
+
* @property {string} [lockTimeout='5s']
|
|
71
|
+
* @property {string} [statementTimeout='30s']
|
|
72
|
+
* @property {boolean} [dryRun=false]
|
|
73
|
+
* @property {boolean} [snapshotBefore=true]
|
|
74
|
+
* @property {boolean} [verifyAfter=true]
|
|
75
|
+
* @property {string} [connectionId] - Database connection ID for multi-DB support
|
|
76
|
+
* @property {'none'|'low'|'medium'|'high'|'critical'} [allowRiskBelow] - Only allow migrations below this risk level
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
export class SwMigrationEngine {
|
|
80
|
+
/**
|
|
81
|
+
* @param {EngineConfig} config
|
|
82
|
+
*/
|
|
83
|
+
constructor(config = {}) {
|
|
84
|
+
this.config = {
|
|
85
|
+
engineVersion: '1.0.0',
|
|
86
|
+
lockTimeout: '5s',
|
|
87
|
+
statementTimeout: '30s',
|
|
88
|
+
dryRun: false,
|
|
89
|
+
snapshotBefore: true,
|
|
90
|
+
verifyAfter: true,
|
|
91
|
+
connectionId: config.connectionId || null,
|
|
92
|
+
allowRiskBelow: 'critical',
|
|
93
|
+
...config,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (!this.config.connectionId) {
|
|
97
|
+
console.warn(
|
|
98
|
+
'[SwMigrationEngine] No connectionId provided. ' +
|
|
99
|
+
'Migration records will not be scoped to a database. ' +
|
|
100
|
+
'This may cause issues in multi-database environments.'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.pool = null;
|
|
105
|
+
this.introspector = null;
|
|
106
|
+
this.differ = new SchemaDiffer();
|
|
107
|
+
this.planner = new MigrationPlanner();
|
|
108
|
+
this.riskEngine = new RiskEngine();
|
|
109
|
+
this.ddlGenerator = new DdlGenerator();
|
|
110
|
+
this.behavioralExtractor = new BehavioralExtractor();
|
|
111
|
+
this.behavioralApplier = new BehavioralApplier();
|
|
112
|
+
this.rollbackGenerator = new RollbackGenerator();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Set the database pool
|
|
117
|
+
* @param {import('pg').Pool} pool
|
|
118
|
+
*/
|
|
119
|
+
setPool(pool) {
|
|
120
|
+
this.pool = pool;
|
|
121
|
+
this.introspector = new SchemaIntrospector(pool);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* STEP 1: Introspect a live database
|
|
126
|
+
* @param {import('pg').Pool} [pool]
|
|
127
|
+
* @param {Object} [options]
|
|
128
|
+
* @returns {Promise<import('./types/schema.js').SchemaSnapshot>}
|
|
129
|
+
*/
|
|
130
|
+
async introspect(pool, options = {}) {
|
|
131
|
+
const usePool = pool || this.pool;
|
|
132
|
+
if (!usePool) {
|
|
133
|
+
throw new IntrospectionError('Database pool is required. Call setPool() or pass pool to introspect().');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.introspector = new SchemaIntrospector(usePool);
|
|
137
|
+
return this.introspector.introspect(options);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* STEP 2: Diff two schema snapshots
|
|
142
|
+
* @param {import('./types/schema.js').SchemaSnapshot} desired - Target schema
|
|
143
|
+
* @param {import('./types/schema.js').SchemaSnapshot} current - Current schema
|
|
144
|
+
* @returns {import('./types/changes.js').SchemaDiff}
|
|
145
|
+
*/
|
|
146
|
+
diff(desired, current) {
|
|
147
|
+
return this.differ.diff(desired, current);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* STEP 3: Generate DDL from a diff
|
|
152
|
+
* @param {import('./types/changes.js').SchemaDiff} diff
|
|
153
|
+
* @param {Object} [options]
|
|
154
|
+
* @returns {string}
|
|
155
|
+
*/
|
|
156
|
+
generateDDL(diff, options = {}) {
|
|
157
|
+
const changes = Array.isArray(diff) ? diff : (diff.changes || []);
|
|
158
|
+
return this.ddlGenerator.generate(changes, options);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* STEP 4: Create a migration plan from a diff
|
|
163
|
+
* @param {import('./types/changes.js').SchemaDiff} diff
|
|
164
|
+
* @param {Object} [options]
|
|
165
|
+
* @returns {import('./types/migration.js').MigrationPlan}
|
|
166
|
+
*/
|
|
167
|
+
plan(diff, options = {}) {
|
|
168
|
+
const plan = this.planner.createPlan(diff, {
|
|
169
|
+
...options,
|
|
170
|
+
pgVersion: options.pgVersion,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const riskAssessment = this.riskEngine.assess(plan.changes || [], options.pgVersion);
|
|
174
|
+
plan.riskAssessment = riskAssessment;
|
|
175
|
+
|
|
176
|
+
const riskOrder = ['none', 'low', 'medium', 'high', 'critical'];
|
|
177
|
+
const maxAllowedIdx = riskOrder.indexOf(this.config.allowRiskBelow);
|
|
178
|
+
const hasCritical = (plan.summary?.riskSummary?.critical || 0) > 0;
|
|
179
|
+
const hasHigh = (plan.summary?.riskSummary?.high || 0) > 0;
|
|
180
|
+
|
|
181
|
+
if (hasCritical && maxAllowedIdx <= riskOrder.indexOf('critical')) {
|
|
182
|
+
plan.blocked = true;
|
|
183
|
+
plan.blockReason = `Migration blocked: ${plan.summary.riskSummary.critical} critical risk(s) detected.`;
|
|
184
|
+
} else if (hasHigh && maxAllowedIdx < riskOrder.indexOf('high')) {
|
|
185
|
+
plan.blocked = true;
|
|
186
|
+
plan.blockReason = `Migration blocked: ${plan.summary.riskSummary.high} high risk(s) detected.`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return plan;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* STEP 5: Execute a migration plan
|
|
194
|
+
* @param {import('pg').Pool} [pool]
|
|
195
|
+
* @param {import('./types/migration.js').MigrationPlan} plan
|
|
196
|
+
* @param {import('./types/execution.js').ExecutionOptions} [options]
|
|
197
|
+
* @returns {Promise<import('./types/migration.js').MigrationResult>}
|
|
198
|
+
*/
|
|
199
|
+
async execute(pool, plan, options = {}) {
|
|
200
|
+
const usePool = pool || this.pool;
|
|
201
|
+
if (!usePool) {
|
|
202
|
+
throw new ExecutionError('Database pool is required. Call setPool() or pass pool to execute().');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (plan.blocked && !options.allowBlocked) {
|
|
206
|
+
throw new PlanBlockedError(plan.blockReason || 'Migration is blocked', { plan });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const connectionId = options.connectionId || this.config.connectionId || null;
|
|
210
|
+
if (!connectionId) {
|
|
211
|
+
console.warn(
|
|
212
|
+
'[SwMigrationEngine] No connectionId for execute(). ' +
|
|
213
|
+
'Migration records will not be scoped to a database.'
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const storage = new MigrationTable(usePool, connectionId);
|
|
218
|
+
await storage.ensureTable();
|
|
219
|
+
|
|
220
|
+
const introspector = new SchemaIntrospector(usePool);
|
|
221
|
+
const executor = new MigrationExecutor(usePool, introspector, storage, {
|
|
222
|
+
...this.config,
|
|
223
|
+
...options,
|
|
224
|
+
connectionId,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (options.onProgress) {
|
|
228
|
+
executor.onProgress(options.onProgress);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return executor.execute(plan, options);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Dry-run a migration — validates SQL without applying changes
|
|
236
|
+
* @param {import('pg').Pool} [pool]
|
|
237
|
+
* @param {import('./types/migration.js').MigrationPlan} plan
|
|
238
|
+
* @param {Object} [options]
|
|
239
|
+
* @returns {Promise<Object>}
|
|
240
|
+
*/
|
|
241
|
+
async dryRun(pool, plan, options = {}) {
|
|
242
|
+
return this.execute(pool, plan, { ...options, dryRun: true });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* ONE-SHOT: Introspect → Diff → Plan → Execute
|
|
247
|
+
* Convenience method for the full pipeline
|
|
248
|
+
* @param {import('pg').Pool} [pool]
|
|
249
|
+
* @param {import('./types/schema.js').SchemaSnapshot} desired - Target schema
|
|
250
|
+
* @param {Object} [options]
|
|
251
|
+
* @returns {Promise<import('./types/migration.js').MigrationResult>}
|
|
252
|
+
*/
|
|
253
|
+
async migrate(pool, desired, options = {}) {
|
|
254
|
+
const usePool = pool || this.pool;
|
|
255
|
+
if (!usePool) {
|
|
256
|
+
throw new ExecutionError('Database pool is required. Call setPool() or pass pool to migrate().');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const connectionId = options.connectionId || this.config.connectionId || null;
|
|
260
|
+
if (!connectionId) {
|
|
261
|
+
console.warn(
|
|
262
|
+
'[SwMigrationEngine] No connectionId for migrate(). ' +
|
|
263
|
+
'Migration records will not be scoped to a database.'
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const current = await this.introspect(usePool, options);
|
|
268
|
+
|
|
269
|
+
if (current.checksum === desired.checksum) {
|
|
270
|
+
return {
|
|
271
|
+
success: true,
|
|
272
|
+
status: 'no_changes',
|
|
273
|
+
migrationId: null,
|
|
274
|
+
message: 'No schema changes detected.',
|
|
275
|
+
diff: { summary: { totalChanges: 0 }, changes: [] },
|
|
276
|
+
executedSteps: [],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const diff = this.diff(desired, current);
|
|
281
|
+
|
|
282
|
+
if (diff.summary.totalChanges === 0) {
|
|
283
|
+
return {
|
|
284
|
+
success: true,
|
|
285
|
+
status: 'no_changes',
|
|
286
|
+
migrationId: null,
|
|
287
|
+
message: 'No schema changes detected.',
|
|
288
|
+
diff,
|
|
289
|
+
executedSteps: [],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const plan = this.plan(diff, options);
|
|
294
|
+
|
|
295
|
+
if (plan.blocked) {
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
status: 'blocked',
|
|
299
|
+
migrationId: null,
|
|
300
|
+
message: plan.blockReason,
|
|
301
|
+
plan,
|
|
302
|
+
diff,
|
|
303
|
+
executedSteps: [],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const execOptions = { ...options, connectionId };
|
|
308
|
+
|
|
309
|
+
if (options.dryRun) {
|
|
310
|
+
const dryRunResult = await this.dryRun(usePool, plan, execOptions);
|
|
311
|
+
return {
|
|
312
|
+
...dryRunResult,
|
|
313
|
+
status: 'dry_run',
|
|
314
|
+
plan,
|
|
315
|
+
diff,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const result = await this.execute(usePool, plan, execOptions);
|
|
320
|
+
return {
|
|
321
|
+
...result,
|
|
322
|
+
plan,
|
|
323
|
+
diff,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get migration history
|
|
329
|
+
* @param {import('pg').Pool} [pool]
|
|
330
|
+
* @param {Object} [options]
|
|
331
|
+
* @returns {Promise<Array>}
|
|
332
|
+
*/
|
|
333
|
+
async getHistory(pool, options = {}) {
|
|
334
|
+
const usePool = pool || this.pool;
|
|
335
|
+
if (!usePool) {
|
|
336
|
+
throw new StorageError('Database pool is required.');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const connectionId = options.connectionId || this.config.connectionId || null;
|
|
340
|
+
const storage = new MigrationTable(usePool, connectionId);
|
|
341
|
+
await storage.ensureTable();
|
|
342
|
+
return storage.getHistory(connectionId, options.limit || 50, options.offset || 0);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get last successful migration
|
|
347
|
+
* @param {import('pg').Pool} [pool]
|
|
348
|
+
* @param {Object} [options]
|
|
349
|
+
* @returns {Promise<Object|null>}
|
|
350
|
+
*/
|
|
351
|
+
async getLastMigration(pool, options = {}) {
|
|
352
|
+
const usePool = pool || this.pool;
|
|
353
|
+
if (!usePool) {
|
|
354
|
+
throw new StorageError('Database pool is required.');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const connectionId = options.connectionId || this.config.connectionId || null;
|
|
358
|
+
const storage = new MigrationTable(usePool, connectionId);
|
|
359
|
+
await storage.ensureTable();
|
|
360
|
+
return storage.getLastMigration(connectionId);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Rollback a migration (best-effort)
|
|
365
|
+
* @param {import('pg').Pool} [pool]
|
|
366
|
+
* @param {string} migrationId
|
|
367
|
+
* @param {Object} [options]
|
|
368
|
+
* @returns {Promise<Object>}
|
|
369
|
+
*/
|
|
370
|
+
async rollback(pool, migrationId, options = {}) {
|
|
371
|
+
const usePool = pool || this.pool;
|
|
372
|
+
if (!usePool) {
|
|
373
|
+
throw new StorageError('Database pool is required.');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const connectionId = options.connectionId || this.config.connectionId || null;
|
|
377
|
+
const storage = new MigrationTable(usePool, connectionId);
|
|
378
|
+
await storage.ensureTable();
|
|
379
|
+
|
|
380
|
+
const migration = await storage.getRollbackSQL(migrationId);
|
|
381
|
+
if (!migration) {
|
|
382
|
+
throw new RollbackError(`Migration ${migrationId} not found.`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (migration.status !== 'completed') {
|
|
386
|
+
throw new RollbackError(`Cannot rollback migration with status "${migration.status}".`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const rollbackSteps = this.rollbackGenerator.generateRollback(migration);
|
|
390
|
+
|
|
391
|
+
if (rollbackSteps.length === 0) {
|
|
392
|
+
return {
|
|
393
|
+
success: false,
|
|
394
|
+
migrationId,
|
|
395
|
+
status: 'no_rollback_available',
|
|
396
|
+
message: 'No rollback steps could be generated for this migration.',
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
for (const step of rollbackSteps) {
|
|
401
|
+
if (!step.isTransactional) {
|
|
402
|
+
step.warning = 'This step runs outside a transaction and cannot be rolled back.';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const transactional = rollbackSteps.filter(s => s.isTransactional);
|
|
407
|
+
const nonTransactional = rollbackSteps.filter(s => !s.isTransactional);
|
|
408
|
+
|
|
409
|
+
const results = [];
|
|
410
|
+
|
|
411
|
+
if (transactional.length > 0) {
|
|
412
|
+
const tm = new TransactionManager(usePool);
|
|
413
|
+
const txResults = await tm.executeTransactional(
|
|
414
|
+
transactional.map(s => ({ id: s.originalChangeId, sql: s.sql })),
|
|
415
|
+
{
|
|
416
|
+
lockTimeout: this.config.lockTimeout,
|
|
417
|
+
statementTimeout: this.config.statementTimeout,
|
|
418
|
+
}
|
|
419
|
+
);
|
|
420
|
+
results.push(...txResults);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
for (const step of nonTransactional) {
|
|
424
|
+
const tm = new TransactionManager(usePool);
|
|
425
|
+
const result = await tm.executeNonTransactional(
|
|
426
|
+
{ id: step.originalChangeId, sql: step.sql },
|
|
427
|
+
{ statementTimeout: this.config.statementTimeout }
|
|
428
|
+
);
|
|
429
|
+
results.push(result);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await storage.markRolledBack(migrationId);
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
success: results.every(r => r.success),
|
|
436
|
+
migrationId,
|
|
437
|
+
status: 'rolled_back',
|
|
438
|
+
steps: results,
|
|
439
|
+
warning: 'Rollback is best-effort. Some changes (DROP TABLE, DROP COLUMN, enum value removal) cannot be reversed.',
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Validate a desired schema against PG version constraints
|
|
445
|
+
* @param {import('./types/schema.js').SchemaSnapshot} desired
|
|
446
|
+
* @param {number} pgVersion
|
|
447
|
+
* @returns {Object}
|
|
448
|
+
*/
|
|
449
|
+
validate(desired, pgVersion) {
|
|
450
|
+
return this.riskEngine.validateVersionCompatibility(desired, pgVersion);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Assess risk for changes (alias for riskEngine.assess)
|
|
455
|
+
* @param {Array} changes
|
|
456
|
+
* @param {number} [pgVersion]
|
|
457
|
+
* @returns {Object}
|
|
458
|
+
*/
|
|
459
|
+
assessRisk(changes, pgVersion) {
|
|
460
|
+
return this.riskEngine.assess(changes, pgVersion);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Create a migration plan from changes
|
|
465
|
+
* @param {Array} changes
|
|
466
|
+
* @param {Object} options
|
|
467
|
+
* @returns {import('./types/migration.js').MigrationPlan}
|
|
468
|
+
*/
|
|
469
|
+
createMigrationPlan(changes, options) {
|
|
470
|
+
const diff = { changes, summary: { totalChanges: changes.length } };
|
|
471
|
+
return this.plan(diff, options);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Diff two schemas (alias for diff)
|
|
476
|
+
* @param {Object} desired
|
|
477
|
+
* @param {Object} current
|
|
478
|
+
* @returns {import('./types/changes.js').SchemaDiff}
|
|
479
|
+
*/
|
|
480
|
+
diffSchemas(desired, current) {
|
|
481
|
+
return this.diff(desired, current);
|
|
482
|
+
}
|
|
483
|
+
}
|