MP025criticalauto-fix

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

What the CLI prints

migrationpilot analyze migration.sql
✗ [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:

.migrationpilotrc.yml
rules:
  MP025: false

# or keep it, and downgrade it
rules:
  MP025:
    severity: warning

Try 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

Related rules