Skip to content

Commit fe94171

Browse files
asg017claude
andcommitted
Finalize all cached vec0 stmts on commit (fixes #295)
vec0Sync only finalized stmtLatestChunk and the four stmtRowids* stmts. The IVF/DiskANN/vectors stmts persisted on the vtab until xDisconnect, which blocked sqlite3_close() (non-v2) with SQLITE_BUSY — the original mozStorage case from #295. The same leak also broke VACUUM with "SQL statements in progress" after any DiskANN operation. Switch vec0Sync to call vec0_free_resources, which already finalizes the full cache. Also fix a latent bug: the DiskANN block in vec0_free_resources was nested inside #if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE, so in the default build (DiskANN on, IVF off) those finalizes were unreachable even from xDisconnect/xDestroy. Split into two independent #if guards. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8b81f40 commit fe94171

2 files changed

Lines changed: 38 additions & 23 deletions

File tree

sqlite-vec.c

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3695,13 +3695,15 @@ void vec0_free_resources(vec0_vtab *p) {
36953695
sqlite3_finalize(p->stmtIvfRowidMapLookup[i]); p->stmtIvfRowidMapLookup[i] = NULL;
36963696
sqlite3_finalize(p->stmtIvfRowidMapDelete[i]); p->stmtIvfRowidMapDelete[i] = NULL;
36973697
sqlite3_finalize(p->stmtIvfCentroidsAll[i]); p->stmtIvfCentroidsAll[i] = NULL;
3698+
}
3699+
#endif
36983700
#if SQLITE_VEC_ENABLE_DISKANN
3701+
for (int i = 0; i < VEC0_MAX_VECTOR_COLUMNS; i++) {
36993702
sqlite3_finalize(p->stmtDiskannNodeRead[i]); p->stmtDiskannNodeRead[i] = NULL;
37003703
sqlite3_finalize(p->stmtDiskannNodeWrite[i]); p->stmtDiskannNodeWrite[i] = NULL;
37013704
sqlite3_finalize(p->stmtDiskannNodeInsert[i]); p->stmtDiskannNodeInsert[i] = NULL;
37023705
sqlite3_finalize(p->stmtVectorsRead[i]); p->stmtVectorsRead[i] = NULL;
37033706
sqlite3_finalize(p->stmtVectorsInsert[i]); p->stmtVectorsInsert[i] = NULL;
3704-
#endif
37053707
}
37063708
#endif
37073709
}
@@ -10370,28 +10372,7 @@ static int vec0Begin(sqlite3_vtab *pVTab) {
1037010372
return SQLITE_OK;
1037110373
}
1037210374
static int vec0Sync(sqlite3_vtab *pVTab) {
10373-
UNUSED_PARAMETER(pVTab);
10374-
vec0_vtab *p = (vec0_vtab *)pVTab;
10375-
if (p->stmtLatestChunk) {
10376-
sqlite3_finalize(p->stmtLatestChunk);
10377-
p->stmtLatestChunk = NULL;
10378-
}
10379-
if (p->stmtRowidsInsertRowid) {
10380-
sqlite3_finalize(p->stmtRowidsInsertRowid);
10381-
p->stmtRowidsInsertRowid = NULL;
10382-
}
10383-
if (p->stmtRowidsInsertId) {
10384-
sqlite3_finalize(p->stmtRowidsInsertId);
10385-
p->stmtRowidsInsertId = NULL;
10386-
}
10387-
if (p->stmtRowidsUpdatePosition) {
10388-
sqlite3_finalize(p->stmtRowidsUpdatePosition);
10389-
p->stmtRowidsUpdatePosition = NULL;
10390-
}
10391-
if (p->stmtRowidsGetChunkPosition) {
10392-
sqlite3_finalize(p->stmtRowidsGetChunkPosition);
10393-
p->stmtRowidsGetChunkPosition = NULL;
10394-
}
10375+
vec0_free_resources((vec0_vtab *)pVTab);
1039510376
return SQLITE_OK;
1039610377
}
1039710378
static int vec0Commit(sqlite3_vtab *pVTab) {

tests/test-cache-finalize.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Regression tests for #295: vec0 must finalize cached prepared statements
2+
on every commit, not just the rowid subset.
3+
4+
Before the fix, `vec0Sync` only finalized `stmtLatestChunk` and the four
5+
`stmtRowids*` stmts; the DiskANN/IVF/vectors-read stmts persisted on the
6+
vtab indefinitely. Symptom: VACUUM after any DiskANN operation failed with
7+
"SQL statements in progress" because the cached stmts kept the connection
8+
busy. (The same leak also caused `sqlite3_close()` non-v2 to return
9+
SQLITE_BUSY — the original Firefox case in issue #295.)
10+
11+
A separate latent bug — the DiskANN finalize block in `vec0_free_resources`
12+
was nested inside `#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE`, so even
13+
xDisconnect/xDestroy didn't finalize DiskANN stmts in the default build.
14+
"""
15+
from helpers import _f32
16+
17+
18+
def test_vacuum_after_diskann_inserts(db):
19+
db.execute(
20+
"create virtual table v using vec0("
21+
"a float[8] indexed by diskann(neighbor_quantizer=binary))"
22+
)
23+
for i in range(1, 11):
24+
db.execute("insert into v(rowid, a) values (?, ?)",
25+
(i, _f32([0.1 * i] * 8)))
26+
db.commit()
27+
db.execute("VACUUM")
28+
29+
30+
def test_vacuum_after_flat_inserts(db):
31+
db.execute("create virtual table v using vec0(a float[2])")
32+
db.execute("insert into v(rowid, a) values (1, ?)", (_f32([0.1, 0.2]),))
33+
db.commit()
34+
db.execute("VACUUM")

0 commit comments

Comments
 (0)