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,81 @@
|
|
|
1
|
+
export class ProgressTracker {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.progress = new Map();
|
|
4
|
+
this.listeners = new Set();
|
|
5
|
+
this.events = [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} stepId
|
|
10
|
+
* @param {'PENDING'|'RUNNING'|'COMPLETED'|'FAILED'} status
|
|
11
|
+
* @param {string} [message]
|
|
12
|
+
*/
|
|
13
|
+
update(stepId, status, message) {
|
|
14
|
+
this.progress.set(stepId, { status, message, timestamp: Date.now() });
|
|
15
|
+
for (const listener of this.listeners) {
|
|
16
|
+
listener({ stepId, status, message, timestamp: Date.now() });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Emit a progress event
|
|
22
|
+
* @param {Object} event
|
|
23
|
+
*/
|
|
24
|
+
emit(event) {
|
|
25
|
+
this.events.push({
|
|
26
|
+
...event,
|
|
27
|
+
timestamp: event.timestamp || Date.now(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
for (const listener of this.listeners) {
|
|
31
|
+
try {
|
|
32
|
+
listener(event);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// Don't let listener errors break execution
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Subscribe to progress events
|
|
41
|
+
* @param {Function} listener
|
|
42
|
+
* @returns {Function} Unsubscribe function
|
|
43
|
+
*/
|
|
44
|
+
subscribe(listener) {
|
|
45
|
+
this.listeners.add(listener);
|
|
46
|
+
return () => this.listeners.delete(listener);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get progress for a step
|
|
51
|
+
* @param {string} stepId
|
|
52
|
+
* @returns {Object}
|
|
53
|
+
*/
|
|
54
|
+
get(stepId) {
|
|
55
|
+
return this.progress.get(stepId) || { status: 'PENDING' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get all progress
|
|
60
|
+
* @returns {Object}
|
|
61
|
+
*/
|
|
62
|
+
getAll() {
|
|
63
|
+
return Object.fromEntries(this.progress);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get all events
|
|
68
|
+
* @returns {Array}
|
|
69
|
+
*/
|
|
70
|
+
getEvents() {
|
|
71
|
+
return this.events;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Clear all progress and events
|
|
76
|
+
*/
|
|
77
|
+
clear() {
|
|
78
|
+
this.progress.clear();
|
|
79
|
+
this.events = [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export class RecoveryManager {
|
|
2
|
+
/** @param {import('../types/execution.js').StorageProvider} storage */
|
|
3
|
+
constructor(storage) {
|
|
4
|
+
this.storage = storage;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} migrationId
|
|
9
|
+
* @returns {Promise<import('../types/execution.js').RecoveryResult>}
|
|
10
|
+
*/
|
|
11
|
+
async recover(migrationId) {
|
|
12
|
+
const migration = await this.storage.getMigration(migrationId);
|
|
13
|
+
if (!migration) {
|
|
14
|
+
return { action: 'UNKNOWN', message: 'Migration not found' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (migration.status === 'COMPLETED') {
|
|
18
|
+
return { action: 'NONE', message: 'Migration already completed' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (migration.status === 'FAILED') {
|
|
22
|
+
return { action: 'RETRY', message: 'Database is consistent, migration can be retried' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (migration.status === 'RUNNING') {
|
|
26
|
+
return { action: 'WAIT', message: 'Migration is still running' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (migration.status === 'STALE') {
|
|
30
|
+
return { action: 'STALE_CLEANUP', message: 'Migration process died, cleaning up' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { action: 'UNKNOWN', message: `Unknown migration status: ${migration.status}` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} migrationId
|
|
38
|
+
* @returns {Promise<void>}
|
|
39
|
+
*/
|
|
40
|
+
async markFailed(migrationId) {
|
|
41
|
+
const migration = await this.storage.getMigration(migrationId);
|
|
42
|
+
if (migration) {
|
|
43
|
+
migration.status = 'FAILED';
|
|
44
|
+
await this.storage.saveMigration(migration);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class SnapshotManager {
|
|
2
|
+
/**
|
|
3
|
+
* @param {import('../introspection/index.js').SchemaIntrospector} introspector
|
|
4
|
+
* @param {import('../types/execution.js').StorageProvider} storage
|
|
5
|
+
*/
|
|
6
|
+
constructor(introspector, storage) {
|
|
7
|
+
this.introspector = introspector;
|
|
8
|
+
this.storage = storage;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** @returns {Promise<string>} */
|
|
12
|
+
async capture() {
|
|
13
|
+
const schema = await this.introspector.introspect();
|
|
14
|
+
await this.storage.saveSnapshot(schema.checksum, schema);
|
|
15
|
+
return schema.checksum;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @param {string} checksum */
|
|
19
|
+
async restore(checksum) {
|
|
20
|
+
return this.storage.loadSnapshot(checksum);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const DOLLAR_QUOTE_REGEX = /\$([a-zA-Z_][a-zA-Z0-9_]*)?\$/g;
|
|
2
|
+
|
|
3
|
+
export function splitSqlStatements(sql) {
|
|
4
|
+
if (!sql || typeof sql !== 'string') return [];
|
|
5
|
+
|
|
6
|
+
const statements = [];
|
|
7
|
+
let current = '';
|
|
8
|
+
let i = 0;
|
|
9
|
+
|
|
10
|
+
while (i < sql.length) {
|
|
11
|
+
if (sql[i] === '-' && i + 1 < sql.length && sql[i + 1] === '-') {
|
|
12
|
+
current += sql[i];
|
|
13
|
+
i++;
|
|
14
|
+
while (i < sql.length && sql[i] !== '\n') {
|
|
15
|
+
current += sql[i];
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
if (i < sql.length) {
|
|
19
|
+
current += sql[i];
|
|
20
|
+
i++;
|
|
21
|
+
}
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (sql[i] === '/' && i + 1 < sql.length && sql[i + 1] === '*') {
|
|
26
|
+
current += sql[i];
|
|
27
|
+
i++;
|
|
28
|
+
current += sql[i];
|
|
29
|
+
i++;
|
|
30
|
+
while (i < sql.length && !(sql[i - 1] === '*' && sql[i] === '/')) {
|
|
31
|
+
current += sql[i];
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
if (i < sql.length) {
|
|
35
|
+
current += sql[i];
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (sql[i] === '$') {
|
|
42
|
+
const dollarMatch = sql.substring(i).match(DOLLAR_QUOTE_REGEX);
|
|
43
|
+
if (dollarMatch) {
|
|
44
|
+
const tag = dollarMatch[0];
|
|
45
|
+
current += tag;
|
|
46
|
+
i += tag.length;
|
|
47
|
+
|
|
48
|
+
const endPattern = tag;
|
|
49
|
+
while (i < sql.length) {
|
|
50
|
+
if (sql.substring(i, i + tag.length) === endPattern) {
|
|
51
|
+
current += tag;
|
|
52
|
+
i += tag.length;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
current += sql[i];
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (sql[i] === "'") {
|
|
63
|
+
current += sql[i];
|
|
64
|
+
i++;
|
|
65
|
+
while (i < sql.length) {
|
|
66
|
+
if (sql[i] === "'") {
|
|
67
|
+
current += sql[i];
|
|
68
|
+
i++;
|
|
69
|
+
if (i < sql.length && sql[i] === "'") {
|
|
70
|
+
current += sql[i];
|
|
71
|
+
i++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
current += sql[i];
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (sql[i] === '"') {
|
|
83
|
+
current += sql[i];
|
|
84
|
+
i++;
|
|
85
|
+
while (i < sql.length && sql[i] !== '"') {
|
|
86
|
+
current += sql[i];
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
if (i < sql.length) {
|
|
90
|
+
current += sql[i];
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (sql[i] === ';') {
|
|
97
|
+
const trimmed = current.trim();
|
|
98
|
+
if (trimmed.length > 0) {
|
|
99
|
+
statements.push(trimmed);
|
|
100
|
+
}
|
|
101
|
+
current = '';
|
|
102
|
+
i++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
current += sql[i];
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const trimmed = current.trim();
|
|
111
|
+
if (trimmed.length > 0) {
|
|
112
|
+
statements.push(trimmed);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return statements;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function sanitizeSavepointName(str) {
|
|
119
|
+
return str.replace(/[^a-zA-Z0-9_]/g, '_').substring(0, 63);
|
|
120
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction Manager - Handles transaction execution with proper error handling
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class TransactionManager {
|
|
6
|
+
/**
|
|
7
|
+
* @param {import('pg').Pool} pool
|
|
8
|
+
*/
|
|
9
|
+
constructor(pool) {
|
|
10
|
+
this.pool = pool;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute steps in a transaction
|
|
15
|
+
* @param {Array} steps - Steps to execute
|
|
16
|
+
* @param {Object} options - { lockTimeout, statementTimeout, dryRun, continueOnError }
|
|
17
|
+
* @returns {Promise<Array>} Results of each step
|
|
18
|
+
*/
|
|
19
|
+
async executeTransactional(steps, options = {}) {
|
|
20
|
+
const client = await this.pool.connect();
|
|
21
|
+
const results = [];
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await client.query('BEGIN');
|
|
25
|
+
|
|
26
|
+
if (options.lockTimeout) {
|
|
27
|
+
await client.query(`SET LOCAL lock_timeout = '${options.lockTimeout}'`);
|
|
28
|
+
}
|
|
29
|
+
if (options.statementTimeout) {
|
|
30
|
+
await client.query(`SET LOCAL statement_timeout = '${options.statementTimeout}'`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await client.query(`SET LOCAL search_path = 'public'`);
|
|
34
|
+
|
|
35
|
+
for (const step of steps) {
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
try {
|
|
38
|
+
const result = await client.query(step.sql);
|
|
39
|
+
results.push({
|
|
40
|
+
stepId: step.id,
|
|
41
|
+
success: true,
|
|
42
|
+
duration: Date.now() - startTime,
|
|
43
|
+
rowCount: result.rowCount,
|
|
44
|
+
rows: result.rows,
|
|
45
|
+
});
|
|
46
|
+
} catch (error) {
|
|
47
|
+
results.push({
|
|
48
|
+
stepId: step.id,
|
|
49
|
+
success: false,
|
|
50
|
+
duration: Date.now() - startTime,
|
|
51
|
+
error: error.message,
|
|
52
|
+
code: error.code,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!options.continueOnError) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (options.dryRun) {
|
|
62
|
+
await client.query('ROLLBACK');
|
|
63
|
+
} else {
|
|
64
|
+
await client.query('COMMIT');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return results;
|
|
68
|
+
|
|
69
|
+
} catch (error) {
|
|
70
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
71
|
+
throw error;
|
|
72
|
+
} finally {
|
|
73
|
+
client.release();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Execute a single non-transactional step
|
|
79
|
+
* @param {Object} step - The step to execute
|
|
80
|
+
* @param {Object} options - { statementTimeout }
|
|
81
|
+
* @returns {Promise<Object>}
|
|
82
|
+
*/
|
|
83
|
+
async executeNonTransactional(step, options = {}) {
|
|
84
|
+
const client = await this.pool.connect();
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (options.statementTimeout) {
|
|
88
|
+
await client.query(`SET statement_timeout = '${options.statementTimeout}'`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const startTime = Date.now();
|
|
92
|
+
const result = await client.query(step.sql);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
stepId: step.id,
|
|
96
|
+
success: true,
|
|
97
|
+
duration: Date.now() - startTime,
|
|
98
|
+
rowCount: result.rowCount,
|
|
99
|
+
isTransactional: false,
|
|
100
|
+
warning: 'This step was executed outside a transaction and cannot be automatically rolled back',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
stepId: step.id,
|
|
106
|
+
success: false,
|
|
107
|
+
error: error.message,
|
|
108
|
+
code: error.code,
|
|
109
|
+
isTransactional: false,
|
|
110
|
+
requiresManualRecovery: true,
|
|
111
|
+
};
|
|
112
|
+
} finally {
|
|
113
|
+
client.release();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Execute steps with validation first, then for real
|
|
119
|
+
* @param {Array} steps
|
|
120
|
+
* @param {Object} options
|
|
121
|
+
* @returns {Promise<Object>}
|
|
122
|
+
*/
|
|
123
|
+
async executeWithValidation(steps, options = {}) {
|
|
124
|
+
const validationResult = await this.executeTransactional(steps, {
|
|
125
|
+
...options,
|
|
126
|
+
dryRun: true,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const hasErrors = validationResult.some(r => !r.success);
|
|
130
|
+
if (hasErrors) {
|
|
131
|
+
return {
|
|
132
|
+
validated: false,
|
|
133
|
+
validationResults: validationResult,
|
|
134
|
+
message: 'Validation failed. Migration not applied.',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!options.dryRun) {
|
|
139
|
+
const executionResult = await this.executeTransactional(steps, {
|
|
140
|
+
...options,
|
|
141
|
+
dryRun: false,
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
validated: true,
|
|
145
|
+
executionResults: executionResult,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
validated: true,
|
|
151
|
+
validationResults: validationResult,
|
|
152
|
+
message: 'Validation passed (dry run). No changes applied.',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Execute with savepoint support for partial rollback
|
|
158
|
+
* @param {Array} steps
|
|
159
|
+
* @param {Object} options
|
|
160
|
+
* @returns {Promise<Object>}
|
|
161
|
+
*/
|
|
162
|
+
async executeWithSavepoints(steps, options = {}) {
|
|
163
|
+
const client = await this.pool.connect();
|
|
164
|
+
const results = [];
|
|
165
|
+
let lastSuccessfulSavepoint = 0;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await client.query('BEGIN');
|
|
169
|
+
|
|
170
|
+
if (options.lockTimeout) {
|
|
171
|
+
await client.query(`SET LOCAL lock_timeout = '${options.lockTimeout}'`);
|
|
172
|
+
}
|
|
173
|
+
if (options.statementTimeout) {
|
|
174
|
+
await client.query(`SET LOCAL statement_timeout = '${options.statementTimeout}'`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (let i = 0; i < steps.length; i++) {
|
|
178
|
+
const step = steps[i];
|
|
179
|
+
const savepointName = `sp_${i}`;
|
|
180
|
+
|
|
181
|
+
await client.query(`SAVEPOINT ${savepointName}`);
|
|
182
|
+
|
|
183
|
+
const startTime = Date.now();
|
|
184
|
+
try {
|
|
185
|
+
const result = await client.query(step.sql);
|
|
186
|
+
results.push({
|
|
187
|
+
stepId: step.id,
|
|
188
|
+
success: true,
|
|
189
|
+
duration: Date.now() - startTime,
|
|
190
|
+
rowCount: result.rowCount,
|
|
191
|
+
savepoint: savepointName,
|
|
192
|
+
});
|
|
193
|
+
lastSuccessfulSavepoint = i;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
await client.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
|
|
196
|
+
|
|
197
|
+
results.push({
|
|
198
|
+
stepId: step.id,
|
|
199
|
+
success: false,
|
|
200
|
+
duration: Date.now() - startTime,
|
|
201
|
+
error: error.message,
|
|
202
|
+
code: error.code,
|
|
203
|
+
savepoint: savepointName,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (!options.continueOnError) {
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (options.dryRun) {
|
|
213
|
+
await client.query('ROLLBACK');
|
|
214
|
+
} else {
|
|
215
|
+
await client.query('COMMIT');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
success: true,
|
|
220
|
+
results,
|
|
221
|
+
lastSuccessfulSavepoint,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
} catch (error) {
|
|
225
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
results,
|
|
229
|
+
lastSuccessfulSavepoint,
|
|
230
|
+
error: error.message,
|
|
231
|
+
};
|
|
232
|
+
} finally {
|
|
233
|
+
client.release();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Execute with retry on deadlock
|
|
239
|
+
* @param {Array} steps
|
|
240
|
+
* @param {Object} options
|
|
241
|
+
* @returns {Promise<Object>}
|
|
242
|
+
*/
|
|
243
|
+
async executeWithRetry(steps, options = {}) {
|
|
244
|
+
const maxRetries = options.maxRetries || 3;
|
|
245
|
+
const retryDelay = options.retryDelay || 1000;
|
|
246
|
+
let lastError;
|
|
247
|
+
|
|
248
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
249
|
+
try {
|
|
250
|
+
const result = await this.executeTransactional(steps, options);
|
|
251
|
+
return {
|
|
252
|
+
success: true,
|
|
253
|
+
results: result,
|
|
254
|
+
attempts: attempt,
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
lastError = error;
|
|
258
|
+
|
|
259
|
+
const isDeadlock = error.code === '40P01';
|
|
260
|
+
const isLockNotAvailable = error.code === '55P03';
|
|
261
|
+
const isSerializationFailure = error.code === '40001';
|
|
262
|
+
|
|
263
|
+
if (!isDeadlock && !isLockNotAvailable && !isSerializationFailure) {
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (attempt < maxRetries) {
|
|
268
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay * attempt));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
success: false,
|
|
275
|
+
error: lastError?.message,
|
|
276
|
+
attempts: maxRetries,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Dry run a single step
|
|
282
|
+
* @param {Object} step
|
|
283
|
+
* @returns {Promise<Object>}
|
|
284
|
+
*/
|
|
285
|
+
async dryRun(step) {
|
|
286
|
+
return this.executeTransactional([step], { dryRun: true });
|
|
287
|
+
}
|
|
288
|
+
}
|