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,423 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
const ENGINE_VERSION = '1.0.0';
|
|
4
|
+
|
|
5
|
+
export class MigrationTable {
|
|
6
|
+
tableName = 'migration_history';
|
|
7
|
+
|
|
8
|
+
constructor(pool, connectionId = null) {
|
|
9
|
+
this.pool = pool;
|
|
10
|
+
this.connectionId = connectionId;
|
|
11
|
+
|
|
12
|
+
if (!connectionId) {
|
|
13
|
+
console.warn(
|
|
14
|
+
'[SchemaWeaver] No connectionId provided. ' +
|
|
15
|
+
'Migration records will not be scoped to a specific database. ' +
|
|
16
|
+
'This may cause issues in multi-database environments.'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async ensureTable() {
|
|
22
|
+
const tableCheck = await this.pool.query(`
|
|
23
|
+
SELECT EXISTS (
|
|
24
|
+
SELECT FROM information_schema.tables
|
|
25
|
+
WHERE table_name = 'migration_history'
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
|
|
29
|
+
if (!tableCheck.rows[0].exists) {
|
|
30
|
+
await this.pool.query(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS migration_history (
|
|
32
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
33
|
+
connection_id UUID,
|
|
34
|
+
version VARCHAR(30) NOT NULL,
|
|
35
|
+
name VARCHAR(255) NOT NULL,
|
|
36
|
+
checksum VARCHAR(64) NOT NULL DEFAULT '',
|
|
37
|
+
up_sql TEXT NOT NULL DEFAULT '',
|
|
38
|
+
down_sql TEXT,
|
|
39
|
+
full_snapshot_sql TEXT,
|
|
40
|
+
commit_message TEXT,
|
|
41
|
+
applied_by UUID,
|
|
42
|
+
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
43
|
+
execution_time_ms INTEGER,
|
|
44
|
+
error_message TEXT,
|
|
45
|
+
rolled_back_at TIMESTAMPTZ,
|
|
46
|
+
rolled_back_by UUID,
|
|
47
|
+
metadata JSONB,
|
|
48
|
+
applied_at TIMESTAMPTZ,
|
|
49
|
+
created_at TIMESTAMPTZ DEFAULT now(),
|
|
50
|
+
|
|
51
|
+
schema_diff JSONB,
|
|
52
|
+
sql_statements JSONB,
|
|
53
|
+
execution_results JSONB,
|
|
54
|
+
snapshot_before JSONB,
|
|
55
|
+
snapshot_after JSONB,
|
|
56
|
+
risk_summary JSONB,
|
|
57
|
+
warnings JSONB,
|
|
58
|
+
direction VARCHAR(10) DEFAULT 'up',
|
|
59
|
+
change_count INTEGER DEFAULT 0,
|
|
60
|
+
create_count INTEGER DEFAULT 0,
|
|
61
|
+
alter_count INTEGER DEFAULT 0,
|
|
62
|
+
drop_count INTEGER DEFAULT 0,
|
|
63
|
+
rename_count INTEGER DEFAULT 0,
|
|
64
|
+
pg_version VARCHAR(20),
|
|
65
|
+
engine_version VARCHAR(20),
|
|
66
|
+
rollback_sql JSONB,
|
|
67
|
+
tags TEXT[]
|
|
68
|
+
)
|
|
69
|
+
`);
|
|
70
|
+
} else {
|
|
71
|
+
await this.ensureEngineColumns();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await this.ensureIndexes();
|
|
75
|
+
await this.checkLegacyTable();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async ensureEngineColumns() {
|
|
79
|
+
const engineColumns = [
|
|
80
|
+
{ name: 'schema_diff', type: 'JSONB' },
|
|
81
|
+
{ name: 'sql_statements', type: 'JSONB' },
|
|
82
|
+
{ name: 'execution_results', type: 'JSONB' },
|
|
83
|
+
{ name: 'snapshot_before', type: 'JSONB' },
|
|
84
|
+
{ name: 'snapshot_after', type: 'JSONB' },
|
|
85
|
+
{ name: 'risk_summary', type: 'JSONB' },
|
|
86
|
+
{ name: 'warnings', type: 'JSONB' },
|
|
87
|
+
{ name: 'direction', type: 'VARCHAR(10)', def: "'up'" },
|
|
88
|
+
{ name: 'change_count', type: 'INTEGER', def: '0' },
|
|
89
|
+
{ name: 'create_count', type: 'INTEGER', def: '0' },
|
|
90
|
+
{ name: 'alter_count', type: 'INTEGER', def: '0' },
|
|
91
|
+
{ name: 'drop_count', type: 'INTEGER', def: '0' },
|
|
92
|
+
{ name: 'rename_count', type: 'INTEGER', def: '0' },
|
|
93
|
+
{ name: 'pg_version', type: 'VARCHAR(20)' },
|
|
94
|
+
{ name: 'engine_version', type: 'VARCHAR(20)' },
|
|
95
|
+
{ name: 'rollback_sql', type: 'JSONB' },
|
|
96
|
+
{ name: 'tags', type: 'TEXT[]' },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
for (const col of engineColumns) {
|
|
100
|
+
const colCheck = await this.pool.query(`
|
|
101
|
+
SELECT EXISTS (
|
|
102
|
+
SELECT FROM information_schema.columns
|
|
103
|
+
WHERE table_name = 'migration_history'
|
|
104
|
+
AND column_name = $1
|
|
105
|
+
)
|
|
106
|
+
`, [col.name]);
|
|
107
|
+
|
|
108
|
+
if (!colCheck.rows[0].exists) {
|
|
109
|
+
const defaultClause = col.def ? ` DEFAULT ${col.def}` : '';
|
|
110
|
+
await this.pool.query(`
|
|
111
|
+
ALTER TABLE migration_history
|
|
112
|
+
ADD COLUMN ${col.name} ${col.type}${defaultClause}
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async ensureIndexes() {
|
|
119
|
+
const indexes = [
|
|
120
|
+
{ name: 'idx_migration_history_status', on: 'status' },
|
|
121
|
+
{ name: 'idx_migration_history_connection', on: 'connection_id' },
|
|
122
|
+
{ name: 'idx_migration_history_applied_at', on: 'applied_at DESC NULLS LAST' },
|
|
123
|
+
{ name: 'idx_migration_history_version', on: 'version' },
|
|
124
|
+
{ name: 'idx_migration_history_name', on: 'name' },
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
for (const idx of indexes) {
|
|
128
|
+
await this.pool.query(`
|
|
129
|
+
CREATE INDEX IF NOT EXISTS ${idx.name}
|
|
130
|
+
ON migration_history (${idx.on})
|
|
131
|
+
`).catch(() => {});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async checkLegacyTable() {
|
|
136
|
+
const legacyCheck = await this.pool.query(`
|
|
137
|
+
SELECT EXISTS (
|
|
138
|
+
SELECT FROM information_schema.tables
|
|
139
|
+
WHERE table_name = '_sw_migrations'
|
|
140
|
+
)
|
|
141
|
+
`);
|
|
142
|
+
|
|
143
|
+
if (legacyCheck.rows[0].exists) {
|
|
144
|
+
console.warn(
|
|
145
|
+
'[SchemaWeaver] Legacy table _sw_migrations detected. ' +
|
|
146
|
+
'This table is deprecated. Migration history is now stored in migration_history.'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
generateVersion() {
|
|
152
|
+
const now = new Date();
|
|
153
|
+
const yyyy = now.getFullYear();
|
|
154
|
+
const MM = String(now.getMonth() + 1).padStart(2, '0');
|
|
155
|
+
const dd = String(now.getDate()).padStart(2, '0');
|
|
156
|
+
const HH = String(now.getHours()).padStart(2, '0');
|
|
157
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
158
|
+
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
159
|
+
return `${yyyy}${MM}${dd}${HH}${mm}${ss}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
computeChecksum(plan) {
|
|
163
|
+
const canonical = JSON.stringify(
|
|
164
|
+
plan.steps?.map(s => ({ sql: s.sql, id: s.id })) || []
|
|
165
|
+
);
|
|
166
|
+
return crypto.createHash('sha256').update(canonical).digest('hex');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
generateUpSql(plan) {
|
|
170
|
+
return plan.steps?.map(s => s.sql).filter(Boolean).join(';\n') || '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getEngineVersion() {
|
|
174
|
+
return ENGINE_VERSION;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
mapExecutorStatus(executorStatus) {
|
|
178
|
+
const STATUS_MAP = {
|
|
179
|
+
'COMPLETED': 'completed',
|
|
180
|
+
'completed': 'completed',
|
|
181
|
+
'PARTIALLY_APPLIED': 'partially_applied',
|
|
182
|
+
'partially_applied': 'partially_applied',
|
|
183
|
+
'FAILED': 'failed',
|
|
184
|
+
'failed': 'failed',
|
|
185
|
+
'DRY_RUN_SUCCESS': 'completed',
|
|
186
|
+
'dry_run_success': 'completed',
|
|
187
|
+
'DRY_RUN_FAILURE': 'failed',
|
|
188
|
+
'dry_run_failure': 'failed',
|
|
189
|
+
'running': 'running',
|
|
190
|
+
'pending': 'pending',
|
|
191
|
+
'rolled_back': 'rolled_back',
|
|
192
|
+
};
|
|
193
|
+
return STATUS_MAP[executorStatus] || executorStatus?.toLowerCase() || 'failed';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async createRecord(plan, connectionId = null) {
|
|
197
|
+
const cid = connectionId || this.connectionId;
|
|
198
|
+
const version = this.generateVersion();
|
|
199
|
+
const checksum = this.computeChecksum(plan);
|
|
200
|
+
const upSql = this.generateUpSql(plan);
|
|
201
|
+
|
|
202
|
+
const changes = plan.changes || plan.steps || [];
|
|
203
|
+
const changeCount = changes.length;
|
|
204
|
+
const createCount = changes.filter(c => c.changeType === 'CREATE').length;
|
|
205
|
+
const alterCount = changes.filter(c => c.changeType === 'ALTER').length;
|
|
206
|
+
const dropCount = changes.filter(c => c.changeType === 'DROP').length;
|
|
207
|
+
const renameCount = changes.filter(c => c.changeType === 'RENAME').length;
|
|
208
|
+
|
|
209
|
+
const result = await this.pool.query(`
|
|
210
|
+
INSERT INTO migration_history (
|
|
211
|
+
connection_id, version, name, checksum,
|
|
212
|
+
up_sql, status, direction,
|
|
213
|
+
schema_diff, sql_statements,
|
|
214
|
+
risk_summary, warnings,
|
|
215
|
+
change_count, create_count, alter_count, drop_count, rename_count,
|
|
216
|
+
pg_version, engine_version,
|
|
217
|
+
applied_at, created_at
|
|
218
|
+
) VALUES (
|
|
219
|
+
$1, $2, $3, $4,
|
|
220
|
+
$5, 'running', $6,
|
|
221
|
+
$7, $8,
|
|
222
|
+
$9, $10,
|
|
223
|
+
$11, $12, $13, $14, $15,
|
|
224
|
+
$16, $17,
|
|
225
|
+
now(), now()
|
|
226
|
+
)
|
|
227
|
+
RETURNING id, version
|
|
228
|
+
`, [
|
|
229
|
+
cid,
|
|
230
|
+
version,
|
|
231
|
+
plan.name || `migration_${version}`,
|
|
232
|
+
checksum,
|
|
233
|
+
upSql,
|
|
234
|
+
plan.direction || 'up',
|
|
235
|
+
JSON.stringify(plan.schemaDiff || plan.diff || {}),
|
|
236
|
+
JSON.stringify(plan.steps?.map(s => ({ stepId: s.id, sql: s.sql })) || []),
|
|
237
|
+
JSON.stringify(plan.riskSummary || {}),
|
|
238
|
+
JSON.stringify(plan.warnings || []),
|
|
239
|
+
changeCount, createCount, alterCount, dropCount, renameCount,
|
|
240
|
+
plan.pgVersion || null,
|
|
241
|
+
this.getEngineVersion(),
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
return result.rows[0];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async updateStepProgress(recordId, stepId, status, durationMs) {
|
|
248
|
+
await this.pool.query(`
|
|
249
|
+
UPDATE migration_history
|
|
250
|
+
SET execution_results =
|
|
251
|
+
COALESCE(execution_results, '{}'::jsonb) ||
|
|
252
|
+
jsonb_build_object($1::text, jsonb_build_object(
|
|
253
|
+
'status', $2,
|
|
254
|
+
'duration_ms', $3,
|
|
255
|
+
'completed_at', now()
|
|
256
|
+
))
|
|
257
|
+
WHERE id = $4
|
|
258
|
+
`, [stepId, status, durationMs, recordId]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async completeRecord(recordId, execResult) {
|
|
262
|
+
const status = this.mapExecutorStatus(execResult.status);
|
|
263
|
+
const errorMessage = execResult.errors?.length > 0
|
|
264
|
+
? execResult.errors.map(e => `[${e.code || 'ERR'}] ${e.message}`).join('\n')
|
|
265
|
+
: null;
|
|
266
|
+
|
|
267
|
+
await this.pool.query(`
|
|
268
|
+
UPDATE migration_history
|
|
269
|
+
SET status = $1,
|
|
270
|
+
execution_time_ms = $2,
|
|
271
|
+
error_message = $3,
|
|
272
|
+
execution_results = $4,
|
|
273
|
+
snapshot_before = $5,
|
|
274
|
+
snapshot_after = $6,
|
|
275
|
+
rollback_sql = $7,
|
|
276
|
+
applied_at = CASE WHEN $1 IN ('completed', 'partially_applied') THEN now() ELSE applied_at END
|
|
277
|
+
WHERE id = $8
|
|
278
|
+
`, [
|
|
279
|
+
status,
|
|
280
|
+
execResult.durationMs || execResult.duration,
|
|
281
|
+
errorMessage,
|
|
282
|
+
JSON.stringify(execResult.intents || execResult.executionResults || {}),
|
|
283
|
+
JSON.stringify(execResult.snapshotBefore || execResult.snapshots?.before || null),
|
|
284
|
+
JSON.stringify(execResult.snapshotAfter || execResult.snapshots?.after || null),
|
|
285
|
+
JSON.stringify(execResult.rollbackSteps || []),
|
|
286
|
+
recordId,
|
|
287
|
+
]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async failRecord(recordId, error, executedSteps = []) {
|
|
291
|
+
const errorMessage = error.message || String(error);
|
|
292
|
+
const pgError = error.code ? `[${error.code}] ` : '';
|
|
293
|
+
|
|
294
|
+
await this.pool.query(`
|
|
295
|
+
UPDATE migration_history
|
|
296
|
+
SET status = 'failed',
|
|
297
|
+
error_message = $1,
|
|
298
|
+
execution_results = $2
|
|
299
|
+
WHERE id = $3
|
|
300
|
+
`, [
|
|
301
|
+
pgError + errorMessage,
|
|
302
|
+
JSON.stringify({ executedSteps, error: { message: errorMessage, code: error.code } }),
|
|
303
|
+
recordId,
|
|
304
|
+
]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async markRolledBack(recordId, rolledBackBy = null) {
|
|
308
|
+
await this.pool.query(`
|
|
309
|
+
UPDATE migration_history
|
|
310
|
+
SET status = 'rolled_back',
|
|
311
|
+
rolled_back_at = now(),
|
|
312
|
+
rolled_back_by = $1
|
|
313
|
+
WHERE id = $2
|
|
314
|
+
`, [rolledBackBy, recordId]);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async getHistory(connectionId = null, limit = 50, offset = 0) {
|
|
318
|
+
const cid = connectionId || this.connectionId;
|
|
319
|
+
const result = await this.pool.query(`
|
|
320
|
+
SELECT id, version, name, status, direction,
|
|
321
|
+
change_count, create_count, alter_count, drop_count, rename_count,
|
|
322
|
+
execution_time_ms, error_message,
|
|
323
|
+
applied_at, created_at, rolled_back_at
|
|
324
|
+
FROM migration_history
|
|
325
|
+
WHERE connection_id = $1
|
|
326
|
+
ORDER BY applied_at DESC NULLS LAST
|
|
327
|
+
LIMIT $2 OFFSET $3
|
|
328
|
+
`, [cid, limit, offset]);
|
|
329
|
+
return result.rows;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async getLastMigration(connectionId = null) {
|
|
333
|
+
const cid = connectionId || this.connectionId;
|
|
334
|
+
const result = await this.pool.query(`
|
|
335
|
+
SELECT id, version, name, status, checksum, applied_at
|
|
336
|
+
FROM migration_history
|
|
337
|
+
WHERE connection_id = $1 AND status = 'completed'
|
|
338
|
+
ORDER BY applied_at DESC
|
|
339
|
+
LIMIT 1
|
|
340
|
+
`, [cid]);
|
|
341
|
+
return result.rows[0] || null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async getMigration(recordId) {
|
|
345
|
+
const result = await this.pool.query(`
|
|
346
|
+
SELECT * FROM migration_history WHERE id = $1
|
|
347
|
+
`, [recordId]);
|
|
348
|
+
return result.rows[0] || null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async getMigrationByVersion(connectionId = null, version) {
|
|
352
|
+
const cid = connectionId || this.connectionId;
|
|
353
|
+
const result = await this.pool.query(`
|
|
354
|
+
SELECT * FROM migration_history
|
|
355
|
+
WHERE connection_id = $1 AND version = $2
|
|
356
|
+
`, [cid, version]);
|
|
357
|
+
return result.rows[0] || null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async getByStatus(connectionId = null, status) {
|
|
361
|
+
const cid = connectionId || this.connectionId;
|
|
362
|
+
const result = await this.pool.query(`
|
|
363
|
+
SELECT * FROM migration_history
|
|
364
|
+
WHERE connection_id = $1 AND status = $2
|
|
365
|
+
ORDER BY created_at DESC
|
|
366
|
+
`, [cid, status]);
|
|
367
|
+
return result.rows;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async getRollbackSQL(recordId) {
|
|
371
|
+
const result = await this.pool.query(`
|
|
372
|
+
SELECT
|
|
373
|
+
id, name, schema_diff,
|
|
374
|
+
rollback_sql, sql_statements,
|
|
375
|
+
status, applied_at
|
|
376
|
+
FROM migration_history
|
|
377
|
+
WHERE id = $1
|
|
378
|
+
`, [recordId]);
|
|
379
|
+
return result.rows[0] || null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async setRollbackSQL(recordId, rollbackSteps) {
|
|
383
|
+
await this.pool.query(`
|
|
384
|
+
UPDATE migration_history
|
|
385
|
+
SET rollback_sql = $1
|
|
386
|
+
WHERE id = $2
|
|
387
|
+
`, [JSON.stringify(rollbackSteps), recordId]);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async getStats(connectionId = null) {
|
|
391
|
+
const cid = connectionId || this.connectionId;
|
|
392
|
+
const result = await this.pool.query(`
|
|
393
|
+
SELECT
|
|
394
|
+
COUNT(*) as total_migrations,
|
|
395
|
+
COUNT(*) FILTER (WHERE status = 'completed') as completed,
|
|
396
|
+
COUNT(*) FILTER (WHERE status = 'failed') as failed,
|
|
397
|
+
COUNT(*) FILTER (WHERE status = 'rolled_back') as rolled_back,
|
|
398
|
+
COUNT(*) FILTER (WHERE status = 'running') as running,
|
|
399
|
+
COUNT(*) FILTER (WHERE status = 'partially_applied') as partially_applied,
|
|
400
|
+
COALESCE(SUM(change_count), 0) as total_changes,
|
|
401
|
+
COALESCE(SUM(execution_time_ms), 0) as total_execution_time_ms,
|
|
402
|
+
MAX(applied_at) as last_migration_at
|
|
403
|
+
FROM migration_history
|
|
404
|
+
WHERE connection_id = $1
|
|
405
|
+
`, [cid]);
|
|
406
|
+
return result.rows[0];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async cleanupOldRecords(keepCount = 100, connectionId = null) {
|
|
410
|
+
const cid = connectionId || this.connectionId;
|
|
411
|
+
const result = await this.pool.query(`
|
|
412
|
+
DELETE FROM migration_history
|
|
413
|
+
WHERE id NOT IN (
|
|
414
|
+
SELECT id FROM migration_history
|
|
415
|
+
WHERE connection_id = $1
|
|
416
|
+
ORDER BY applied_at DESC NULLS LAST
|
|
417
|
+
LIMIT $2
|
|
418
|
+
) AND connection_id = $1
|
|
419
|
+
RETURNING id
|
|
420
|
+
`, [cid, keepCount]);
|
|
421
|
+
return result.rowCount;
|
|
422
|
+
}
|
|
423
|
+
}
|