Skip to content

[Pg] Run CREATE INDEX CONCURRENTLY migration statements outside of transactions - #6133

Open
luskin wants to merge 3 commits into
drizzle-team:mainfrom
luskin:pg/concurrent-index-migrations
Open

[Pg] Run CREATE INDEX CONCURRENTLY migration statements outside of transactions#6133
luskin wants to merge 3 commits into
drizzle-team:mainfrom
luskin:pg/concurrent-index-migrations

Conversation

@luskin

@luskin luskin commented Aug 14, 2026

Copy link
Copy Markdown

Fixes #860

Problem

PgDialect.migrate() wraps all pending migrations in a single transaction. PostgreSQL forbids CREATE INDEX CONCURRENTLY (and CREATE UNIQUE INDEX CONCURRENTLY / DROP INDEX CONCURRENTLY) inside a transaction block, so any migration generated from an index with .concurrently() failed with:

CREATE INDEX CONCURRENTLY cannot run inside a transaction block

drizzle-kit already generates the CONCURRENTLY SQL; it just could not be applied by migrate() (or by drizzle-kit migrate, which delegates to the same code).

Solution

The Postgres migrator now detects statements that start with CREATE [UNIQUE] INDEX CONCURRENTLY or DROP INDEX CONCURRENTLY (after leading whitespace/comments) and executes them outside of the migration transaction:

  • No concurrent statements pending (the common case): behavior is byte-for-byte identical to before. All pending migrations and their journal entries run in one transaction, all-or-nothing.
  • A pending migration contains a concurrent statement: statements batched so far are committed first (required: CREATE INDEX CONCURRENTLY waits for open transactions to finish, so keeping our own transaction open would self-deadlock), the concurrent statement runs on the session directly, and the remaining statements resume in a new transaction. Each migration is applied and recorded in __drizzle_migrations individually, so a failure never rolls back a migration that was already recorded as applied.

Since concurrent index builds are inherently non-transactional, a migration containing one cannot be fully atomic. If such a statement fails mid-build, PostgreSQL can leave an INVALID index behind, which has to be dropped before retrying - the same caveat as running CREATE INDEX CONCURRENTLY manually.

Notes:

  • Only statement detection, no SQL rewriting: exactly the statements in the migration files are executed.
  • Drivers without transaction support (neon-http, xata-http) already ran statements individually and are unaffected. All drivers that use PgDialect.migrate (node-postgres, postgres-js, pglite, neon-serverless, vercel-postgres, aws-data-api, bun-sql, netlify) inherit the fix.
  • Migrations generated with breakpoints: false (all statements in one string) remain unsupported for CONCURRENTLY, as before.

Tests

  • drizzle-orm/tests/pg-migrator.test.ts (new, 24 unit tests): single-transaction behavior preserved (including rollback of all pending migrations on failure), transaction boundaries and journal placement around concurrent statements, per-migration recording, custom migrationsTable/migrationsSchema, failure ordering, and positive/negative statement classification (case-insensitivity, UNIQUE, IF NOT EXISTS, leading comments, quoted identifiers named concurrently, etc.).
  • integration-tests/tests/pg/node-postgres.test.ts + integration-tests/drizzle2/pg-concurrently/ fixture (new): applies a migration with two CONCURRENTLY indexes plus a follow-up migration against real PostgreSQL, verifies both indexes exist and are indisvalid, both migrations are journaled, a second migrate() call is a no-op, and the unique index enforces uniqueness. Without the fix this test fails with the exact error from [BUG]: Can't create index concurrently due to transaction block #860 (verified).
  • Full drizzle-orm unit suite (590 tests), drizzle-orm type tests, integration-tests tsc, existing migrator integration tests for node-postgres, postgres-js and pglite, and dprint all pass.

Made with Cursor

luskin and others added 3 commits August 14, 2026 13:05
…tions

CREATE INDEX CONCURRENTLY, CREATE UNIQUE INDEX CONCURRENTLY and
DROP INDEX CONCURRENTLY cannot run inside a transaction block, so
applying a migration generated from an index with .concurrently()
always failed.

When no pending migration contains such a statement, all pending
migrations still run in a single transaction, exactly as before.
When one does, already batched statements are committed before the
concurrent statement runs on its own connection state, and each
migration is applied and recorded individually so a failure never
rolls back a migration that was already recorded as applied.

Fixes drizzle-team#860

Co-authored-by: Cursor <cursoragent@cursor.com>
Rename journalEntry to insertMigrationSql: in this codebase "journal"
refers to meta/_journal.json, while this SQL inserts into the
migrations table. Name the concurrent-statement mode check, add a test
that consecutive concurrent statements do not open empty transactions,
use one log accessor in the unit tests, and assert the exact unique
constraint in the integration test instead of any rejection.

Co-authored-by: Cursor <cursoragent@cursor.com>
… in tests

In Postgres grammar these clauses follow CONCURRENTLY, so detection is
unaffected; the tests make that explicit.

Co-authored-by: Cursor <cursoragent@cursor.com>
@luskin
luskin force-pushed the pg/concurrent-index-migrations branch from faf6e60 to d5081de Compare August 14, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Can't create index concurrently due to transaction block

1 participant