ddlforge 0.1.1 → 0.2.1
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/README.md +519 -198
- package/dist/src/cli.d.ts +25 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +253 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/engine/analyzer.d.ts.map +1 -1
- package/dist/src/engine/analyzer.js.map +1 -1
- package/dist/src/lexer/sqlTokenizer.d.ts.map +1 -1
- package/dist/src/lexer/sqlTokenizer.js.map +1 -1
- package/dist/src/reporters/json.js.map +1 -1
- package/dist/src/reporters/terminal.js.map +1 -1
- package/dist/src/runner/backoff.d.ts +53 -0
- package/dist/src/runner/backoff.d.ts.map +1 -0
- package/dist/src/runner/backoff.js +65 -0
- package/dist/src/runner/backoff.js.map +1 -0
- package/dist/src/runner/executor.d.ts +106 -0
- package/dist/src/runner/executor.d.ts.map +1 -0
- package/dist/src/runner/executor.js +444 -0
- package/dist/src/runner/executor.js.map +1 -0
- package/dist/src/runner/locksMonitor.d.ts +85 -0
- package/dist/src/runner/locksMonitor.d.ts.map +1 -0
- package/dist/src/runner/locksMonitor.js +183 -0
- package/dist/src/runner/locksMonitor.js.map +1 -0
- package/package.json +7 -3
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge - Exponential backoff with full jitter for lock-timeout retry loops.
|
|
3
|
+
*
|
|
4
|
+
* Algorithm: sleep = Math.random() * Math.min(maxDelay, baseDelay * 2 ** attempt)
|
|
5
|
+
* This prevents lock-convoy / stampede effects by randomising each retry interval.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Computes a full-jitter exponential backoff interval for the given attempt index.
|
|
9
|
+
*
|
|
10
|
+
* @param attempt 0-based attempt index (first retry = 0)
|
|
11
|
+
* @param options Backoff configuration
|
|
12
|
+
* @returns Randomised sleep duration in milliseconds
|
|
13
|
+
*/
|
|
14
|
+
export function computeBackoff(attempt, options = {}) {
|
|
15
|
+
const baseDelayMs = options.baseDelayMs ?? 250;
|
|
16
|
+
const maxDelayMs = options.maxDelayMs ?? 10_000;
|
|
17
|
+
const maxRetries = options.maxRetries ?? 5;
|
|
18
|
+
const ceiling = Math.min(maxDelayMs, baseDelayMs * Math.pow(2, attempt));
|
|
19
|
+
const sleepMs = Math.random() * ceiling;
|
|
20
|
+
const exceeded = attempt >= maxRetries;
|
|
21
|
+
return { sleepMs, attempt, exceeded };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Returns a promise that resolves after `ms` milliseconds.
|
|
25
|
+
* Optionally accepts an AbortSignal: if the signal is aborted before the
|
|
26
|
+
* timer fires, the promise rejects with the signal's reason.
|
|
27
|
+
*
|
|
28
|
+
* @param ms Sleep duration in milliseconds
|
|
29
|
+
* @param signal Optional AbortSignal for cancellation
|
|
30
|
+
*/
|
|
31
|
+
export function sleep(ms, signal) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
if (signal?.aborted) {
|
|
34
|
+
reject(signal.reason ?? new Error('AbortError'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
signal?.removeEventListener('abort', onAbort);
|
|
39
|
+
resolve();
|
|
40
|
+
}, ms);
|
|
41
|
+
function onAbort() {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
reject(this.reason ?? new Error('AbortError'));
|
|
44
|
+
}
|
|
45
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Iterates over retry attempts, yielding the computed backoff for each attempt.
|
|
50
|
+
* Callers should `sleep(backoff.sleepMs)` after receiving each non-first result.
|
|
51
|
+
*
|
|
52
|
+
* Example:
|
|
53
|
+
* ```ts
|
|
54
|
+
* for (const backoff of retryIterator(3, opts)) {
|
|
55
|
+
* if (backoff.attempt > 0) await sleep(backoff.sleepMs, signal);
|
|
56
|
+
* // ... attempt work ...
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function* retryIterator(maxRetries, options = {}) {
|
|
61
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
62
|
+
yield computeBackoff(attempt, { ...options, maxRetries });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=backoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backoff.js","sourceRoot":"","sources":["../../../src/runner/backoff.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,UAA0B,EAAE;IAC1E,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,GAAG,CAAC;IAC/C,MAAM,UAAU,GAAI,OAAO,CAAC,UAAU,IAAK,MAAM,CAAC;IAClD,MAAM,UAAU,GAAI,OAAO,CAAC,UAAU,IAAK,CAAC,CAAC;IAE7C,MAAM,OAAO,GAAI,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1E,MAAM,OAAO,GAAI,IAAI,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC;IACzC,MAAM,QAAQ,GAAG,OAAO,IAAI,UAAU,CAAC;IAEvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AACxC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,KAAK,CAAC,EAAU,EAAE,MAAoB;IACpD,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;YACjD,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC9C,OAAO,EAAE,CAAC;QACZ,CAAC,EAAE,EAAE,CAAC,CAAC;QAEP,SAAS,OAAO;YACd,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,CAAE,IAAoB,CAAC,MAAM,IAAI,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;QAClE,CAAC;QAED,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,SAAS,CAAC,CAAC,aAAa,CAC5B,UAAkB,EAClB,UAA8C,EAAE;IAEhD,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,MAAM,cAAc,CAAC,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge - PostgreSQL DDL execution supervisor.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities
|
|
5
|
+
* ────────────────
|
|
6
|
+
* 1. Dynamically imports `pg` at runtime (graceful error if not installed).
|
|
7
|
+
* 2. Parses SQL migration files into individual statements (reuses the existing
|
|
8
|
+
* zero-dependency lexer — no duplicate parser code).
|
|
9
|
+
* 3. For each statement, selects one of two execution paths:
|
|
10
|
+
*
|
|
11
|
+
* a. **Non-transactional path** (CREATE/DROP INDEX CONCURRENTLY, REINDEX
|
|
12
|
+
* CONCURRENTLY, VACUUM):
|
|
13
|
+
* – Sets session-level `lock_timeout` / `statement_timeout`.
|
|
14
|
+
* – Executes the statement outside any transaction block.
|
|
15
|
+
* – Always RESETs the session timeouts in a `finally` block.
|
|
16
|
+
*
|
|
17
|
+
* b. **Transactional path** (all other statements):
|
|
18
|
+
* – Opens a transaction with `BEGIN`.
|
|
19
|
+
* – Injects `SET LOCAL lock_timeout` and `SET LOCAL statement_timeout`.
|
|
20
|
+
* – Executes the statement.
|
|
21
|
+
* – On error 55P03 (lock_not_available) or 57014 (query_canceled):
|
|
22
|
+
* Rolls back, waits for a full-jitter backoff interval, retries.
|
|
23
|
+
* – On avalanche detected by the lock monitor: rolls back and aborts.
|
|
24
|
+
* – On success: commits and proceeds to the next statement.
|
|
25
|
+
*
|
|
26
|
+
* 4. Reports live progress via an optional EventEmitter-compatible callback.
|
|
27
|
+
*
|
|
28
|
+
* Zero-dependency invariant
|
|
29
|
+
* ─────────────────────────
|
|
30
|
+
* `pg` is imported with `await import('pg')` inside an async function so it
|
|
31
|
+
* never appears in the static import graph. Static linting (`ddlforge check`)
|
|
32
|
+
* works without `pg` installed.
|
|
33
|
+
*/
|
|
34
|
+
export interface ExecutorOptions {
|
|
35
|
+
/** Full PostgreSQL connection string or URL */
|
|
36
|
+
databaseUrl: string;
|
|
37
|
+
/** lock_timeout value (e.g. "2500ms" or "2500"). Default: "3000ms" */
|
|
38
|
+
lockTimeout?: string;
|
|
39
|
+
/** statement_timeout value (e.g. "30000ms"). Default: "30000ms" */
|
|
40
|
+
statementTimeout?: string;
|
|
41
|
+
/** Maximum number of retry attempts per statement on lock errors. Default: 5 */
|
|
42
|
+
maxRetries?: number;
|
|
43
|
+
/** If true, parse and validate statements but never execute them. Default: false */
|
|
44
|
+
dryRun?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Number of blocked backends that triggers an avalanche abort.
|
|
47
|
+
* Default: 1 (abort as soon as even one query queues behind ours).
|
|
48
|
+
*/
|
|
49
|
+
lockQueueThreshold?: number;
|
|
50
|
+
/** Milliseconds between lock-monitor polls. Default: 500 */
|
|
51
|
+
monitorPollMs?: number;
|
|
52
|
+
/** Progress callback invoked after each statement attempt */
|
|
53
|
+
onProgress?: (event: ExecutionEvent) => void;
|
|
54
|
+
/** AbortSignal for external cancellation */
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}
|
|
57
|
+
export type ExecutionEventKind = 'statement-start' | 'statement-success' | 'statement-retry' | 'statement-failed' | 'avalanche-abort' | 'dry-run-statement' | 'migration-complete' | 'migration-failed';
|
|
58
|
+
export interface ExecutionEvent {
|
|
59
|
+
kind: ExecutionEventKind;
|
|
60
|
+
statementIndex: number;
|
|
61
|
+
totalStatements: number;
|
|
62
|
+
statementSql: string;
|
|
63
|
+
attempt: number;
|
|
64
|
+
elapsedMs: number;
|
|
65
|
+
error?: string;
|
|
66
|
+
lockTimeoutMs?: number;
|
|
67
|
+
retryBackoffMs?: number;
|
|
68
|
+
}
|
|
69
|
+
export interface ExecutionResult {
|
|
70
|
+
success: boolean;
|
|
71
|
+
statementsExecuted: number;
|
|
72
|
+
statementsTotal: number;
|
|
73
|
+
durationMs: number;
|
|
74
|
+
error?: string;
|
|
75
|
+
avalanche?: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns true for statements that PostgreSQL forbids inside a transaction
|
|
79
|
+
* block and must therefore run outside BEGIN … COMMIT:
|
|
80
|
+
* - CREATE INDEX CONCURRENTLY
|
|
81
|
+
* - CREATE UNIQUE INDEX CONCURRENTLY
|
|
82
|
+
* - DROP INDEX CONCURRENTLY
|
|
83
|
+
* - REINDEX … CONCURRENTLY
|
|
84
|
+
* - VACUUM (any form)
|
|
85
|
+
*
|
|
86
|
+
* Detection is intentionally conservative: we normalise whitespace and
|
|
87
|
+
* uppercase the first ~120 characters so that leading comments or extra
|
|
88
|
+
* spaces never cause a false negative.
|
|
89
|
+
*/
|
|
90
|
+
export declare function isConcurrentStatement(sql: string): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Executes a SQL migration file against a PostgreSQL database with:
|
|
93
|
+
* - per-statement lock-timeout injection
|
|
94
|
+
* - exponential full-jitter retry on lock acquisition failures
|
|
95
|
+
* - background lock-queue monitoring with automatic cancellation
|
|
96
|
+
*
|
|
97
|
+
* @param sql Full text of the migration file
|
|
98
|
+
* @param options Execution configuration
|
|
99
|
+
*/
|
|
100
|
+
export declare function executeMigration(sql: string, options: ExecutorOptions): Promise<ExecutionResult>;
|
|
101
|
+
/**
|
|
102
|
+
* Parses a SQL migration string and returns the list of individual statements
|
|
103
|
+
* that would be executed. Useful for tooling and dry-run previews.
|
|
104
|
+
*/
|
|
105
|
+
export declare function parseMigrationStatements(sql: string): string[];
|
|
106
|
+
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/runner/executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAUH,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,4CAA4C;IAC5C,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,MAAM,kBAAkB,GAC1B,iBAAiB,GACjB,mBAAmB,GACnB,iBAAiB,GACjB,kBAAkB,GAClB,iBAAiB,GACjB,mBAAmB,GACnB,oBAAoB,GACpB,kBAAkB,CAAC;AAEvB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAqFD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAS1D;AAoBD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,eAAe,CAAC,CA2U1B;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9D"}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge - PostgreSQL DDL execution supervisor.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities
|
|
5
|
+
* ────────────────
|
|
6
|
+
* 1. Dynamically imports `pg` at runtime (graceful error if not installed).
|
|
7
|
+
* 2. Parses SQL migration files into individual statements (reuses the existing
|
|
8
|
+
* zero-dependency lexer — no duplicate parser code).
|
|
9
|
+
* 3. For each statement, selects one of two execution paths:
|
|
10
|
+
*
|
|
11
|
+
* a. **Non-transactional path** (CREATE/DROP INDEX CONCURRENTLY, REINDEX
|
|
12
|
+
* CONCURRENTLY, VACUUM):
|
|
13
|
+
* – Sets session-level `lock_timeout` / `statement_timeout`.
|
|
14
|
+
* – Executes the statement outside any transaction block.
|
|
15
|
+
* – Always RESETs the session timeouts in a `finally` block.
|
|
16
|
+
*
|
|
17
|
+
* b. **Transactional path** (all other statements):
|
|
18
|
+
* – Opens a transaction with `BEGIN`.
|
|
19
|
+
* – Injects `SET LOCAL lock_timeout` and `SET LOCAL statement_timeout`.
|
|
20
|
+
* – Executes the statement.
|
|
21
|
+
* – On error 55P03 (lock_not_available) or 57014 (query_canceled):
|
|
22
|
+
* Rolls back, waits for a full-jitter backoff interval, retries.
|
|
23
|
+
* – On avalanche detected by the lock monitor: rolls back and aborts.
|
|
24
|
+
* – On success: commits and proceeds to the next statement.
|
|
25
|
+
*
|
|
26
|
+
* 4. Reports live progress via an optional EventEmitter-compatible callback.
|
|
27
|
+
*
|
|
28
|
+
* Zero-dependency invariant
|
|
29
|
+
* ─────────────────────────
|
|
30
|
+
* `pg` is imported with `await import('pg')` inside an async function so it
|
|
31
|
+
* never appears in the static import graph. Static linting (`ddlforge check`)
|
|
32
|
+
* works without `pg` installed.
|
|
33
|
+
*/
|
|
34
|
+
import { splitStatements } from '../lexer/sqlTokenizer.js';
|
|
35
|
+
import { sleep, computeBackoff } from './backoff.js';
|
|
36
|
+
import { startLockMonitor, isLockError } from './locksMonitor.js';
|
|
37
|
+
/**
|
|
38
|
+
* Dynamically imports pg and returns the Pool and Client constructors.
|
|
39
|
+
* If pg is not installed, prints clear installation instructions and throws.
|
|
40
|
+
*/
|
|
41
|
+
async function loadPg() {
|
|
42
|
+
try {
|
|
43
|
+
// Dynamic import: NOT in static import graph — ddlforge check still works
|
|
44
|
+
const mod = await import('pg');
|
|
45
|
+
return mod.default;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
const msg = [
|
|
49
|
+
'',
|
|
50
|
+
' ╔══════════════════════════════════════════════════════════╗',
|
|
51
|
+
' ║ ddlforge apply requires the "pg" package to be ║',
|
|
52
|
+
' ║ installed in your project. ║',
|
|
53
|
+
' ║ ║',
|
|
54
|
+
' ║ Run one of: ║',
|
|
55
|
+
' ║ npm install pg ║',
|
|
56
|
+
' ║ yarn add pg ║',
|
|
57
|
+
' ║ pnpm add pg ║',
|
|
58
|
+
' ╚══════════════════════════════════════════════════════════╝',
|
|
59
|
+
'',
|
|
60
|
+
].join('\n');
|
|
61
|
+
process.stderr.write(msg + '\n');
|
|
62
|
+
throw new Error('pg package is not installed. Run: npm install pg');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/* ------------------------------------------------------------------ */
|
|
66
|
+
/* Helpers */
|
|
67
|
+
/* ------------------------------------------------------------------ */
|
|
68
|
+
/**
|
|
69
|
+
* Normalises a timeout value into a bare integer millisecond string
|
|
70
|
+
* suitable for `SET LOCAL lock_timeout = '<value>'`.
|
|
71
|
+
*
|
|
72
|
+
* Accepts:
|
|
73
|
+
* - "3000ms" → "3000"
|
|
74
|
+
* - "3s" → "3000" (NOT handled — pass pre-converted ms values)
|
|
75
|
+
* - "3000" → "3000"
|
|
76
|
+
* - 3000 → "3000"
|
|
77
|
+
*/
|
|
78
|
+
function normaliseTimeoutMs(value) {
|
|
79
|
+
const s = String(value).trim();
|
|
80
|
+
if (s.endsWith('ms')) {
|
|
81
|
+
return s.slice(0, -2);
|
|
82
|
+
}
|
|
83
|
+
return s;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns the `code` from a Postgres-driver error object.
|
|
87
|
+
* pg errors expose `.code` as a top-level property.
|
|
88
|
+
*/
|
|
89
|
+
function pgErrorCode(err) {
|
|
90
|
+
if (err !== null && typeof err === 'object' && 'code' in err) {
|
|
91
|
+
return String(err['code'] ?? '');
|
|
92
|
+
}
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Returns true for statements that PostgreSQL forbids inside a transaction
|
|
97
|
+
* block and must therefore run outside BEGIN … COMMIT:
|
|
98
|
+
* - CREATE INDEX CONCURRENTLY
|
|
99
|
+
* - CREATE UNIQUE INDEX CONCURRENTLY
|
|
100
|
+
* - DROP INDEX CONCURRENTLY
|
|
101
|
+
* - REINDEX … CONCURRENTLY
|
|
102
|
+
* - VACUUM (any form)
|
|
103
|
+
*
|
|
104
|
+
* Detection is intentionally conservative: we normalise whitespace and
|
|
105
|
+
* uppercase the first ~120 characters so that leading comments or extra
|
|
106
|
+
* spaces never cause a false negative.
|
|
107
|
+
*/
|
|
108
|
+
export function isConcurrentStatement(sql) {
|
|
109
|
+
// Strip leading whitespace / line comments and take the first ~120 chars.
|
|
110
|
+
const head = sql.trimStart().slice(0, 120).replace(/\s+/g, ' ').toUpperCase();
|
|
111
|
+
if (/^CREATE(\s+UNIQUE)?\s+INDEX\s+CONCURRENTLY\b/.test(head))
|
|
112
|
+
return true;
|
|
113
|
+
if (/^DROP\s+INDEX\s+CONCURRENTLY\b/.test(head))
|
|
114
|
+
return true;
|
|
115
|
+
if (/^REINDEX\b/.test(head) && head.includes('CONCURRENTLY'))
|
|
116
|
+
return true;
|
|
117
|
+
if (/^VACUUM\b/.test(head))
|
|
118
|
+
return true;
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
/* ------------------------------------------------------------------ */
|
|
122
|
+
/* Statement text reconstruction */
|
|
123
|
+
/* ------------------------------------------------------------------ */
|
|
124
|
+
/**
|
|
125
|
+
* Extracts the original SQL text for a statement from the migration file.
|
|
126
|
+
* We reconstruct it from the token raw values so that whitespace is
|
|
127
|
+
* preserved reasonably well for display purposes.
|
|
128
|
+
*/
|
|
129
|
+
function statementText(raw) {
|
|
130
|
+
// The lexer joins tokens with single spaces; we just use the raw field.
|
|
131
|
+
return raw.trimEnd() + ';';
|
|
132
|
+
}
|
|
133
|
+
/* ------------------------------------------------------------------ */
|
|
134
|
+
/* Main executor */
|
|
135
|
+
/* ------------------------------------------------------------------ */
|
|
136
|
+
/**
|
|
137
|
+
* Executes a SQL migration file against a PostgreSQL database with:
|
|
138
|
+
* - per-statement lock-timeout injection
|
|
139
|
+
* - exponential full-jitter retry on lock acquisition failures
|
|
140
|
+
* - background lock-queue monitoring with automatic cancellation
|
|
141
|
+
*
|
|
142
|
+
* @param sql Full text of the migration file
|
|
143
|
+
* @param options Execution configuration
|
|
144
|
+
*/
|
|
145
|
+
export async function executeMigration(sql, options) {
|
|
146
|
+
const { databaseUrl, lockTimeout = '3000ms', statementTimeout = '30000ms', maxRetries = 5, dryRun = false, lockQueueThreshold = 1, monitorPollMs = 500, onProgress, signal, } = options;
|
|
147
|
+
const lockTimeoutMs = normaliseTimeoutMs(lockTimeout);
|
|
148
|
+
const statementTimeoutMs = normaliseTimeoutMs(statementTimeout);
|
|
149
|
+
const migrationStart = Date.now();
|
|
150
|
+
// ── Parse statements (zero-dependency lexer) ────────────────────────
|
|
151
|
+
const statements = splitStatements(sql);
|
|
152
|
+
const total = statements.length;
|
|
153
|
+
if (total === 0) {
|
|
154
|
+
return { success: true, statementsExecuted: 0, statementsTotal: 0, durationMs: 0 };
|
|
155
|
+
}
|
|
156
|
+
// ── Dry run mode ─────────────────────────────────────────────────────
|
|
157
|
+
if (dryRun) {
|
|
158
|
+
for (let i = 0; i < statements.length; i++) {
|
|
159
|
+
const stmt = statements[i];
|
|
160
|
+
onProgress?.({
|
|
161
|
+
kind: 'dry-run-statement',
|
|
162
|
+
statementIndex: i,
|
|
163
|
+
totalStatements: total,
|
|
164
|
+
statementSql: statementText(stmt.raw),
|
|
165
|
+
attempt: 0,
|
|
166
|
+
elapsedMs: Date.now() - migrationStart,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
statementsExecuted: 0,
|
|
172
|
+
statementsTotal: total,
|
|
173
|
+
durationMs: Date.now() - migrationStart,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// ── Load pg ──────────────────────────────────────────────────────────
|
|
177
|
+
const { Pool, Client } = await loadPg();
|
|
178
|
+
// One pool for the migration; monitor gets its own direct clients
|
|
179
|
+
const pool = new Pool({ connectionString: databaseUrl, max: 1 });
|
|
180
|
+
let statementsExecuted = 0;
|
|
181
|
+
let globalError;
|
|
182
|
+
let avalanche = false;
|
|
183
|
+
try {
|
|
184
|
+
for (let i = 0; i < statements.length; i++) {
|
|
185
|
+
if (signal?.aborted) {
|
|
186
|
+
globalError = 'Execution aborted by caller signal.';
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
const stmt = statements[i];
|
|
190
|
+
const stmtSql = statementText(stmt.raw);
|
|
191
|
+
const stmtStart = Date.now();
|
|
192
|
+
let succeeded = false;
|
|
193
|
+
let stmtError;
|
|
194
|
+
onProgress?.({
|
|
195
|
+
kind: 'statement-start',
|
|
196
|
+
statementIndex: i,
|
|
197
|
+
totalStatements: total,
|
|
198
|
+
statementSql: stmtSql,
|
|
199
|
+
attempt: 0,
|
|
200
|
+
elapsedMs: stmtStart - migrationStart,
|
|
201
|
+
lockTimeoutMs: Number(lockTimeoutMs),
|
|
202
|
+
});
|
|
203
|
+
// ── Retry loop per statement ──────────────────────────────────────
|
|
204
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
205
|
+
if (signal?.aborted)
|
|
206
|
+
break;
|
|
207
|
+
// Jitter-sleep before all retries (not before first attempt)
|
|
208
|
+
if (attempt > 0) {
|
|
209
|
+
const backoff = computeBackoff(attempt - 1, {
|
|
210
|
+
baseDelayMs: 250,
|
|
211
|
+
maxDelayMs: 10_000,
|
|
212
|
+
maxRetries,
|
|
213
|
+
});
|
|
214
|
+
onProgress?.({
|
|
215
|
+
kind: 'statement-retry',
|
|
216
|
+
statementIndex: i,
|
|
217
|
+
totalStatements: total,
|
|
218
|
+
statementSql: stmtSql,
|
|
219
|
+
attempt,
|
|
220
|
+
elapsedMs: Date.now() - migrationStart,
|
|
221
|
+
retryBackoffMs: backoff.sleepMs,
|
|
222
|
+
});
|
|
223
|
+
await sleep(backoff.sleepMs, signal);
|
|
224
|
+
if (signal?.aborted)
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
// Grab a client from the pool for this attempt
|
|
228
|
+
const client = await pool.connect();
|
|
229
|
+
// Snapshot our backend PID for the lock monitor
|
|
230
|
+
let migrationPid = -1;
|
|
231
|
+
try {
|
|
232
|
+
const pidResult = await client.query('SELECT pg_backend_pid() AS pid');
|
|
233
|
+
migrationPid = Number(pidResult.rows[0]?.['pid'] ?? -1);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// Fallback: proceed without monitoring
|
|
237
|
+
}
|
|
238
|
+
// Start the lock monitor on its own independent connection
|
|
239
|
+
const monitorClientFactory = async () => {
|
|
240
|
+
const monClient = new Client({ connectionString: databaseUrl });
|
|
241
|
+
await monClient.connect?.();
|
|
242
|
+
return monClient;
|
|
243
|
+
};
|
|
244
|
+
const monitor = migrationPid > 0
|
|
245
|
+
? startLockMonitor({
|
|
246
|
+
migrationPid,
|
|
247
|
+
clientFactory: monitorClientFactory,
|
|
248
|
+
pollIntervalMs: monitorPollMs,
|
|
249
|
+
queueThreshold: lockQueueThreshold,
|
|
250
|
+
signal,
|
|
251
|
+
})
|
|
252
|
+
: null;
|
|
253
|
+
const isConcurrent = isConcurrentStatement(stmtSql);
|
|
254
|
+
if (isConcurrent) {
|
|
255
|
+
// ── Non-transactional path (CONCURRENTLY / VACUUM) ─────────────
|
|
256
|
+
// PostgreSQL forbids these inside a transaction block.
|
|
257
|
+
// Apply session-level timeouts instead of transaction-local ones
|
|
258
|
+
// and always RESET them in the finally block.
|
|
259
|
+
try {
|
|
260
|
+
await client.query(`SET lock_timeout = '${lockTimeoutMs}'`);
|
|
261
|
+
await client.query(`SET statement_timeout = '${statementTimeoutMs}'`);
|
|
262
|
+
// Execute the statement outside any transaction
|
|
263
|
+
await client.query(stmtSql);
|
|
264
|
+
// Stop monitor — execution succeeded, no avalanche
|
|
265
|
+
monitor?.stop();
|
|
266
|
+
const monResult = await monitor?.result;
|
|
267
|
+
if (monResult?.avalanche) {
|
|
268
|
+
// Race: monitor fired just as the statement completed.
|
|
269
|
+
// Statement already succeeded so treat as success.
|
|
270
|
+
}
|
|
271
|
+
succeeded = true;
|
|
272
|
+
stmtError = undefined;
|
|
273
|
+
break; // exit retry loop
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
const code = pgErrorCode(err);
|
|
277
|
+
// Stop monitor before doing anything else
|
|
278
|
+
monitor?.stop();
|
|
279
|
+
const monResult = await monitor?.result;
|
|
280
|
+
// No transaction to roll back for concurrent statements.
|
|
281
|
+
if (monResult?.avalanche) {
|
|
282
|
+
// Lock-queue avalanche: abort the entire migration
|
|
283
|
+
onProgress?.({
|
|
284
|
+
kind: 'avalanche-abort',
|
|
285
|
+
statementIndex: i,
|
|
286
|
+
totalStatements: total,
|
|
287
|
+
statementSql: stmtSql,
|
|
288
|
+
attempt,
|
|
289
|
+
elapsedMs: Date.now() - migrationStart,
|
|
290
|
+
error: `Lock-queue avalanche: ${monResult.blockedCount} backend(s) queued. Cancelled at ${monResult.cancelledAt}.`,
|
|
291
|
+
});
|
|
292
|
+
globalError = `Lock-queue avalanche detected. ${monResult.blockedCount} backend(s) were waiting behind migration PID ${migrationPid}. Migration aborted to protect connection pool.`;
|
|
293
|
+
avalanche = true;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
if (isLockError(code) && attempt < maxRetries) {
|
|
297
|
+
// Retryable lock error — continue retry loop
|
|
298
|
+
stmtError = `[${code}] ${err.message ?? String(err)}`;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
// Non-retryable error or max retries exhausted
|
|
302
|
+
stmtError = `[${code || 'ERR'}] ${err.message ?? String(err)}`;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
// Always reset session-level timeouts so the connection is
|
|
307
|
+
// returned to the pool in a clean state.
|
|
308
|
+
try {
|
|
309
|
+
await client.query('RESET lock_timeout');
|
|
310
|
+
}
|
|
311
|
+
catch { /* ignore */ }
|
|
312
|
+
try {
|
|
313
|
+
await client.query('RESET statement_timeout');
|
|
314
|
+
}
|
|
315
|
+
catch { /* ignore */ }
|
|
316
|
+
client.release?.();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
// ── Transactional path (all other DDL / DML) ────────────────────
|
|
321
|
+
try {
|
|
322
|
+
// Open transaction
|
|
323
|
+
await client.query('BEGIN');
|
|
324
|
+
// Inject session-local timeouts (scoped to this transaction)
|
|
325
|
+
await client.query(`SET LOCAL lock_timeout = '${lockTimeoutMs}'`);
|
|
326
|
+
await client.query(`SET LOCAL statement_timeout = '${statementTimeoutMs}'`);
|
|
327
|
+
// Execute the DDL statement
|
|
328
|
+
await client.query(stmtSql);
|
|
329
|
+
// Commit
|
|
330
|
+
await client.query('COMMIT');
|
|
331
|
+
// Stop monitor — execution succeeded, no avalanche
|
|
332
|
+
monitor?.stop();
|
|
333
|
+
const monResult = await monitor?.result;
|
|
334
|
+
if (monResult?.avalanche) {
|
|
335
|
+
// Race condition: monitor fired just as we committed.
|
|
336
|
+
// The COMMIT succeeded so we treat this as a success.
|
|
337
|
+
// (pg_cancel_backend after COMMIT is a no-op.)
|
|
338
|
+
}
|
|
339
|
+
succeeded = true;
|
|
340
|
+
stmtError = undefined;
|
|
341
|
+
break; // exit retry loop
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
const code = pgErrorCode(err);
|
|
345
|
+
// Stop monitor before doing anything else
|
|
346
|
+
monitor?.stop();
|
|
347
|
+
const monResult = await monitor?.result;
|
|
348
|
+
// Rollback the failed transaction
|
|
349
|
+
try {
|
|
350
|
+
await client.query('ROLLBACK');
|
|
351
|
+
}
|
|
352
|
+
catch { /* ignore */ }
|
|
353
|
+
if (monResult?.avalanche) {
|
|
354
|
+
// Lock-queue avalanche: abort the entire migration
|
|
355
|
+
onProgress?.({
|
|
356
|
+
kind: 'avalanche-abort',
|
|
357
|
+
statementIndex: i,
|
|
358
|
+
totalStatements: total,
|
|
359
|
+
statementSql: stmtSql,
|
|
360
|
+
attempt,
|
|
361
|
+
elapsedMs: Date.now() - migrationStart,
|
|
362
|
+
error: `Lock-queue avalanche: ${monResult.blockedCount} backend(s) queued. Cancelled at ${monResult.cancelledAt}.`,
|
|
363
|
+
});
|
|
364
|
+
globalError = `Lock-queue avalanche detected. ${monResult.blockedCount} backend(s) were waiting behind migration PID ${migrationPid}. Migration aborted to protect connection pool.`;
|
|
365
|
+
avalanche = true;
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
if (isLockError(code) && attempt < maxRetries) {
|
|
369
|
+
// Retryable lock error — continue retry loop
|
|
370
|
+
stmtError = `[${code}] ${err.message ?? String(err)}`;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
// Non-retryable error or max retries exhausted
|
|
374
|
+
stmtError = `[${code || 'ERR'}] ${err.message ?? String(err)}`;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
client.release?.();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} // end retry loop
|
|
382
|
+
if (avalanche)
|
|
383
|
+
break;
|
|
384
|
+
if (signal?.aborted && !succeeded) {
|
|
385
|
+
globalError = 'Execution aborted by caller signal.';
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
if (succeeded) {
|
|
389
|
+
statementsExecuted++;
|
|
390
|
+
onProgress?.({
|
|
391
|
+
kind: 'statement-success',
|
|
392
|
+
statementIndex: i,
|
|
393
|
+
totalStatements: total,
|
|
394
|
+
statementSql: stmtSql,
|
|
395
|
+
attempt: 0,
|
|
396
|
+
elapsedMs: Date.now() - stmtStart,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
globalError = stmtError ?? 'Unknown execution error.';
|
|
401
|
+
onProgress?.({
|
|
402
|
+
kind: 'statement-failed',
|
|
403
|
+
statementIndex: i,
|
|
404
|
+
totalStatements: total,
|
|
405
|
+
statementSql: stmtSql,
|
|
406
|
+
attempt: maxRetries,
|
|
407
|
+
elapsedMs: Date.now() - stmtStart,
|
|
408
|
+
error: globalError,
|
|
409
|
+
});
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
await pool.end().catch(() => { });
|
|
416
|
+
}
|
|
417
|
+
const durationMs = Date.now() - migrationStart;
|
|
418
|
+
const success = !globalError;
|
|
419
|
+
onProgress?.({
|
|
420
|
+
kind: success ? 'migration-complete' : 'migration-failed',
|
|
421
|
+
statementIndex: statementsExecuted,
|
|
422
|
+
totalStatements: total,
|
|
423
|
+
statementSql: '',
|
|
424
|
+
attempt: 0,
|
|
425
|
+
elapsedMs: durationMs,
|
|
426
|
+
error: globalError,
|
|
427
|
+
});
|
|
428
|
+
return {
|
|
429
|
+
success,
|
|
430
|
+
statementsExecuted,
|
|
431
|
+
statementsTotal: total,
|
|
432
|
+
durationMs,
|
|
433
|
+
error: globalError,
|
|
434
|
+
avalanche: avalanche || undefined,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Parses a SQL migration string and returns the list of individual statements
|
|
439
|
+
* that would be executed. Useful for tooling and dry-run previews.
|
|
440
|
+
*/
|
|
441
|
+
export function parseMigrationStatements(sql) {
|
|
442
|
+
return splitStatements(sql).map(s => statementText(s.raw));
|
|
443
|
+
}
|
|
444
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../src/runner/executor.ts"],"names":[],"mappings":"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"}
|