fix(store): make cleanupOrphanedVectors atomic across both vector tables - #766
Closed
fa1k3 wants to merge 1 commit into
Closed
fix(store): make cleanupOrphanedVectors atomic across both vector tables#766fa1k3 wants to merge 1 commit into
fa1k3 wants to merge 1 commit into
Conversation
cleanupOrphanedVectors ran its two DELETEs (vectors_vec, then content_vectors) as separate implicit transactions. An interruption between them (process crash, SQLITE_BUSY under a concurrent writer) desyncs the two tables: the embedding rows are gone from vectors_vec while content_vectors still reports the chunks as embedded, so the index silently loses vector-search coverage with no way to notice or repair short of re-embedding. Wrap the orphan count and both DELETEs in a single db.transaction(). The count shares the transaction so the returned number always matches what the DELETEs actually removed, even when another connection mutates documents concurrently. Nested callers are safe: both better-sqlite3 and bun:sqlite implement nested transaction functions via savepoints. Tests: happy path, fault injection between the DELETEs proving the first one rolls back and the connection stays usable, and savepoint participation in an outer transaction.
|
Dear tobi/qmd,
We would like to acknowledge that we have received your request and a ticket has been created.
A support representative will be reviewing your request and will send you a personal response.(usually within 24-48 hours).
Thank you for your patience.
…On Mon, Jul 13 2026, at 07:41 PM, tobi/qmd ***@***.***> wrote:
Summary cleanupOrphanedVectors() runs its two DELETEs ( vectors_vec, then content_vectors) as separate implicit transactions. An interruption between them — process crash, or SQLITE_BUSY from a concurrent writer hitting the second statement — desyncs the two tables: the embedding rows are already gone from vectors_vec while content_vectors still reports the chunks as embedded.
Since these rows are orphaned (no active document) and vector search post-filters on documents.active = 1, live search is unaffected right away. The failure is latent: qmd is content-addressable, so if the same content later returns and that hash is reactivated, the stale content_vectors rows make getHashesNeedingEmbedding treat it as already embedded — qmd embed skips it, and the document is silently unsearchable by vector with no orphan left for a future cleanup to catch.
What changed
* The orphan count and both DELETEs now run inside a single db.transaction(), executed as BEGIN IMMEDIATE: the count reads before the DELETEs write, and upgrading a deferred read snapshot under a concurrent WAL writer fails with SQLITE_BUSY_SNAPSHOT instead of honoring the busy timeout. The shared Database interface type now reflects the .immediate variant both drivers already expose.
* The count shares the transaction so the returned number always matches what the DELETEs actually removed, even when another connection mutates documents concurrently.
* Nested callers stay safe: both better-sqlite3 and bun:sqlite implement nested transaction functions via savepoints (an immediate transaction invoked inside an outer transaction becomes a savepoint).
* Changelog entry under [Unreleased].
Verification
* New tests in test/store.test.ts ( cleanupOrphanedVectors atomicity):
* happy path: orphaned chunks removed from both tables, survivors intact, count correct
* fault injection between the two DELETEs: the vectors_vec DELETE rolls back (this test fails against the previous code) and the connection stays usable for a plain retry
* outer-transaction rollback also restores the cleanup's deletions
* inner-savepoint isolation: a cleanup failure caught inside an outer transaction rolls back alone while the outer transaction's own write commits bun run test:types clean, bun run build passes
* Suites green under both runtimes (vitest via Node and bun test) You can view, comment on, or merge this pull request online at: #766 (#766) Commit Summary c7c18b8 fix(store): make cleanupOrphanedVectors atomic across both vector tables (c7c18b8) File Changes
( 4 files) (https://github.com/tobi/qmd/pull/766/files) M CHANGELOG.md
(10) (https://github.com/tobi/qmd/pull/766/files#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed) M src/db.ts
(4) (https://github.com/tobi/qmd/pull/766/files#diff-1d214e57ddda9095d296e5700ebce701333bfefcf417e234c584d14091b2f50d) M src/store.ts
(69) (https://github.com/tobi/qmd/pull/766/files#diff-2717c7fb31c2070dd96f07394997809bc9be7f3fff03ab638db696cc480f39b6) M test/store.test.ts
(133) (https://github.com/tobi/qmd/pull/766/files#diff-ecc9e7b9a74de1ebac3cd3038ffc91cd42684ca2890738d3b93139c723c36407) Patch Links: https://github.com/tobi/qmd/pull/766.patch (https://github.com/tobi/qmd/pull/766.patch) https://github.com/tobi/qmd/pull/766.diff (https://github.com/tobi/qmd/pull/766.diff)
—
Reply to this email directly, view it on GitHub, or (#766?email_source=notifications&email_token=BRF3HG44OS5QOB4SAM2G7KL5EU3QLA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DANBXGI3DSMBVGSTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVRTG633UMVZF6Y3MNFRWW) unsubscribe. (https://github.com/notifications/unsubscribe-auth/BRF3HG2VRUOIEO22SO6IRYL5EU3QLAVCNFSNUABGKJSXA33TNF2G64TZHMYTCMJSGM3DKMZQGE5US43TOVSTWNBYG43TAMJQHAZDFILWAI)
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and (https://github.com/notifications/mobile/ios/BRF3HG23PELRGGYUWHZYEW35EU3QLA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DANBXGI3DSMBVGSTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVJTG633UMVZF62LPOM) Android. Download it today! (https://github.com/notifications/mobile/android/BRF3HG7ELDQDRB5HQOWG6SL5EU3QLA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DANBXGI3DSMBVGSTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVZTG633UMVZF6YLOMRZG62LE)
You are receiving this because you are subscribed to this thread. Message ID: <tobi/qmd/pull/766 @ github . com> [
{
***@***.***": "http://schema.org",
***@***.***": "EmailMessage",
"potentialAction": {
***@***.***": "ViewAction",
"target": "#766?email_source=notifications\u0026email_token=BRF3HGZLOAX7ST4O4245LED5EU3QLA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DANBXGI3DSMBVGSTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVNTW2YLJNRPWG3DJMNVQ",
"url": "#766?email_source=notifications\u0026email_token=BRF3HGZLOAX7ST4O4245LED5EU3QLA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DANBXGI3DSMBVGSTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVNTW2YLJNRPWG3DJMNVQ",
"name": "View Pull Request"
},
"description": "View this Pull Request on GitHub",
"publisher": {
***@***.***": "Organization",
"name": "GitHub",
"url": "https://github.com"
}
}
]
|
5 tasks
Owner
|
Dear tobi/qmd,
We would like to acknowledge that we have received your request and a ticket has been created.
A support representative will be reviewing your request and will send you a personal response.(usually within 24-48 hours).
Thank you for your patience.
…On Thu, Aug 13 2026, at 02:15 AM, tobi/qmd ***@***.***> wrote:
tobi left a comment (tobi/qmd#766) (#766 (comment))
Superseded by #856 — same atomic cleanup, rebased onto current main after (#856) #851 extracted countOrphanedVectors (CHANGELOG + that refactor were the conflicts here). (#851)
—
Reply to this email directly, view it on GitHub, or (#766?email_source=notifications&email_token=BRF3HGYMW2DJRK32WA3WPYD5JUQEVA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRXGUYTIMZUGM2KM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5275143434) unsubscribe. (https://github.com/notifications/unsubscribe-auth/BRF3HG6XYBNEAJGKLGMUIT35JUQEVAVCNFSNUABGKJSXA33TNF2G64TZHMYTCMJSGM3DKMZQGE5US43TOVSTWNBYG43TAMJQHAZDFILWAI)
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and (https://github.com/notifications/mobile/ios/BRF3HG3V7K7FS47LHNOSFC35JUQEVA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRXGUYTIMZUGM2KM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJKTGN5XXIZLSL5UW64Y) Android. Download it today! (https://github.com/notifications/mobile/android/BRF3HG4GFXS2PW3RBKCPLHL5JUQEVA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRXGUYTIMZUGM2KM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLTGN5XXIZLSL5QW4ZDSN5UWI)
You are receiving this because you commented. Message ID: <tobi/qmd/pull/766/c5275143434 @ github . com> [
{
***@***.***": "http://schema.org",
***@***.***": "EmailMessage",
"potentialAction": {
***@***.***": "ViewAction",
"target": "#766?email_source=notifications\u0026email_token=BRF3HG3LVJ7VEZGJHJE5UH35JUQEVA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRXGUYTIMZUGM2KM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJK3HNVQWS3C7MNWGSY3L#issuecomment-5275143434",
"url": "#766?email_source=notifications\u0026email_token=BRF3HG3LVJ7VEZGJHJE5UH35JUQEVA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRXGUYTIMZUGM2KM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJK3HNVQWS3C7MNWGSY3L#issuecomment-5275143434",
"name": "View Pull Request"
},
"description": "View this Pull Request on GitHub",
"publisher": {
***@***.***": "Organization",
"name": "GitHub",
"url": "https://github.com"
}
}
]
|
Owner
|
Closed in favor of #856, which lands this fix on current main. |
|
Dear tobi/qmd,
We would like to acknowledge that we have received your request and a ticket has been created.
A support representative will be reviewing your request and will send you a personal response.(usually within 24-48 hours).
Thank you for your patience.
…On Thu, Aug 13 2026, at 02:19 AM, tobi/qmd ***@***.***> wrote:
Closed #766. (#766)
—
Reply to this email directly, view it on GitHub, or (#766?email_source=notifications&email_token=BRF3HGZGOWVRFS5V2A2H2H35JUQTXA5CNFSNUABQM5UWIORPF5TWS5BNNB2WEL2JONZXKZKFOZSW45CON52GSZTJMNQXI2LPNYXTEOJTG42TSNRTHE4THJTSMVQXG33OU5RW63LNMVXHJJLFOZSW45FMMZXW65DFOJPWG3DJMNVQ#event-29375963993) unsubscribe. (https://github.com/notifications/unsubscribe-auth/BRF3HGYHZJDXNTRWCYXOCBL5JUQTXAVCNFSNUABGKJSXA33TNF2G64TZHMYTCMJSGM3DKMZQGE5US43TOVSTWNBYG43TAMJQHAZDFILWAI)
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and (https://github.com/notifications/mobile/ios/BRF3HG4SB3ORW5ZB32NRQUT5JUQTXA5CNFSNUABQM5UWIORPF5TWS5BNNB2WEL2JONZXKZKFOZSW45CON52GSZTJMNQXI2LPNYXTEOJTG42TSNRTHE4THJTSMVQXG33OU5RW63LNMVXHJJLFOZSW45FKMZXW65DFOJPWS33T) Android. Download it today! (https://github.com/notifications/mobile/android/BRF3HGYIL4XHLUYZKRSJLR35JUQTXA5CNFSNUABQM5UWIORPF5TWS5BNNB2WEL2JONZXKZKFOZSW45CON52GSZTJMNQXI2LPNYXTEOJTG42TSNRTHE4THJTSMVQXG33OU5RW63LNMVXHJJLFOZSW45FOMZXW65DFOJPWC3TEOJXWSZA)
You are receiving this because you commented. Message ID: <tobi/qmd/pull/766/issue_event/29375963993 @ github . com> [
{
***@***.***": "http://schema.org",
***@***.***": "EmailMessage",
"potentialAction": {
***@***.***": "ViewAction",
"target": "#766?email_source=notifications\u0026email_token=BRF3HG2NPJ2VOHXSHJBWJVT5JUQTXA5CNFSNUABQM5UWIORPF5TWS5BNNB2WEL2JONZXKZKFOZSW45CON52GSZTJMNQXI2LPNYXTEOJTG42TSNRTHE4THJTSMVQXG33OU5RW63LNMVXHJJLFOZSW45FLM5WWC2LML5RWY2LDNM#event-29375963993",
"url": "#766?email_source=notifications\u0026email_token=BRF3HG2NPJ2VOHXSHJBWJVT5JUQTXA5CNFSNUABQM5UWIORPF5TWS5BNNB2WEL2JONZXKZKFOZSW45CON52GSZTJMNQXI2LPNYXTEOJTG42TSNRTHE4THJTSMVQXG33OU5RW63LNMVXHJJLFOZSW45FLM5WWC2LML5RWY2LDNM#event-29375963993",
"name": "View Pull Request"
},
"description": "View this Pull Request on GitHub",
"publisher": {
***@***.***": "Organization",
"name": "GitHub",
"url": "https://github.com"
}
}
]
|
|
Dear tobi/qmd,
We would like to acknowledge that we have received your request and a ticket has been created.
A support representative will be reviewing your request and will send you a personal response.(usually within 24-48 hours).
Thank you for your patience.
…On Thu, Aug 13 2026, at 02:19 AM, tobi/qmd ***@***.***> wrote:
tobi left a comment (tobi/qmd#766) (#766 (comment))
Closed in favor of #856, which lands this fix on current main. (#856)
—
Reply to this email directly, view it on GitHub, or (#766?email_source=notifications&email_token=BRF3HGYNSLKIW36D7GHNE4T5JUQTZA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRXGUYTOMBTGMZKM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5275170332) unsubscribe. (https://github.com/notifications/unsubscribe-auth/BRF3HG25S6KCZWMQIMSJQU35JUQTZAVCNFSNUABGKJSXA33TNF2G64TZHMYTCMJSGM3DKMZQGE5US43TOVSTWNBYG43TAMJQHAZDFILWAI)
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and (https://github.com/notifications/mobile/ios/BRF3HGYGXSN2AJYXTF54WEL5JUQTZA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRXGUYTOMBTGMZKM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJKTGN5XXIZLSL5UW64Y) Android. Download it today! (https://github.com/notifications/mobile/android/BRF3HG3WGOAXHGMTLEMNBZ35JUQTZA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRXGUYTOMBTGMZKM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLTGN5XXIZLSL5QW4ZDSN5UWI)
You are receiving this because you commented. Message ID: <tobi/qmd/pull/766/c5275170332 @ github . com> [
{
***@***.***": "http://schema.org",
***@***.***": "EmailMessage",
"potentialAction": {
***@***.***": "ViewAction",
"target": "#766?email_source=notifications\u0026email_token=BRF3HG5NILSUSX55NE6NMTD5JUQTZA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRXGUYTOMBTGMZKM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJK3HNVQWS3C7MNWGSY3L#issuecomment-5275170332",
"url": "#766?email_source=notifications\u0026email_token=BRF3HG5NILSUSX55NE6NMTD5JUQTZA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRXGUYTOMBTGMZKM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJK3HNVQWS3C7MNWGSY3L#issuecomment-5275170332",
"name": "View Pull Request"
},
"description": "View this Pull Request on GitHub",
"publisher": {
***@***.***": "Organization",
"name": "GitHub",
"url": "https://github.com"
}
}
]
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
cleanupOrphanedVectors()runs its two DELETEs (vectors_vec, thencontent_vectors) as separate implicit transactions. An interruption between them — process crash, orSQLITE_BUSYfrom a concurrent writer hitting the second statement — desyncs the two tables: the embedding rows are already gone fromvectors_vecwhilecontent_vectorsstill reports the chunks as embedded.Since these rows are orphaned (no active document) and vector search post-filters on
documents.active = 1, live search is unaffected right away. The failure is latent: qmd is content-addressable, so if the same content later returns and that hash is reactivated, the stalecontent_vectorsrows makegetHashesNeedingEmbeddingtreat it as already embedded —qmd embedskips it, and the document is silently unsearchable by vector with no orphan left for a futurecleanupto catch.What changed
db.transaction(), executed asBEGIN IMMEDIATE: the count reads before the DELETEs write, and upgrading a deferred read snapshot under a concurrent WAL writer fails withSQLITE_BUSY_SNAPSHOTinstead of honoring the busy timeout. The sharedDatabaseinterface type now reflects the.immediatevariant both drivers already expose.documentsconcurrently.bun:sqliteimplement nested transaction functions via savepoints (an immediate transaction invoked inside an outer transaction becomes a savepoint).[Unreleased].Verification
test/store.test.ts(cleanupOrphanedVectors atomicity):vectors_vecDELETE rolls back (this test fails against the previous code) and the connection stays usable for a plain retrybun run test:typesclean,bun run buildpassesbun test)