Skip to content

feat(db): concurrent index builds and safe concurrent index drops - #943

Merged
sebyx07 merged 5 commits into
mainfrom
feat/db-concurrent-indexes
Oct 31, 2026
Merged

sebyx07 merged 5 commits into
mainfrom
feat/db-concurrent-indexes

Conversation

@sebyx07

@sebyx07 sebyx07 commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Why

Gaps found while running a production app (tesote/bank-integrations, docs/upstream-issues.md §21–22):

  • x db gen wrote every index as a plain create index inside the migration's transaction. That takes a SHARE lock on a hot table and races the migrate role's lock_timeout.
  • An entity couldn't ask for create index concurrently.
  • Every dropped index was a transactional drop index, which takes ACCESS EXCLUSIVE. The app's gate refuses that, so it kept three redundant indexes on its hottest table.

What

  • Entity option: indexes: [{ on: [...], concurrently: true }], the same shape as using.
    • It is a build strategy, not part of the index's identity: not in its name or the snapshot, and toggling it generates nothing.
    • A foreign key's own index re-declared with it is built concurrently.
  • Generator:
    • A concurrent index becomes a non-transactional step marked -- transaction: false: create index concurrently if not exists …, with a concurrent drop in its down.
    • Index-only work is one migration with transactional: false. A mixed diff becomes <id>.sql (transactional) plus an <id>_concurrently.sql follow-up, each with its own snapshot and hash (GeneratedMigration.followUp, x db gen --json).
    • An index on a table created in the same migration stays in the transaction (the table is empty and not yet visible).
    • A changed concurrent definition is dropped and rebuilt, both concurrently.
  • Safe drop: every index removed from an entity is now drop index concurrently if exists in the non-transactional step, and its down rebuilds it concurrently.
    • A constraint-backed unique index (where a concurrent drop fails with 2BP01, measured on PG 18.6) drops its constraint in the transactional step first.
    • The destructive check is unchanged: a concurrent index drop is not flagged, while a column or table drop beside it still is (tested).
  • Runner:
    • A marked migration runs one autocommitted statement at a time on the lock-holding session, with no lock_timeout. A concurrent build waits on older transactions, which is a lock wait; a live test proves a 200 ms limit fails it with 55P03.
    • The ledger row is written last.
    • Before each named concurrent build, the runner drops any INVALID index of that name, so an interrupted build heals on the next run.
    • New X_MIGRATION_CONCURRENT_IN_TRANSACTION refuses a concurrent statement in an unmarked migration before the transaction opens, instead of letting Postgres fail mid-transaction with 25001.
    • The marker scan is shared with -- destructive: true (marker-line.ts).

Tests

  • Unit: marker, statement recognition, generator output (one or two files, ids, snapshots, toggle is a no-op, new table, changed definition, ordinary indexes byte-identical), and the runner step (no BEGIN, no lock_timeout, ledger last, invalid-index heal, rollback, the new error, hostile ids).
  • PGlite: a real interrupted unique build leaves an INVALID index and no ledger row, and the next run rebuilds it; the two-step constraint drop. The test fails with the heal disabled.
  • Live (PG 18): the 25001 premise, interrupted-build recovery, the no-lock_timeout proof, and the measured 2BP01 row. 1280 db tests pass.
  • No churn: x db gen in examples/dummy and dummy/social-media-clone makes no new migration.

bun run verify is green (14 of 20; the app-only steps skip at the repo root). The serve-graph module ceilings are raised by 6 with reasons. CHANGELOG [Unreleased], db and entity READMEs, the wiki and llms.txt are updated.

🤖 Generated with Claude Code

An entity index can declare `concurrently: true`; `x db gen` emits it as a non-transactional step
(`-- transaction: false`): `create index concurrently if not exists`, alone when the diff is index
work only, else as an `<id>_concurrently` follow-up migration after the transactional one. Every
index removed from an entity is dropped with `drop index concurrently if exists` in that step (a
constraint-backed unique index drops its constraint first, in the transactional step).

The runner applies a marked migration statement by statement on the lock-holding session, with no
lock_timeout (a concurrent build waits on older transactions by design), writes the ledger row last,
and drops an INVALID index left by an interrupted build before building it again.
X_MIGRATION_CONCURRENT_IN_TRANSACTION refuses a concurrent statement in an unmarked migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@developerz-ai

developerz-ai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

🟡 Reviewed

1 actionable comment(s) · grounded on your code

⏱ 32s wall clock · gpt-6.1-sol via router · 2 model call(s) · 418 output token(s)

🤖 developerz.ai — automated review, running on your model and your box.

@developerz-ai developerz-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary — 46 file(s), 6 finding(s).

Critical 0 · Major 2 · Minor 4 · Nit 0

[bot] completed: Reviewed concurrent index generation and CLI projection; found constraint-removal rollback gaps, a uniqueness gap during concurrent replacement, and an inaccurate intermediate snapshot. [bot] completed: Reviewed concurrent migration execution and recovery.

File Findings
packages/db/src/concurrent-index.live.test.ts 1 minor
packages/db/src/concurrent-index.ts 1 minor
packages/db/src/concurrent-plan.ts 2 major, 1 minor
wiki/Migrations-And-Backfills.md 1 minor

🤖 developerz.ai review — automated, running on your model and your box. What is this? · Reviewed by router/gpt-6.1-sol on box sebastian-1 — 759.9k in / 5.67k out over 19 calls; cost unknown: no price on record for router/gpt-6.1-sol.

Comment thread packages/db/src/concurrent-index.ts Resolved
Comment thread packages/db/src/concurrent-plan.ts Resolved
Comment thread packages/db/src/concurrent-plan.ts Resolved
Comment thread packages/db/src/concurrent-plan.ts Resolved
Comment thread wiki/Migrations-And-Backfills.md Resolved
@developerz-ai

developerz-ai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Address review findings on #943

Queued — waiting for a free box.

Sessions: review 1 · review 2 · review 3 · review 4


🤖 developerz.ai, an automated agent, not a human — developerz-ai[bot]

…y build; REINDEX parsed whole; the lock_timeout test proves the wait

- A constraint-backed unique index is unhooked and dropped in the non-transactional step, after every
  concurrent build (a renamed key's replacement exists first); the transactional step drops no key,
  so its rollback owes none and its snapshot keeps it. The unhook runs in its own short transaction
  under SET LOCAL lock_timeout.
- REINDEX: the full option list and CONCURRENTLY [boolean] are parsed.
- The live test waits until the build is seen blocked, outlives 200 ms, then releases the reader.
- Wiki and README: one file, one non-transactional file, or two — only concurrent work splits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@developerz-ai developerz-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary — 16 file(s), 3 finding(s).

Critical 0 · Major 1 · Minor 2 · Nit 0

[bot] completed — The increment moves constraint removal after replacement builds and strengthens REINDEX detection. Two execution edge cases remain: transaction-forbidden handwritten statements and quoted REINDEX options. [bot] completed: Reviewed the concurrent-step fixes.

2 non-blocking finding(s) below major (no thread on a re-review — only blocking findings open threads)
  • packages/db/README.md:183 minor · docs [bot] The claim that every build precedes every drop is false for a same-name definition change: redefineConcurrently explicitly emits its drop before its build. Qualify the ordering guarantee here and in the wiki Order row to exclude same-name redefinitions. Also at…
  • packages/db/src/concurrent-index.ts:113 minor · bug [bot] REINDEX (VERBOSE 'true') INDEX CONCURRENTLY idx is missed because leadingTokens stops at the string literal, so preflight permits it inside BEGIN and PostgreSQL returns 25001. Parse quoted option values without terminating the REINDEX prefix scan and add this regression case.

🤖 developerz.ai review — automated, running on your model and your box. What is this? · Reviewed by router/gpt-6.1-sol on box hv-dedicated-6 — 472.3k in / 2.6k out over 9 calls; cost unknown: no price on record for router/gpt-6.1-sol.

Comment thread packages/db/src/migration-step.ts Resolved
…unded transaction; vacuum and its kin run autocommitted

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@developerz-ai developerz-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary — 2 file(s), 1 finding(s).

Critical 0 · Major 1 · Minor 0 · Nit 0

[bot] Completed review: restores autocommit execution for maintenance statements, but also removes lock-timeout protection from other blocking DDL in marked migrations.

🤖 developerz.ai review — automated, running on your model and your box. What is this? · Reviewed by router/gpt-6.1-sol on box hv-dedicated-dev-din — 79.7k in / 661 out over 4 calls; cost unknown: no price on record for router/gpt-6.1-sol.

Comment thread packages/db/src/migration-step.ts Resolved
… autocommits; every other blocking statement keeps lock_timeout

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@developerz-ai developerz-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary — 2 file(s), 1 finding(s).

Critical 0 · Major 1 · Minor 0 · Nit 0

[bot] completed: Extends lock-timeout protection to non-concurrent statements in marked migrations, but misclassifies table-specific CLUSTER and REINDEX SCHEMA, causing unbounded blocking or migration failure.

🤖 developerz.ai review — automated, running on your model and your box. What is this? · Reviewed by router/gpt-6.1-sol on box hv-dedicated-6 — 57.6k in / 2.02k out over 4 calls; cost unknown: no price on record for router/gpt-6.1-sol.

Comment thread packages/db/src/migration-step.ts Resolved
…he bounded transaction

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@developerz-ai developerz-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary — 2 file(s), 1 finding(s).

Critical 0 · Major 0 · Minor 1 · Nit 0

[bot] completed: The increment bounds table-specific CLUSTER operations while preserving autocommit for bare CLUSTER, but misses the parenthesized option syntax.

🤖 developerz.ai review — automated, running on your model and your box. What is this? · Reviewed by router/gpt-6.1-sol on box sebastian-1 — 52.6k in / 418 out over 2 calls; cost unknown: no price on record for router/gpt-6.1-sol.

@sebyx07
sebyx07 merged commit 6721605 into main Oct 31, 2026
16 checks passed
@sebyx07
sebyx07 deleted the feat/db-concurrent-indexes branch October 31, 2026 14:27
@sebyx07 sebyx07 mentioned this pull request Oct 31, 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.

1 participant