Skip to content

Commit ee9bd2b

Browse files
asg017claude
andauthored
Fix SQLITE_DONE leak in ClearMetadata that broke DELETE on long text metadata (#274) (#275)
vec0Update_Delete_ClearMetadata's long-text branch runs a DELETE via sqlite3_step, which returns SQLITE_DONE (101) on success. The code checked for failure but never normalized the success case to SQLITE_OK. The function's epilogue returned SQLITE_DONE as-is, which the caller (vec0Update_Delete) treated as an error, aborting the DELETE scan and silently leaving rows behind. - Normalize rc to SQLITE_OK after successful sqlite3_step in ClearMetadata - Move sqlite3_finalize before the rc check (cleanup on all paths) - Add test_delete_by_metadata_with_long_text regression test - Update test_deletes snapshot (row 3 now correctly deleted) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dfd8dc5 commit ee9bd2b

3 files changed

Lines changed: 43 additions & 26 deletions

File tree

sqlite-vec.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8944,11 +8944,17 @@ int vec0Update_Delete_ClearMetadata(vec0_vtab *p, int metadata_idx, i64 rowid, i
89448944
}
89458945
sqlite3_bind_int64(stmt, 1, rowid);
89468946
rc = sqlite3_step(stmt);
8947+
sqlite3_finalize(stmt);
89478948
if(rc != SQLITE_DONE) {
89488949
rc = SQLITE_ERROR;
89498950
goto done;
89508951
}
8951-
sqlite3_finalize(stmt);
8952+
// Fix for https://github.com/asg017/sqlite-vec/issues/274
8953+
// sqlite3_step returns SQLITE_DONE (101) on DML success, but the
8954+
// `done:` epilogue treats anything other than SQLITE_OK as an error.
8955+
// Without this, SQLITE_DONE propagates up to vec0Update_Delete,
8956+
// which aborts the DELETE scan and silently drops remaining rows.
8957+
rc = SQLITE_OK;
89528958
}
89538959
break;
89548960
}

tests/__snapshots__/test-metadata.ambr

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
OrderedDict({
2828
'chunk_id': 1,
2929
'size': 8,
30-
'validity': b'\x06',
31-
'rowids': b'\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
30+
'validity': b'\x02',
31+
'rowids': b'\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
3232
}),
3333
]),
3434
}),
@@ -37,7 +37,7 @@
3737
'rows': list([
3838
OrderedDict({
3939
'rowid': 1,
40-
'data': b'\x06',
40+
'data': b'\x02',
4141
}),
4242
]),
4343
}),
@@ -46,7 +46,7 @@
4646
'rows': list([
4747
OrderedDict({
4848
'rowid': 1,
49-
'data': b'\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
49+
'data': b'\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
5050
}),
5151
]),
5252
}),
@@ -55,7 +55,7 @@
5555
'rows': list([
5656
OrderedDict({
5757
'rowid': 1,
58-
'data': b'\x00\x00\x00\x00\x00\x00\x00\x00\x9a\x99\x99\x99\x99\x99\x01@ffffff\n@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
58+
'data': b'\x00\x00\x00\x00\x00\x00\x00\x00\x9a\x99\x99\x99\x99\x99\x01@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
5959
}),
6060
]),
6161
}),
@@ -64,17 +64,13 @@
6464
'rows': list([
6565
OrderedDict({
6666
'rowid': 1,
67-
'data': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00test2\x00\x00\x00\x00\x00\x00\x00\r\x00\x00\x00123456789012\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
67+
'data': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00test2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
6868
}),
6969
]),
7070
}),
7171
'v_metadatatext03': OrderedDict({
7272
'sql': 'select * from v_metadatatext03',
7373
'rows': list([
74-
OrderedDict({
75-
'rowid': 3,
76-
'data': '1234567890123',
77-
}),
7874
]),
7975
}),
8076
'v_rowids': OrderedDict({
@@ -86,20 +82,14 @@
8682
'chunk_id': 1,
8783
'chunk_offset': 1,
8884
}),
89-
OrderedDict({
90-
'rowid': 3,
91-
'id': None,
92-
'chunk_id': 1,
93-
'chunk_offset': 2,
94-
}),
9585
]),
9686
}),
9787
'v_vector_chunks00': OrderedDict({
9888
'sql': 'select * from v_vector_chunks00',
9989
'rows': list([
10090
OrderedDict({
10191
'rowid': 1,
102-
'vectors': b'\x00\x00\x00\x00""""3333\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
92+
'vectors': b'\x00\x00\x00\x00""""\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
10393
}),
10494
]),
10595
}),
@@ -370,14 +360,6 @@
370360
'f': 2.2,
371361
't': 'test2',
372362
}),
373-
OrderedDict({
374-
'rowid': 3,
375-
'vector': b'3333',
376-
'b': 1,
377-
'n': 3,
378-
'f': 3.3,
379-
't': '1234567890123',
380-
}),
381363
]),
382364
})
383365
# ---

tests/test-metadata.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,35 @@ def test_deletes(db, snapshot):
265265
assert vec0_shadow_table_contents(db, "v") == snapshot()
266266

267267

268+
def test_delete_by_metadata_with_long_text(db):
269+
"""Regression for https://github.com/asg017/sqlite-vec/issues/274.
270+
271+
ClearMetadata left rc=SQLITE_DONE after the long-text DELETE, which
272+
propagated as an error and silently aborted the DELETE scan.
273+
"""
274+
db.execute(
275+
"create virtual table v using vec0("
276+
" tag text, embedding float[4], chunk_size=8"
277+
")"
278+
)
279+
for i in range(6):
280+
db.execute(
281+
"insert into v(tag, embedding) values (?, zeroblob(16))",
282+
[f"long_text_value_{i}"],
283+
)
284+
for i in range(4):
285+
db.execute(
286+
"insert into v(tag, embedding) values (?, zeroblob(16))",
287+
[f"long_text_value_0"],
288+
)
289+
assert db.execute("select count(*) from v").fetchone()[0] == 10
290+
291+
# DELETE by metadata WHERE — the pattern from the issue
292+
db.execute("delete from v where tag = 'long_text_value_0'")
293+
assert db.execute("select count(*) from v where tag = 'long_text_value_0'").fetchone()[0] == 0
294+
assert db.execute("select count(*) from v").fetchone()[0] == 5
295+
296+
268297
def test_knn(db, snapshot):
269298
db.execute(
270299
"create virtual table v using vec0(vector float[1], name text, chunk_size=8)"

0 commit comments

Comments
 (0)