require-statement-timeout
Long-running DDL without a preceding SET statement_timeout.
- operation
- Session settings
- lock taken
- ACCESS EXCLUSIVE
- remediation
- Fixed by --fix
- category
- Lock safety
- Can stall the lock queue
What triggers it
Fires when isLongRunningCandidate matches: VACUUM (FULL), any ClusterStmt, any ReindexStmt, a non-concurrent CREATE INDEX, or the AT_ValidateConstraint, AT_SetNotNull, or AT_AlterColumnType subcommand of ALTER TABLE — and no earlier statement in the file already sets statement_timeout.
What does not
Statements not classified as long-running (for example CREATE INDEX CONCURRENTLY, a plain ADD COLUMN) never reach the check, and the rule returns as soon as it finds statement_timeout — as a VariableSetStmt or just the raw substring — in any earlier statement in the file, even one unrelated to this operation.
Where it applies
Applies to every PostgreSQL version MigrationPilot targets. It works on the SQL text alone — no database connection needed.
The lock, and what it blocks
Lock type depends on the underlying statement: ACCESS EXCLUSIVE for CLUSTER, VACUUM FULL, non-concurrent CREATE INDEX, and column type changes or SET NOT NULL; SHARE or ACCESS EXCLUSIVE for REINDEX. Without a timeout, a stalled operation holds that lock indefinitely instead of failing fast, queuing everything behind it.
Why it matters
Without statement_timeout, a DDL operation that encounters unexpected conditions (bloated table, heavy WAL, slow I/O) can hold locks for hours, turning a routine migration into a full outage.
Unsafe, and safe
Flagged
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_user; -- Full table scan with no bound on how long it may run
Safe alternative
SET statement_timeout = '30s'; ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_user; RESET statement_timeout;
What it assumes
Detection is purely textual: it looks for the string statement_timeout anywhere in an earlier statement in the file, so it cannot confirm the timeout actually applies before this statement runs or is set to a sane value; an unrelated SET statement_timeout earlier in the file is enough to silence the warning.
What backs this rule
Every rule is a claim about PostgreSQL, so it carries what the claim rests on: a handbook chapter that cites the manual, the incidents that put it there, and the version it was last checked against.
This is the entry that matters most.
- verified against
- PostgreSQL 17.10
- last checked
- 2026-08-11
- confidence
- High
PostgreSQL manual
Public incidents and write-ups
- GoCardless: Zero-downtime Postgres migrations — the hard parts2024-06
- Xata: Schema changes and the Postgres lock queue2024-06-18
- GitLab.com production incident #6642: Post Deploy migrations Failure on Auto-Deploy2022-03-18
- postgres.ai: Zero-downtime Postgres schema migrations need this — lock_timeout and retries2021-09-23
What the CLI prints
⚠ [MP020] WARNING (line 1) Long-running DDL without a preceding SET statement_timeout. This operation could hold locks for an extended time if it runs longer than expected. Safe alternative: -- Set a timeout so the operation is killed if it runs too long SET statement_timeout = '30s'; ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_user RESET statement_timeout; Why: Without statement_timeout, a DDL operation that encounters unexpected conditions (bloated table, heavy WAL, slow I/O) can hold locks for hours, turning a routine migration into a full outage. Docs: https://migrationpilot.dev/rules/mp020
Generated by running the CLI's own formatter over the flagged example above, so it is the text the tool actually produces. A real run also reports the other rules that fire on the same statement; those blocks are left out here.
Turning it off
For one statement, put a comment on the line before it:
-- migrationpilot-disable MP020 ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_user;
For the whole project, in .migrationpilotrc.yml — by name or by id:
rules:
MP020: false
# or keep it, and downgrade it
rules:
MP020:
severity: warningTry it
Open this rule's flagged example in the playground. It runs in your browser — edit it and watch the finding appear and disappear.
Run MP020 in the playground