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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift Detector - Detect schema drift during migration execution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class DriftDetector {
|
|
6
|
+
/**
|
|
7
|
+
* Compare before/after snapshots to detect drift
|
|
8
|
+
* @param {Object} snapshotBefore - Snapshot before migration
|
|
9
|
+
* @param {Object} snapshotAfter - Snapshot after migration
|
|
10
|
+
* @param {Object} expectedDiff - The diff we applied
|
|
11
|
+
* @returns {Object} Drift report
|
|
12
|
+
*/
|
|
13
|
+
detect(snapshotBefore, snapshotAfter, expectedDiff) {
|
|
14
|
+
const drift = {
|
|
15
|
+
detected: false,
|
|
16
|
+
unexpectedChanges: [],
|
|
17
|
+
missingChanges: [],
|
|
18
|
+
extraChanges: [],
|
|
19
|
+
summary: {
|
|
20
|
+
objectsModified: 0,
|
|
21
|
+
objectsCreated: 0,
|
|
22
|
+
objectsDropped: 0,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (!snapshotBefore?.checksums || !snapshotAfter?.checksums) {
|
|
27
|
+
return drift;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const beforeMap = new Map(
|
|
31
|
+
snapshotBefore.checksums.map(c => [`${c.schema}.${c.name}.${c.kind}`, c.checksum])
|
|
32
|
+
);
|
|
33
|
+
const afterMap = new Map(
|
|
34
|
+
snapshotAfter.checksums.map(c => [`${c.schema}.${c.name}.${c.kind}`, c.checksum])
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const ourPaths = new Set(
|
|
38
|
+
(expectedDiff.changes || []).map(c => c.objectKey || c.path)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
for (const [key, afterChecksum] of afterMap) {
|
|
42
|
+
const beforeChecksum = beforeMap.get(key);
|
|
43
|
+
|
|
44
|
+
if (beforeChecksum && beforeChecksum !== afterChecksum) {
|
|
45
|
+
if (!ourPaths.has(key)) {
|
|
46
|
+
drift.detected = true;
|
|
47
|
+
drift.unexpectedChanges.push({
|
|
48
|
+
path: key,
|
|
49
|
+
type: 'modified',
|
|
50
|
+
message: `Schema object ${key} was modified by another process during migration`,
|
|
51
|
+
});
|
|
52
|
+
drift.summary.objectsModified++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const [key] of beforeMap) {
|
|
58
|
+
if (!afterMap.has(key)) {
|
|
59
|
+
const wasDropped = (expectedDiff.changes || []).some(
|
|
60
|
+
c => (c.objectKey === key || c.path === key) && c.changeType?.startsWith('DROP')
|
|
61
|
+
);
|
|
62
|
+
if (!wasDropped) {
|
|
63
|
+
drift.detected = true;
|
|
64
|
+
drift.unexpectedChanges.push({
|
|
65
|
+
path: key,
|
|
66
|
+
type: 'dropped',
|
|
67
|
+
message: `Schema object ${key} was dropped by another process during migration`,
|
|
68
|
+
});
|
|
69
|
+
drift.summary.objectsDropped++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const [key] of afterMap) {
|
|
75
|
+
if (!beforeMap.has(key)) {
|
|
76
|
+
const wasCreated = (expectedDiff.changes || []).some(
|
|
77
|
+
c => (c.objectKey === key || c.path === key) && c.changeType?.startsWith('CREATE')
|
|
78
|
+
);
|
|
79
|
+
if (!wasCreated) {
|
|
80
|
+
drift.detected = true;
|
|
81
|
+
drift.unexpectedChanges.push({
|
|
82
|
+
path: key,
|
|
83
|
+
type: 'created',
|
|
84
|
+
message: `Schema object ${key} was created by another process during migration`,
|
|
85
|
+
});
|
|
86
|
+
drift.summary.objectsCreated++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const expectedCreates = (expectedDiff.changes || []).filter(
|
|
92
|
+
c => c.changeType?.startsWith('CREATE')
|
|
93
|
+
);
|
|
94
|
+
for (const expected of expectedCreates) {
|
|
95
|
+
const key = expected.objectKey || expected.path;
|
|
96
|
+
if (!afterMap.has(key)) {
|
|
97
|
+
drift.missingChanges.push({
|
|
98
|
+
path: key,
|
|
99
|
+
expectedType: 'CREATE',
|
|
100
|
+
message: `Expected object ${key} was not created`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return drift;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Compare two schema snapshots for column-level drift
|
|
110
|
+
* @param {Object} snapshotBefore
|
|
111
|
+
* @param {Object} snapshotAfter
|
|
112
|
+
* @param {string} tableName - Table to check
|
|
113
|
+
* @returns {Object} Column-level drift report
|
|
114
|
+
*/
|
|
115
|
+
detectColumnDrift(snapshotBefore, snapshotAfter, tableName) {
|
|
116
|
+
const drift = {
|
|
117
|
+
tableName,
|
|
118
|
+
columnsAdded: [],
|
|
119
|
+
columnsDropped: [],
|
|
120
|
+
columnsModified: [],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const beforeCols = this.getTableColumns(snapshotBefore, tableName);
|
|
124
|
+
const afterCols = this.getTableColumns(snapshotAfter, tableName);
|
|
125
|
+
|
|
126
|
+
for (const [colName, afterCol] of Object.entries(afterCols)) {
|
|
127
|
+
if (!beforeCols[colName]) {
|
|
128
|
+
drift.columnsAdded.push(colName);
|
|
129
|
+
} else if (JSON.stringify(beforeCols[colName]) !== JSON.stringify(afterCol)) {
|
|
130
|
+
drift.columnsModified.push({
|
|
131
|
+
name: colName,
|
|
132
|
+
before: beforeCols[colName],
|
|
133
|
+
after: afterCol,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const colName of Object.keys(beforeCols)) {
|
|
139
|
+
if (!afterCols[colName]) {
|
|
140
|
+
drift.columnsDropped.push(colName);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
drift.hasDrift = drift.columnsAdded.length > 0 ||
|
|
145
|
+
drift.columnsDropped.length > 0 ||
|
|
146
|
+
drift.columnsModified.length > 0;
|
|
147
|
+
|
|
148
|
+
return drift;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get columns for a table from snapshot
|
|
153
|
+
* @param {Object} snapshot
|
|
154
|
+
* @param {string} tableName
|
|
155
|
+
* @returns {Object}
|
|
156
|
+
*/
|
|
157
|
+
getTableColumns(snapshot, tableName) {
|
|
158
|
+
if (!snapshot?.schemas) return {};
|
|
159
|
+
|
|
160
|
+
const parts = tableName.split('.');
|
|
161
|
+
const schemaName = parts.length > 1 ? parts[0] : 'public';
|
|
162
|
+
const table = parts.length > 1 ? parts[1] : parts[0];
|
|
163
|
+
|
|
164
|
+
const schema = snapshot.schemas[schemaName];
|
|
165
|
+
if (!schema?.tables) return {};
|
|
166
|
+
|
|
167
|
+
const tableObj = schema.tables.find(t => t.name === table);
|
|
168
|
+
if (!tableObj?.columns) return {};
|
|
169
|
+
|
|
170
|
+
const cols = {};
|
|
171
|
+
for (const col of tableObj.columns) {
|
|
172
|
+
cols[col.name] = {
|
|
173
|
+
dataType: col.dataType,
|
|
174
|
+
isNullable: col.isNullable,
|
|
175
|
+
defaultValue: col.defaultValue,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return cols;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Quick drift check using pg_stat_* system view activity
|
|
183
|
+
* @param {import('pg').Pool} pool
|
|
184
|
+
* @returns {Promise<Object>}
|
|
185
|
+
*/
|
|
186
|
+
async quickDriftCheck(pool) {
|
|
187
|
+
const client = await pool.connect();
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const activityQuery = `
|
|
191
|
+
SELECT DISTINCT
|
|
192
|
+
query,
|
|
193
|
+
state,
|
|
194
|
+
query_start,
|
|
195
|
+
now() - query_start AS duration
|
|
196
|
+
FROM pg_stat_activity
|
|
197
|
+
WHERE state = 'active'
|
|
198
|
+
AND query NOT LIKE 'pg_stat_activity%'
|
|
199
|
+
AND pid != pg_backend_pid()
|
|
200
|
+
AND now() - query_start < interval '5 minutes'
|
|
201
|
+
ORDER BY query_start
|
|
202
|
+
`;
|
|
203
|
+
|
|
204
|
+
const activity = await client.query(activityQuery);
|
|
205
|
+
|
|
206
|
+
const ddlActivity = activity.rows.filter(r =>
|
|
207
|
+
r.query.toUpperCase().match(/^(CREATE|ALTER|DROP|TRUNCATE)\s/)
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
hasDDLActivity: ddlActivity.length > 0,
|
|
212
|
+
activityCount: activity.rows.length,
|
|
213
|
+
ddlActivityCount: ddlActivity.length,
|
|
214
|
+
ddlQueries: ddlActivity,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
} finally {
|
|
218
|
+
client.release();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { MigrationExecutor } from './migration-executor.js';
|
|
2
|
+
export { TransactionManager } from './transaction-manager.js';
|
|
3
|
+
export { LockManager } from './lock-manager.js';
|
|
4
|
+
export { ProgressTracker } from './progress-tracker.js';
|
|
5
|
+
export { SnapshotManager } from './snapshot-manager.js';
|
|
6
|
+
export { DriftDetector } from './drift-detector.js';
|
|
7
|
+
export { RecoveryManager } from './recovery-manager.js';
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
export class LockManager {
|
|
4
|
+
isLocked = false;
|
|
5
|
+
lockId = null;
|
|
6
|
+
connectionId = null;
|
|
7
|
+
_heartbeatTimer = null;
|
|
8
|
+
_heartbeatClient = null;
|
|
9
|
+
|
|
10
|
+
constructor(pool, options = {}) {
|
|
11
|
+
this.pool = pool;
|
|
12
|
+
this.locks = new Map();
|
|
13
|
+
this.connectionId = options.connectionId || null;
|
|
14
|
+
this.lockId = this.computeLockKey(this.connectionId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
computeLockKey(connectionId) {
|
|
18
|
+
if (!connectionId) {
|
|
19
|
+
console.warn(
|
|
20
|
+
'[SchemaWeaver] No connectionId for lock key computation. ' +
|
|
21
|
+
'Using default lock key. This may cause cross-database lock conflicts.'
|
|
22
|
+
);
|
|
23
|
+
return 12345;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const hash = crypto.createHash('sha256')
|
|
27
|
+
.update(`schema-weaver-lock:${connectionId}`)
|
|
28
|
+
.digest();
|
|
29
|
+
|
|
30
|
+
const lockKey = hash.readInt32BE(0);
|
|
31
|
+
|
|
32
|
+
return Math.abs(lockKey) || 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async acquire(lockKey, timeout) {
|
|
36
|
+
const effectiveKey = lockKey || this.lockId;
|
|
37
|
+
const client = await this.pool.connect();
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
if (timeout) {
|
|
41
|
+
await client.query(`SET lock_timeout = '${timeout}'`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await client.query(
|
|
45
|
+
'SELECT pg_try_advisory_lock($1) as acquired',
|
|
46
|
+
[effectiveKey]
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const acquired = result.rows[0].acquired;
|
|
50
|
+
|
|
51
|
+
if (acquired) {
|
|
52
|
+
this.isLocked = true;
|
|
53
|
+
this.lockId = effectiveKey;
|
|
54
|
+
this.locks.set(effectiveKey, {
|
|
55
|
+
acquiredAt: new Date().toISOString(),
|
|
56
|
+
client,
|
|
57
|
+
key: effectiveKey,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return acquired;
|
|
62
|
+
|
|
63
|
+
} catch (error) {
|
|
64
|
+
client.release();
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async acquireAndWait(lockKey, timeout) {
|
|
70
|
+
const effectiveKey = lockKey || this.lockId;
|
|
71
|
+
const client = await this.pool.connect();
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
if (timeout) {
|
|
75
|
+
await client.query(`SET lock_timeout = '${timeout}'`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
|
|
80
|
+
await client.query('SELECT pg_advisory_lock($1)', [effectiveKey]);
|
|
81
|
+
|
|
82
|
+
this.isLocked = true;
|
|
83
|
+
this.lockId = effectiveKey;
|
|
84
|
+
this.locks.set(effectiveKey, {
|
|
85
|
+
acquiredAt: new Date().toISOString(),
|
|
86
|
+
client,
|
|
87
|
+
key: effectiveKey,
|
|
88
|
+
waitTime: Date.now() - startTime,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return true;
|
|
92
|
+
|
|
93
|
+
} catch (error) {
|
|
94
|
+
client.release();
|
|
95
|
+
|
|
96
|
+
if (error.code === '55P03') {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async release(lockKey) {
|
|
104
|
+
const effectiveKey = lockKey || this.lockId;
|
|
105
|
+
const lockInfo = this.locks.get(effectiveKey);
|
|
106
|
+
|
|
107
|
+
this.stopHeartbeat();
|
|
108
|
+
|
|
109
|
+
if (lockInfo) {
|
|
110
|
+
const { client } = lockInfo;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await client.query('SELECT pg_advisory_unlock($1)', [effectiveKey]);
|
|
114
|
+
this.locks.delete(effectiveKey);
|
|
115
|
+
this.isLocked = false;
|
|
116
|
+
this.lockId = null;
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
this.isLocked = false;
|
|
120
|
+
this.lockId = null;
|
|
121
|
+
return false;
|
|
122
|
+
} finally {
|
|
123
|
+
client.release();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const standaloneClient = await this.pool.connect();
|
|
128
|
+
try {
|
|
129
|
+
await standaloneClient.query('SELECT pg_advisory_unlock($1)', [effectiveKey]);
|
|
130
|
+
this.locks.delete(effectiveKey);
|
|
131
|
+
this.isLocked = false;
|
|
132
|
+
this.lockId = null;
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
this.isLocked = false;
|
|
136
|
+
this.lockId = null;
|
|
137
|
+
return false;
|
|
138
|
+
} finally {
|
|
139
|
+
standaloneClient.release();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async isLocked(lockKey) {
|
|
144
|
+
const effectiveKey = lockKey || this.lockId;
|
|
145
|
+
const selfHeld = this.locks.has(effectiveKey);
|
|
146
|
+
|
|
147
|
+
if (selfHeld) {
|
|
148
|
+
return { held: true, heldBySelf: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const client = await this.pool.connect();
|
|
152
|
+
try {
|
|
153
|
+
const result = await client.query(
|
|
154
|
+
'SELECT pg_try_advisory_lock($1) as acquired',
|
|
155
|
+
[effectiveKey]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const acquired = result.rows[0].acquired;
|
|
159
|
+
|
|
160
|
+
if (acquired) {
|
|
161
|
+
await client.query('SELECT pg_advisory_unlock($1)', [effectiveKey]);
|
|
162
|
+
return { held: false, heldBySelf: false };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { held: true, heldBySelf: false };
|
|
166
|
+
|
|
167
|
+
} finally {
|
|
168
|
+
client.release();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
isHeldBySelf(lockKey) {
|
|
173
|
+
const effectiveKey = lockKey || this.lockId;
|
|
174
|
+
return this.locks.has(effectiveKey);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async releaseAll() {
|
|
178
|
+
let count = 0;
|
|
179
|
+
|
|
180
|
+
for (const [lockKey] of this.locks) {
|
|
181
|
+
try {
|
|
182
|
+
await this.release(lockKey);
|
|
183
|
+
count++;
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return count;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
startHeartbeat(lockKey, intervalMs = 30000) {
|
|
192
|
+
this.stopHeartbeat();
|
|
193
|
+
const effectiveKey = lockKey || this.lockId;
|
|
194
|
+
this.lockId = effectiveKey;
|
|
195
|
+
|
|
196
|
+
this._heartbeatTimer = setInterval(async () => {
|
|
197
|
+
try {
|
|
198
|
+
await this.heartbeat();
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
}, intervalMs);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
stopHeartbeat() {
|
|
205
|
+
if (this._heartbeatTimer) {
|
|
206
|
+
clearInterval(this._heartbeatTimer);
|
|
207
|
+
this._heartbeatTimer = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async heartbeat() {
|
|
212
|
+
if (!this.isLocked || !this.lockId) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const held = await this.isLockHeld(this.lockId);
|
|
217
|
+
if (!held) {
|
|
218
|
+
this.isLocked = false;
|
|
219
|
+
throw new Error(`Advisory lock ${this.lockId} is no longer held. Connection may have been recycled.`);
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async isLockHeld(lockKey) {
|
|
225
|
+
const effectiveKey = lockKey || this.lockId;
|
|
226
|
+
const result = await this.pool.query(
|
|
227
|
+
`SELECT COUNT(*)::int AS count FROM pg_locks WHERE locktype = 'advisory' AND objid = $1`,
|
|
228
|
+
[effectiveKey]
|
|
229
|
+
);
|
|
230
|
+
return result.rows[0].count > 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async forceRelease(lockKey) {
|
|
234
|
+
const effectiveKey = lockKey || this.lockId;
|
|
235
|
+
const client = await this.pool.connect();
|
|
236
|
+
try {
|
|
237
|
+
const result = await client.query(
|
|
238
|
+
`SELECT pg_try_advisory_lock($1) AS acquired`,
|
|
239
|
+
[effectiveKey]
|
|
240
|
+
);
|
|
241
|
+
if (result.rows[0].acquired) {
|
|
242
|
+
await client.query(`SELECT pg_advisory_unlock($1)`, [effectiveKey]);
|
|
243
|
+
this.isLocked = false;
|
|
244
|
+
this.locks.delete(effectiveKey);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
} finally {
|
|
249
|
+
client.release();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async checkDeadlock(lockKey) {
|
|
254
|
+
const effectiveKey = lockKey || this.lockId;
|
|
255
|
+
const client = await this.pool.connect();
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const lockWaitQuery = `
|
|
259
|
+
SELECT
|
|
260
|
+
blocked.pid AS blocked_pid,
|
|
261
|
+
blocked.query AS blocked_query,
|
|
262
|
+
blocking.pid AS blocking_pid,
|
|
263
|
+
blocking.query AS blocking_query,
|
|
264
|
+
now() - blocked.query_start AS wait_duration
|
|
265
|
+
FROM pg_stat_activity blocked
|
|
266
|
+
JOIN pg_locks blocked_locks ON blocked_locks.pid = blocked.pid
|
|
267
|
+
JOIN pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
|
|
268
|
+
AND blocking_locks.database = blocked_locks.database
|
|
269
|
+
AND blocking_locks.relation = blocked_locks.relation
|
|
270
|
+
AND blocking_locks.pid != blocked_locks.pid
|
|
271
|
+
JOIN pg_stat_activity blocking ON blocking.pid = blocking_locks.pid
|
|
272
|
+
WHERE blocked_locks.granted = false
|
|
273
|
+
`;
|
|
274
|
+
|
|
275
|
+
const result = await client.query(lockWaitQuery);
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
blockedQueries: result.rows,
|
|
279
|
+
hasPotentialDeadlock: result.rows.length > 0,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
} finally {
|
|
283
|
+
client.release();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async getAllLocks() {
|
|
288
|
+
const client = await this.pool.connect();
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const result = await client.query(`
|
|
292
|
+
SELECT
|
|
293
|
+
locktype,
|
|
294
|
+
objid AS lock_key,
|
|
295
|
+
pid,
|
|
296
|
+
mode,
|
|
297
|
+
granted
|
|
298
|
+
FROM pg_locks
|
|
299
|
+
WHERE locktype = 'advisory'
|
|
300
|
+
`);
|
|
301
|
+
|
|
302
|
+
return result.rows;
|
|
303
|
+
|
|
304
|
+
} finally {
|
|
305
|
+
client.release();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|