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, andGEN.L4.DOWN_MISSINGwill say so.- A failed concurrent build leaves an
INVALIDindex. 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
- PostgreSQL 18 —
CREATE INDEX—CONCURRENTLYcannot run inside a transaction block; the server raises an error and does nothing
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.