From 0c840237f354559281fe3ba0c1fb86d8f1d17241 Mon Sep 17 00:00:00 2001 From: Rolf Rando Date: Tue, 5 May 2026 12:21:45 -0700 Subject: [PATCH 1/2] Add 'release-cached-stmts' vec0 command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hosts that embed sqlite-vec sometimes need to finalize vec0's cached prepared statements without renaming or destroying the table. The cache (stmtRowidsInsertRowid, stmtDiskannNodeRead, stmtVectorsInsert, etc.) is otherwise only finalized in xDisconnect / xDestroy / xRename, which is too late or too heavy for some use cases. Concrete motivating case: Firefox's mozStorage calls sqlite3_close() on shutdown, which fails (and asserts in debug builds) while any sqlite3_stmt* is still alive on the connection — including vec0's. The existing workarounds are either an ALTER TABLE rename pair (bumps the schema cookie and writes to shadow tables) or closing/reopening the connection (heavy hammer). Adds a new index-type-agnostic command dispatched via the existing FTS5-style command column: INSERT INTO vec_history(vec_history) VALUES('release-cached-stmts'); This calls vec0_free_resources(p) directly. Cached statements are re-prepared lazily on next use. --- sqlite-vec.c | 29 ++++++++++++++++ tests/test-release-cached-stmts.py | 56 ++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 tests/test-release-cached-stmts.py diff --git a/sqlite-vec.c b/sqlite-vec.c index dc33c67..b02ba23 100644 --- a/sqlite-vec.c +++ b/sqlite-vec.c @@ -10271,6 +10271,33 @@ int vec0Update_Update(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv) { return SQLITE_OK; } +/* + * Index-type-agnostic vec0 control commands dispatched via the FTS5-style + * command column. Returns SQLITE_OK if handled, SQLITE_EMPTY otherwise. + * + * Recognized: + * "release-cached-stmts" + * Finalize this vec0 vtab's cached prepared statements + * (stmtRowidsInsertRowid, stmtDiskannNodeRead, etc.) without renaming + * or destroying the table. They are re-prepared lazily on next use. + * + * Hosts embedding sqlite-vec sometimes need this. mozStorage in + * Firefox, for example, calls sqlite3_close() on shutdown, which + * fails (and asserts in debug builds) while any sqlite3_stmt* is + * still live on the connection. vec0's cache would normally only be + * finalized in xDisconnect, which runs *after* that close attempt. + * Issuing this command before close lets the connection drain + * cleanly. Cheaper than the rename-pair workaround because it + * doesn't bump the schema cookie or write to shadow tables. + */ +static int vec0_handle_general_command(vec0_vtab *p, const char *cmd) { + if (strcmp(cmd, "release-cached-stmts") == 0) { + vec0_free_resources(p); + return SQLITE_OK; + } + return SQLITE_EMPTY; +} + static int vec0Update(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv, sqlite_int64 *pRowid) { // DELETE operation @@ -10297,6 +10324,8 @@ static int vec0Update(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv, if (cmdRc == SQLITE_EMPTY) cmdRc = diskann_handle_command(p, cmd); #endif + if (cmdRc == SQLITE_EMPTY) + cmdRc = vec0_handle_general_command(p, cmd); if (cmdRc == SQLITE_EMPTY) { vtab_set_error(pVTab, "unknown vec0 command: '%s'", cmd); return SQLITE_ERROR; diff --git a/tests/test-release-cached-stmts.py b/tests/test-release-cached-stmts.py new file mode 100644 index 0000000..aa68f60 --- /dev/null +++ b/tests/test-release-cached-stmts.py @@ -0,0 +1,56 @@ +import sqlite3 +import pytest +from helpers import _f32 + + +def test_release_cached_stmts_basic(db): + """The release-cached-stmts command should succeed and not affect data.""" + db.execute("create virtual table v using vec0(a float[2], chunk_size=8)") + db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1, 0.2])]) + db.execute("insert into v(rowid, a) values (2, ?)", [_f32([0.3, 0.4])]) + + db.execute("insert into v(v) values ('release-cached-stmts')") + + # Data should still be there and queryable; vec0's cached statements + # are re-prepared on demand. + rows = db.execute( + "select rowid from v where a match ? and k=10", + [_f32([0.1, 0.2])], + ).fetchall() + assert sorted(r[0] for r in rows) == [1, 2] + + +def test_release_cached_stmts_before_any_use(db): + """Issuing the command before any inserts should be a no-op.""" + db.execute("create virtual table v using vec0(a float[2], chunk_size=8)") + db.execute("insert into v(v) values ('release-cached-stmts')") + # Inserts and queries still work after. + db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1, 0.2])]) + rows = db.execute( + "select rowid from v where a match ? and k=10", + [_f32([0.1, 0.2])], + ).fetchall() + assert rows[0][0] == 1 + + +def test_release_cached_stmts_diskann(db): + """Works on DiskANN-indexed tables (the original motivating case).""" + db.execute(""" + create virtual table v using vec0( + a float[8] indexed by diskann(neighbor_quantizer=binary) + ) + """) + db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 8)]) + db.execute("insert into v(v) values ('release-cached-stmts')") + rows = db.execute( + "select rowid from v where a match ? and k=10", + [_f32([0.1] * 8)], + ).fetchall() + assert rows[0][0] == 1 + + +def test_release_cached_stmts_unknown_subcommand(db): + """Unknown vec0 commands should still error as before.""" + db.execute("create virtual table v using vec0(a float[2], chunk_size=8)") + with pytest.raises(sqlite3.OperationalError, match="unknown vec0 command"): + db.execute("insert into v(v) values ('not-a-real-command')") From 065920739ca29201d93e5ccab5be632ca61b398a Mon Sep 17 00:00:00 2001 From: Rolf Rando Date: Tue, 5 May 2026 13:01:31 -0700 Subject: [PATCH 2/2] update comment --- sqlite-vec.c | 9 --------- 1 file changed, 9 deletions(-) diff --git a/sqlite-vec.c b/sqlite-vec.c index b02ba23..e73c912 100644 --- a/sqlite-vec.c +++ b/sqlite-vec.c @@ -10280,15 +10280,6 @@ int vec0Update_Update(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv) { * Finalize this vec0 vtab's cached prepared statements * (stmtRowidsInsertRowid, stmtDiskannNodeRead, etc.) without renaming * or destroying the table. They are re-prepared lazily on next use. - * - * Hosts embedding sqlite-vec sometimes need this. mozStorage in - * Firefox, for example, calls sqlite3_close() on shutdown, which - * fails (and asserts in debug builds) while any sqlite3_stmt* is - * still live on the connection. vec0's cache would normally only be - * finalized in xDisconnect, which runs *after* that close attempt. - * Issuing this command before close lets the connection drain - * cleanly. Cheaper than the rename-pair workaround because it - * doesn't bump the schema cookie or write to shadow tables. */ static int vec0_handle_general_command(vec0_vtab *p, const char *cmd) { if (strcmp(cmd, "release-cached-stmts") == 0) {