Skip to content

fix(migrations): build a new schema's indexes plainly so provisioning never waits on other transactions - #5700

Merged
nicoloboschi merged 2 commits into
mainfrom
fix/new-schema-index-builds
Oct 19, 2026
Merged

nicoloboschi merged 2 commits into
mainfrom
fix/new-schema-index-builds

Conversation

@cdbartholomew

Copy link
Copy Markdown
Contributor

Problem

The migrations create and drop their indexes CONCURRENTLY (37 CREATE INDEX CONCURRENTLY and 25 DROP INDEX CONCURRENTLY), so a deploy that adds one never blocks writes on a populated table. The cost is that CONCURRENTLY waits, several times per statement, for every transaction open anywhere in the database when it starts, not just on its own table.

When a schema is being created from nothing, that wait protects nothing, because its tables are empty and nothing else uses them. But in a multi-schema database, a single long transaction in any other schema stalls each of the new schema's concurrent index statements for up to that transaction's duration. Examples are a large bulk UPDATE, a long report, or a slow background task.

With a workload that keeps such transactions running, provisioning a new schema goes from about a second to several minutes, and the first request waiting on it times out.

Fix

When a migration run starts on a schema that has no alembic_version yet, a before_cursor_execute hook rewrites CREATE [UNIQUE] INDEX CONCURRENTLY and DROP INDEX CONCURRENTLY to the plain form. It does this only for statements that name that schema ("<schema>".). A plain build on an empty table is instant and locks only that table.

These still use CONCURRENTLY:

  • existing schemas, whose tables may be populated and live;
  • the default schema;
  • unqualified statements;
  • statements naming any other schema;
  • anything that isn't index DDL.

So an index on a shared or populated table is never built under a blocking lock by accident. The hook lives in alembic/env.py, so none of the migration files change and future migrations are covered automatically.

Tests

tests/test_migration_new_schema_indexes.py:

  • The rewrite rule: index DDL naming the new schema loses CONCURRENTLY. Other schemas, public, unqualified statements, non-DDL and REINDEX are left alone.
  • With a REPEATABLE READ transaction held open on another connection, a new schema migrates to head in about 1 second. Without the fix this test fails: the migration waits for that transaction and doesn't finish while it stays open. I checked this by running it with the hook removed.
  • Same result either way: a schema migrated with the rewrite ends with exactly the index definitions of one migrated with CONCURRENTLY, and no index is left INVALID.

The existing migration and schema-provisioning suites pass (test_migration_*, test_alembic_dag, test_extension_alembic_tree, test_admin_rename_bank, test_claim_bank_fairness, test_admin_backup_restore): 305 passed, 127 skipped. ruff check and ruff format are clean, and ty reports no new diagnostics.

https://claude.ai/code/session_01MbkqGjkRXNBBpZUP8f1UmF

@koriyoshi2041

Copy link
Copy Markdown
Contributor

The rewrite currently treats the target-schema substring anywhere in the statement as proof that the index DDL belongs only to that schema. That can remove CONCURRENTLY from a live schema too. At this head, these inputs become plain DDL:

DROP INDEX CONCURRENTLY "tenant_abc".idx_a, "tenant_live".idx_b
CREATE INDEX CONCURRENTLY idx ON "tenant_live".t (a) /* "tenant_abc". */

The first is valid PostgreSQL syntax and downgrades the drop for both indexes; the second shows the same substring boundary without touching the target schema at all. I reproduced both directly against plain_index_ddl() at ce2f146916 (the submitted nine focused rewrite tests also pass).

Could the predicate identify the actual ON <schema>. relation for CREATE and require every listed index to use the new schema for DROP? At minimum, a mixed-schema DROP negative control would preserve the stated invariant that a shared or populated schema never receives blocking index DDL.

@nicoloboschi
nicoloboschi force-pushed the fix/new-schema-index-builds branch from 6d5ab60 to c515df7 Compare October 19, 2026 17:12
cdbartholomew and others added 2 commits October 19, 2026 19:31
… never waits on other transactions

The migrations create and drop indexes CONCURRENTLY so a deploy that adds one never
blocks writes on a populated table. CONCURRENTLY waits for every transaction open
anywhere in the database when it starts, not just on its own table. On a schema being
created from nothing that wait protects nothing (its tables are empty), but a single
long transaction in any other schema stalls each of its dozens of concurrent index
statements, so provisioning one new schema takes minutes instead of seconds and the
first request waiting on it times out.

When a migration run starts on a schema with no alembic_version yet, rewrite
CREATE/DROP INDEX CONCURRENTLY to the plain form for statements that name that schema.
A plain build on an empty table is instant and locks only that table. Existing schemas,
the default schema, unqualified statements and anything naming another schema keep
CONCURRENTLY.

Tests:
- the rewrite applies only to index DDL that names the new schema;
- with a REPEATABLE READ transaction held open on another connection, a new schema
  migrates to head (before this change it waited for that transaction and never
  finished while it stayed open; verified failing without the fix);
- a schema migrated this way ends with exactly the index definitions of one migrated
  with CONCURRENTLY, none INVALID.

Claude-Session: https://claude.ai/code/session_01MbkqGjkRXNBBpZUP8f1UmF
Review fix: the event listener's parameters and return were untyped.
@nicoloboschi
nicoloboschi force-pushed the fix/new-schema-index-builds branch from c515df7 to bf0c485 Compare October 19, 2026 17:31
@nicoloboschi
nicoloboschi merged commit 04f0345 into main Oct 19, 2026
117 checks passed
@nicoloboschi
nicoloboschi deleted the fix/new-schema-index-builds branch October 19, 2026 17:53
@nicoloboschi nicoloboschi mentioned this pull request Nov 3, 2026
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.

3 participants