ban-concurrent-in-transaction
CONCURRENTLY operations inside a transaction block always fail at runtime.
- operation
- Transactions
- lock taken
- no table lock
- remediation
- Fixed by --fix
- category
- Lock safety
What triggers it
Fires when checkConcurrent finds concurrent: true on an IndexStmt or DropStmt, or a concurrently DefElem on a ReindexStmt params list, and isInsideTransaction(ctx) reports the statement sits inside a BEGIN/COMMIT block in the migration file.
What does not
Any CREATE INDEX, DROP INDEX, or REINDEX that is not CONCURRENTLY never reaches the transaction check. A CONCURRENTLY statement that is not inside a BEGIN/COMMIT block — the normal case for a standalone migration statement — also returns null.
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
Not a lock issue: CONCURRENTLY operations manage their own multi-step internal locking, which is exactly why PostgreSQL refuses to run them inside a caller-supplied transaction block and raises a hard runtime ERROR instead.
Why it matters
CREATE INDEX CONCURRENTLY, DROP INDEX CONCURRENTLY, and REINDEX CONCURRENTLY cannot run inside a transaction. If your migration framework wraps in BEGIN/COMMIT, the operation will error.
Unsafe, and safe
Flagged
BEGIN; CREATE INDEX CONCURRENTLY idx_users_email ON users (email); COMMIT;
Safe alternative
-- Must run outside a transaction CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
Deploy and transaction boundaries
CONCURRENTLY statements must run outside any BEGIN/COMMIT block in the migration file. The auto-fix lifts the statement out of its surrounding transaction only when it is the first, last, or only statement in that block — anything more tangled needs a manual split into a separate deploy step.
What it assumes
Transaction detection relies on isInsideTransaction(ctx), meaning the BEGIN/COMMIT structure visible in the migration file text. If a migration runner wraps every file in an implicit transaction that never appears as SQL, the rule cannot see that and would miss it.
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.
Every safe-migration guide tells you to use CREATE INDEX CONCURRENTLY.
- verified against
- PostgreSQL 17.10
- last checked
- 2026-08-11
- confidence
- High
PostgreSQL manual
Public incidents and write-ups
What the CLI prints
✗ [MP025] CRITICAL (line 2) CONCURRENTLY operations cannot run inside a transaction block. PostgreSQL will raise: "ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block". Remove the surrounding BEGIN/COMMIT. Safe alternative: -- Remove the surrounding transaction block: -- CONCURRENTLY operations manage their own locking. CREATE INDEX CONCURRENTLY idx_users_email ON users (email) Why: CONCURRENTLY operations cannot run inside a transaction block. PostgreSQL will raise a runtime ERROR, causing the entire migration to fail. Many migration frameworks wrap operations in transactions by default, making this a common trap. Docs: https://migrationpilot.dev/rules/mp025
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 MP025 BEGIN;
For the whole project, in .migrationpilotrc.yml — by name or by id:
rules:
MP025: false
# or keep it, and downgrade it
rules:
MP025:
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 MP025 in the playground