Why does CREATE INDEX CONCURRENTLY fail inside a transaction?
Non-transactional statement mixed into a multi-statement migration
Critical: this fails outright or takes production down.
What happens
Statements like CREATE INDEX CONCURRENTLY refuse to run in a transaction block. Mixed with other DDL in one migration, either the whole file fails, or the runner drops the transaction and a mid-file error strands the schema between states with no rollback.
Why it is dangerous on a populated table
On an empty table this finishes before anyone notices. On a populated one the lock is held for the whole operation, every query on the table queues behind it, and the connection pool fills from the back.
Fires on
ALTER TABLE orders ADD COLUMN region text;
CREATE INDEX CONCURRENTLY idx ON orders (region);The safe pattern
Split the file: transactional DDL in one migration, and each CREATE INDEX CONCURRENTLY (or other non-transactional statement) alone in its own migration marked to run outside a transaction, so every step either fully commits or fully rolls back.
-- migration 1 (transactional):
SET lock_timeout = '5s';
ALTER TABLE orders ADD COLUMN region text;
-- migration 2 (alone, outside a transaction):
-- CREATE INDEX CONCURRENTLY idx_orders_region ON orders (region);Fixtures
The rule ships with these files and the test suite runs them on every change: the first set must fire, the second must stay silent.
Fires (2)
-- Guarded transactional DDL mixed with CONCURRENTLY: still a mix.
SET lock_timeout = '5s';
ALTER TABLE orders ADD COLUMN region text;
CREATE INDEX CONCURRENTLY idx_orders_region ON orders (region);ALTER TABLE orders ADD COLUMN region text;
CREATE INDEX CONCURRENTLY idx_orders_region ON orders (region);Stays silent (3)
CREATE INDEX CONCURRENTLY idx_orders_region ON orders (region);-- The recommended pattern: a lock_timeout guard (BV034) and only
-- non-transactional statements, in a migration run outside a transaction.
SET lock_timeout = '5s';
CREATE INDEX CONCURRENTLY idx_orders_user_created ON orders (user_id, created_at);
CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id);ALTER TABLE orders ADD COLUMN region text;
ALTER TABLE orders ADD COLUMN city text;How to check locally
This rule runs locally in the free CLI, or with the full corpus through the hosted service. No install, nothing leaves your machine:
npx bolvrk check migration.sql