Skip to content

fix(db): avoid "database is busy" during CJK FTS rebuild - #797

Open
epruseal wants to merge 1 commit into
tobi:mainfrom
epruseal:fix/cjk-fts-rebuild-db-busy
Open

fix(db): avoid "database is busy" during CJK FTS rebuild#797
epruseal wants to merge 1 commit into
tobi:mainfrom
epruseal:fix/cjk-fts-rebuild-db-busy

Conversation

@epruseal

Copy link
Copy Markdown

What

rebuildFTSForCjkNormalization() streamed source rows via a better-sqlite3
.iterate() cursor while later beginning a transaction on the same
connection to flush each batch. Holding that cursor open across the
BEGIN raises "database is busy" instead of completing the rebuild.

Why

The streaming cursor was introduced by #737 specifically to avoid loading
every document body into memory at once (the original OOM bug). That fix
was correct for the memory problem but introduced this one: two concurrent
statements (an open iterator + a transaction) on the same connection.

How

Replaced the .iterate() cursor with bounded, keyset-paginated batches
(WHERE id > ? ORDER BY id LIMIT ?) — each SELECT fully finalizes before
its batch's insert transaction starts, so the connection is never busy with
two concurrent statements. Batches stay bounded (500 rows at a time, same
as before), so this doesn't reintroduce the OOM bug #737 fixed — confirmed
by that PR's own large-library migration test, still passing.

Updated the structural regression test to assert the actual protected
invariant (bounded LIMIT + keyset pagination, never one unbounded .all()
over every active document) instead of a specific API shape
(.iterate(), not .all()), since that literal check no longer applies now
that bounded .all() batches are the mechanism.

Testing

  • tsc -p tsconfig.build.json --noEmit clean
  • test/store-cjk-fts.test.ts (6 tests) and test/store-concurrency.test.ts
    (2 tests) all pass

Split out of #761 per review feedback there, so this can be reviewed and
reverted independently of the embedding-provider change.

rebuildFTSForCjkNormalization() streamed source rows via a better-sqlite3
.iterate() cursor while later beginning a transaction on the same
connection to flush each batch. Holding that cursor open across the
BEGIN raises "database is busy" instead of completing the rebuild.

Replace the streaming cursor with bounded, keyset-paginated batches
(`WHERE id > ? ORDER BY id LIMIT ?`) — each SELECT fully finalizes
before its batch's insert transaction starts, so the connection is
never busy with two concurrent statements. Batches stay bounded (500
rows at a time, same as before), so this doesn't reintroduce the
original OOM bug .iterate() was fixing (fix/cjk-fts-rebuild-oom, tobi#737)
— confirmed by the existing large-library migration test, which still
passes.

Updated the structural regression test to assert the actual protected
invariant (bounded LIMIT + keyset pagination, never one unbounded
.all() over every active document) instead of a specific API shape
(".iterate(), not .all()"), since that literal check no longer applies
now that bounded .all() batches are the mechanism.
@PowderAddicts

PowderAddicts commented Jul 27, 2026 via email

Copy link
Copy Markdown

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.

2 participants