diff --git a/sqlite-vec.c b/sqlite-vec.c index 0a33577..7af3b6a 100644 --- a/sqlite-vec.c +++ b/sqlite-vec.c @@ -3695,13 +3695,15 @@ void vec0_free_resources(vec0_vtab *p) { sqlite3_finalize(p->stmtIvfRowidMapLookup[i]); p->stmtIvfRowidMapLookup[i] = NULL; sqlite3_finalize(p->stmtIvfRowidMapDelete[i]); p->stmtIvfRowidMapDelete[i] = NULL; sqlite3_finalize(p->stmtIvfCentroidsAll[i]); p->stmtIvfCentroidsAll[i] = NULL; + } +#endif #if SQLITE_VEC_ENABLE_DISKANN + for (int i = 0; i < VEC0_MAX_VECTOR_COLUMNS; i++) { sqlite3_finalize(p->stmtDiskannNodeRead[i]); p->stmtDiskannNodeRead[i] = NULL; sqlite3_finalize(p->stmtDiskannNodeWrite[i]); p->stmtDiskannNodeWrite[i] = NULL; sqlite3_finalize(p->stmtDiskannNodeInsert[i]); p->stmtDiskannNodeInsert[i] = NULL; sqlite3_finalize(p->stmtVectorsRead[i]); p->stmtVectorsRead[i] = NULL; sqlite3_finalize(p->stmtVectorsInsert[i]); p->stmtVectorsInsert[i] = NULL; -#endif } #endif } @@ -10370,28 +10372,7 @@ static int vec0Begin(sqlite3_vtab *pVTab) { return SQLITE_OK; } static int vec0Sync(sqlite3_vtab *pVTab) { - UNUSED_PARAMETER(pVTab); - vec0_vtab *p = (vec0_vtab *)pVTab; - if (p->stmtLatestChunk) { - sqlite3_finalize(p->stmtLatestChunk); - p->stmtLatestChunk = NULL; - } - if (p->stmtRowidsInsertRowid) { - sqlite3_finalize(p->stmtRowidsInsertRowid); - p->stmtRowidsInsertRowid = NULL; - } - if (p->stmtRowidsInsertId) { - sqlite3_finalize(p->stmtRowidsInsertId); - p->stmtRowidsInsertId = NULL; - } - if (p->stmtRowidsUpdatePosition) { - sqlite3_finalize(p->stmtRowidsUpdatePosition); - p->stmtRowidsUpdatePosition = NULL; - } - if (p->stmtRowidsGetChunkPosition) { - sqlite3_finalize(p->stmtRowidsGetChunkPosition); - p->stmtRowidsGetChunkPosition = NULL; - } + vec0_free_resources((vec0_vtab *)pVTab); return SQLITE_OK; } static int vec0Commit(sqlite3_vtab *pVTab) { diff --git a/tests/test-cache-finalize.py b/tests/test-cache-finalize.py new file mode 100644 index 0000000..c81f45a --- /dev/null +++ b/tests/test-cache-finalize.py @@ -0,0 +1,34 @@ +"""Regression tests for #295: vec0 must finalize cached prepared statements +on every commit, not just the rowid subset. + +Before the fix, `vec0Sync` only finalized `stmtLatestChunk` and the four +`stmtRowids*` stmts; the DiskANN/IVF/vectors-read stmts persisted on the +vtab indefinitely. Symptom: VACUUM after any DiskANN operation failed with +"SQL statements in progress" because the cached stmts kept the connection +busy. (The same leak also caused `sqlite3_close()` non-v2 to return +SQLITE_BUSY — the original Firefox case in issue #295.) + +A separate latent bug — the DiskANN finalize block in `vec0_free_resources` +was nested inside `#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE`, so even +xDisconnect/xDestroy didn't finalize DiskANN stmts in the default build. +""" +from helpers import _f32 + + +def test_vacuum_after_diskann_inserts(db): + db.execute( + "create virtual table v using vec0(" + "a float[8] indexed by diskann(neighbor_quantizer=binary))" + ) + for i in range(1, 11): + db.execute("insert into v(rowid, a) values (?, ?)", + (i, _f32([0.1 * i] * 8))) + db.commit() + db.execute("VACUUM") + + +def test_vacuum_after_flat_inserts(db): + db.execute("create virtual table v using vec0(a float[2])") + db.execute("insert into v(rowid, a) values (1, ?)", (_f32([0.1, 0.2]),)) + db.commit() + db.execute("VACUUM")