Skip to main content

PG.L3.CONCURRENTLY_IN_TRANSACTION — CONCURRENTLY cannot run inside Laravel's transaction

  • Category: safety
  • Level: 3
  • Confidence: deterministic
  • Downtime class: online
  • Stability: stable
  • Suites: lint
  • Applies to: PostgreSQL 18

This is the finding you get for taking the advice on the previous page.

CREATE INDEX CONCURRENTLY cannot run inside a transaction block — PostgreSQL raises an error and does nothing at all. Laravel wraps a migration's up() in a transaction by default on PostgreSQL. So a migration that switches to CONCURRENTLY to avoid blocking writes fails outright until it also opts out of the wrapper.

The fix is one property

final class AddOrdersCustomerIndex extends Migration
{
public bool $withinTransaction = false;

public function up(): void
{
DB::statement('CREATE INDEX CONCURRENTLY idx_orders_customer_id ON orders (customer_id)');
}
}

What leaving the transaction actually changes

It is not a formality, and the rest of this page is why the rule sits at level 3 rather than being a footnote on the index rule.

Without the wrapper, a migration can fail halfway and stay half applied. There is nothing to roll back. That has three consequences worth designing around:

  • One operation per migration. If the file does three things and the second fails, the first has happened and the third has not, and no down() was written for that state.
  • down() becomes the only teardown. It is worth writing carefully here, and GEN.L4.DOWN_MISSING will say so.
  • A failed concurrent build leaves an INVALID index. Complete-looking catalog row, ignored by the planner. Re-running the migration will fail on the duplicate name until it is dropped.

Flagged

// This body runs in the migration's default transaction, where CONCURRENTLY is rejected:
DB::statement('CREATE INDEX CONCURRENTLY idx_orders_customer_id ON orders (customer_id)');

Preferred

// Set public bool $withinTransaction = false; on the migration class so the
// statement runs outside a transaction, as CONCURRENTLY requires:
DB::statement('CREATE INDEX CONCURRENTLY idx_orders_customer_id ON orders (customer_id)');

The two blocks are identical on purpose. The statement was never the problem — the context it runs in was, and that context is declared on the class rather than in the body.

The downtime class is online

Nothing about this operation blocks traffic; the whole point of CONCURRENTLY is that it does not. What the rule reports is that the statement will not run at all, which costs a failed deploy rather than a lock. Those are different problems, and the class prices only the second.

Sources

The fix material this rule carries

A finding from this rule carries machine-readable fix material, using this sequence:

  • concurrently — Build the index without taking the write lock the ordinary form takes.

The payload is material for you or an agent to apply. SQLens writes no migration and runs no DDL. See the remediation payload for every field, the placeholder semantics, and the version rules a consumer has to follow.