From 455306493e71cb8ddf61187ae5972482f574b1e7 Mon Sep 17 00:00:00 2001 From: Zhaoqi Xu Date: Sat, 24 Oct 2026 06:56:48 -0400 Subject: [PATCH] libsql-sqlite3: Clear vector index on DELETE without WHERE A DELETE without a WHERE clause takes the truncate optimization, which runs OP_Clear on the table and each index b-tree. A vector index keeps its entries in its shadow table rather than in its b-tree, so the shadow rows were left behind and the next insert that reused a rowid failed with "vector index(insert): failed to insert shadow row". Open each vector index with OPFLAG_FORDELETE on that path, which clears it the same way REINDEX does. Fixes #2289 --- .../bundled/SQLite3MultipleCiphers/src/sqlite3.c | 15 +++++++++++++++ libsql-ffi/bundled/src/sqlite3.c | 15 +++++++++++++++ libsql-sqlite3/src/delete.c | 14 ++++++++++++++ libsql-sqlite3/test/libsql_vector_index.test | 11 +++++++++++ 4 files changed, 55 insertions(+) diff --git a/libsql-ffi/bundled/SQLite3MultipleCiphers/src/sqlite3.c b/libsql-ffi/bundled/SQLite3MultipleCiphers/src/sqlite3.c index 62a917fb9b..181b3309e3 100644 --- a/libsql-ffi/bundled/SQLite3MultipleCiphers/src/sqlite3.c +++ b/libsql-ffi/bundled/SQLite3MultipleCiphers/src/sqlite3.c @@ -53,6 +53,7 @@ ** src/btree.h ** src/build.c ** src/callback.c +** src/delete.c ** src/func.c ** src/insert.c ** src/loadext.c @@ -131230,6 +131231,20 @@ SQLITE_PRIVATE void sqlite3DeleteFrom( } for(pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext){ assert( pIdx->pSchema==pTab->pSchema ); +#ifndef SQLITE_OMIT_VECTOR + /* + * Vector index entries live in the index's shadow table, not in its + * b-tree, so OP_Clear alone leaves them behind. Open the index with + * OPFLAG_FORDELETE, which clears it, as REINDEX does. + */ + if( IsVectorIndex(pIdx) ){ + int iVecCur = pParse->nTab++; + sqlite3VdbeAddOp3(v, OP_OpenVectorIdx, iVecCur, pIdx->tnum, iDb); + sqlite3VdbeSetP4KeyInfo(pParse, pIdx); + sqlite3VdbeChangeP5(v, OPFLAG_FORDELETE); + sqlite3VdbeAddOp1(v, OP_Close, iVecCur); + } +#endif if( IsPrimaryKeyIndex(pIdx) && !HasRowid(pTab) ){ sqlite3VdbeAddOp3(v, OP_Clear, pIdx->tnum, iDb, memCnt ? memCnt : -1); }else{ diff --git a/libsql-ffi/bundled/src/sqlite3.c b/libsql-ffi/bundled/src/sqlite3.c index 62a917fb9b..181b3309e3 100644 --- a/libsql-ffi/bundled/src/sqlite3.c +++ b/libsql-ffi/bundled/src/sqlite3.c @@ -53,6 +53,7 @@ ** src/btree.h ** src/build.c ** src/callback.c +** src/delete.c ** src/func.c ** src/insert.c ** src/loadext.c @@ -131230,6 +131231,20 @@ SQLITE_PRIVATE void sqlite3DeleteFrom( } for(pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext){ assert( pIdx->pSchema==pTab->pSchema ); +#ifndef SQLITE_OMIT_VECTOR + /* + * Vector index entries live in the index's shadow table, not in its + * b-tree, so OP_Clear alone leaves them behind. Open the index with + * OPFLAG_FORDELETE, which clears it, as REINDEX does. + */ + if( IsVectorIndex(pIdx) ){ + int iVecCur = pParse->nTab++; + sqlite3VdbeAddOp3(v, OP_OpenVectorIdx, iVecCur, pIdx->tnum, iDb); + sqlite3VdbeSetP4KeyInfo(pParse, pIdx); + sqlite3VdbeChangeP5(v, OPFLAG_FORDELETE); + sqlite3VdbeAddOp1(v, OP_Close, iVecCur); + } +#endif if( IsPrimaryKeyIndex(pIdx) && !HasRowid(pTab) ){ sqlite3VdbeAddOp3(v, OP_Clear, pIdx->tnum, iDb, memCnt ? memCnt : -1); }else{ diff --git a/libsql-sqlite3/src/delete.c b/libsql-sqlite3/src/delete.c index 8fac7c2f32..d4b32dff33 100644 --- a/libsql-sqlite3/src/delete.c +++ b/libsql-sqlite3/src/delete.c @@ -484,6 +484,20 @@ void sqlite3DeleteFrom( } for(pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext){ assert( pIdx->pSchema==pTab->pSchema ); +#ifndef SQLITE_OMIT_VECTOR + /* + * Vector index entries live in the index's shadow table, not in its + * b-tree, so OP_Clear alone leaves them behind. Open the index with + * OPFLAG_FORDELETE, which clears it, as REINDEX does. + */ + if( IsVectorIndex(pIdx) ){ + int iVecCur = pParse->nTab++; + sqlite3VdbeAddOp3(v, OP_OpenVectorIdx, iVecCur, pIdx->tnum, iDb); + sqlite3VdbeSetP4KeyInfo(pParse, pIdx); + sqlite3VdbeChangeP5(v, OPFLAG_FORDELETE); + sqlite3VdbeAddOp1(v, OP_Close, iVecCur); + } +#endif if( IsPrimaryKeyIndex(pIdx) && !HasRowid(pTab) ){ sqlite3VdbeAddOp3(v, OP_Clear, pIdx->tnum, iDb, memCnt ? memCnt : -1); }else{ diff --git a/libsql-sqlite3/test/libsql_vector_index.test b/libsql-sqlite3/test/libsql_vector_index.test index c3cd148577..b184b824c5 100644 --- a/libsql-sqlite3/test/libsql_vector_index.test +++ b/libsql-sqlite3/test/libsql_vector_index.test @@ -98,6 +98,17 @@ do_execsql_test vector-delete { SELECT rowid FROM vector_top_k('t_delete_idx', vector('[1,1,1]'), 1); } {10 20} +do_execsql_test vector-delete-all { + CREATE TABLE t_delete_all( id INTEGER PRIMARY KEY, v FLOAT32(3) ); + CREATE INDEX t_delete_all_idx ON t_delete_all( libsql_vector_idx(v) ); + INSERT INTO t_delete_all VALUES (1, vector('[1,1,1]')); + INSERT INTO t_delete_all VALUES (2, vector('[-1,-1,-1]')); + DELETE FROM t_delete_all; + SELECT count(*) FROM t_delete_all_idx_shadow; + INSERT INTO t_delete_all VALUES (1, vector('[1,1,1]')); + SELECT rowid FROM vector_top_k('t_delete_all_idx', vector('[-1,-1,-1]'), 2); +} {0 1} + do_execsql_test vector-update { CREATE TABLE t_update( id INTEGER PRIMARY KEY, v FLOAT32(3) ); CREATE INDEX t_update_idx ON t_update( libsql_vector_idx(v) );