Skip to content

Return the SQLite errors of the IVF commands and inserts - #354

Open
MayCXC wants to merge 3 commits into
asg017:mainfrom
MayCXC:ivf-command-errors
Open

MayCXC wants to merge 3 commits into
asg017:mainfrom
MayCXC:ivf-command-errors

Conversation

@MayCXC

@MayCXC MayCXC commented Oct 23, 2026 •

Copy link
Copy Markdown

When a statement inside an IVF command fails, an interrupt included, the command carries on, and it can report success after rebuilding the index from part of its vectors.

db.execute("create virtual table t using vec0(v float[4] indexed by ivf(nlist=2, nprobe=2))")
# 20 inserts, then compute-centroids
steps = 0
def interrupt_one_statement():  # step 9 is the first statement clear-centroids runs
    global steps
    steps += 1
    return steps == 9
db.set_progress_handler(interrupt_one_statement, 1)
db.execute("insert into t(t) values ('clear-centroids')")  # succeeds
db.set_progress_handler(None, 0)
db.execute("select rowid from t where v match ? and k = 20", [q]).fetchall()  # []: every row is gone from the index

The commands, and the cell writes behind every insert, ignored the result of most of their statements:

  • ivf_load_all_vectors() stopped at a failed step and returned what it had read as all the vectors. compute-centroids and clear-centroids then deleted the cells and rebuilt them from that part, and compute-centroids deleted and rewrote _ivf_vectors from it too.
  • assign-vectors deleted the unassigned cells whatever had happened to the vectors it was moving out of them.
  • ivf_cell_insert() wrote a vector, its rowid and its rowid-map entry without checking any of the writes, so an insert could report success with its row missing from the index.
  • Where a failure was reported, it was as "SQL logic error" or "No vectors" rather than the failure's own error.
  • compute-centroids wrapped its work in SAVEPOINT ivf_train, which never opened: SQLite refuses a savepoint while a write statement is in progress ("cannot open savepoint - SQL statements in progress"), and the INSERT running the command is one. Its ROLLBACK TO restored nothing.

Every statement and blob access in the commands, ivf_exec(), ivf_is_trained(), ivf_load_all_vectors() and ivf_cell_insert() now returns its error, and the command or insert returns it. In autocommit mode, and for an interrupt or a memory, I/O or full-disk error anywhere, SQLite then rolls the transaction back with every write the command made. Any other error inside BEGIN leaves the writes before it, as it leaves the writes of any failed vec0 statement there: a one-row INSERT into a virtual table opens no statement journal, which is #333. The savepoint is removed.

Measured by interrupting one statement at each VM step of a command on 20 rows (with the IVF define):

command main: runs that lost rows here
compute-centroids 69 of 477, and 413 reported success, 28 "SQL logic error", 23 "No vectors" 0 of 471, each reporting "interrupted"
clear-centroids 679 of 1666 0 of 1666
assign-vectors 716 of 1853 0 of 1863

Here, the clear-centroids and assign-vectors runs that complete despite the interrupt are those where the interrupted statement is the cell lookup in ivf_cell_find_or_create(), which then opens a new cell: one cell more, no row lost.

Test: test_ivf_command_interrupted_at_each_step in test-ivf.py interrupts each step of compute-centroids (without a quantizer and with int8), clear-centroids and assign-vectors, and checks that each run reports the interrupt or completes, that every row is still found, and that the command then runs to the end with every row found. All four fail on main and pass here. The Python suite passes (with -DSQLITE_VEC_EXPERIMENTAL_IVF_ENABLE=1, 308 passed, 4 skipped; without it, 239 passed, 73 skipped), under ASan and UBSan as well, and both builds' warnings are main's.

This builds on #351 and carries its commit: both change how the commands handle the trained state, and on main they touch adjacent lines. It merges cleanly with every other open PR that touches the IVF code, #347 and #349 to #353 included, but not with #313, which rewrites set-centroid, assign-vectors and clear-centroids to store quantized vectors. The two changes are independent, #313 changing what those commands write and this checking each write, so whichever lands second takes both by hand in those three functions.

Deletes and KNN queries still pass over a failed step, measured the same way: a delete reports success and leaves its row in the index in 25 of 122 runs, and a KNN query on a trained index returns fewer rows than it should, as if they were all, in 52 of 146. ivf_delete() and ivf_query_knn() are changed by #347 and #350, so this leaves them as they are.

Origin. Found in the IVF port to photostructure/sqlite-vec, photostructure#12 (65817f4), which lists the ignored results and the savepoint among the port's differences from upstream. The test interrupts a command at each step as mceachen's test_interrupt_is_reported_as_interrupt does vec0's statements in photostructure/sqlite-vec@7a9ab11, a test the port extends to IVF in debd171; the change and the test are written here for asg017's main. No open or closed PR or issue here makes the same change.

ivf_is_trained() kept the trained state per connection once it had read
it, and compute-centroids, set-centroid and clear-centroids set that
cache as they ran. Another connection's command, or a rolled-back one,
left it wrong: a connection that had queried before another trained the
index scanned only the emptied unassigned cells and found no rows, and
one that had trained it, then saw another connection clear it or rolled
its own training back, failed every insert and query with "SQL logic
error". The state is now read from _info each time, so it is always
that of the transaction reading it.
The IVF commands, and the cell writes behind every insert, ignored the
result of most of their statements, so one that failed part way, an
interrupt included, left the command or insert to carry on and report
success over a half-rebuilt index. compute-centroids and clear-centroids
read every vector through ivf_load_all_vectors(), which stopped at a
failed step and returned what it had read as all of them, then deleted
the cells and rebuilt them from that part, and compute-centroids its
_ivf_vectors too. assign-vectors deleted the unassigned cells whatever
happened to the vectors it was moving out of them, and ivf_cell_insert()
wrote a vector, its rowid and its map entry without checking any of
them. Where a failure was reported, it was as "SQL logic error" or "No
vectors". compute-centroids also wrapped its work in a SAVEPOINT that
never opened, since none can while the INSERT running the command is in
progress.

Every statement and blob access in the commands, ivf_exec(),
ivf_is_trained(), ivf_load_all_vectors() and ivf_cell_insert() now
returns its error, and the command or insert returns it to SQLite, which
undoes the statement's writes. The savepoint is gone.
An error the command returns is rolled back with the transaction in
autocommit mode, or inside BEGIN when it is an interrupt or a memory,
I/O or full-disk error. A constraint or other error inside BEGIN leaves
the writes before it, since a single-row INSERT into a virtual table
opens no statement journal.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant