diff --git a/sqlite-vec.c b/sqlite-vec.c index db79d0b..53c4635 100644 --- a/sqlite-vec.c +++ b/sqlite-vec.c @@ -10055,6 +10055,26 @@ int vec0Update_Update(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv) { continue; } + // Block vector UPDATE for index types that don't implement it — + // the DiskANN graph / IVF lists would become stale. + { + enum Vec0IndexType idx_type = p->vector_columns[vector_idx].index_type; + const char *idx_name = NULL; + if (idx_type == VEC0_INDEX_TYPE_DISKANN) idx_name = "DiskANN"; +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + else if (idx_type == VEC0_INDEX_TYPE_IVF) idx_name = "IVF"; +#endif + if (idx_name) { + vtab_set_error( + &p->base, + "UPDATE on vector column \"%.*s\" is not supported for %s indexes.", + p->vector_columns[vector_idx].name_length, + p->vector_columns[vector_idx].name, + idx_name); + return SQLITE_ERROR; + } + } + rc = vec0Update_UpdateVectorColumn(p, chunk_id, chunk_offset, vector_idx, valueVector, rowid); if (rc != SQLITE_OK) { diff --git a/tests/test-diskann.py b/tests/test-diskann.py index 4c049ce..4fad96b 100644 --- a/tests/test-diskann.py +++ b/tests/test-diskann.py @@ -891,7 +891,7 @@ def test_diskann_delete_preserves_graph_connectivity(db): # ====================================================================== def test_diskann_update_vector(db): - """UPDATE a vector on DiskANN table may not be supported; verify it either works or errors cleanly.""" + """UPDATE a vector on DiskANN table should error (will be implemented soon).""" db.execute(""" CREATE VIRTUAL TABLE t USING vec0( emb float[8] INDEXED BY diskann(neighbor_quantizer=binary, n_neighbors=8) @@ -901,17 +901,8 @@ def test_diskann_update_vector(db): db.execute("INSERT INTO t(rowid, emb) VALUES (2, ?)", [_f32([0, 1, 0, 0, 0, 0, 0, 0])]) db.execute("INSERT INTO t(rowid, emb) VALUES (3, ?)", [_f32([0, 0, 1, 0, 0, 0, 0, 0])]) - # UPDATE may not be fully supported for DiskANN yet; verify no crash - result = exec(db, "UPDATE t SET emb = ? WHERE rowid = 1", [_f32([0, 0.9, 0.1, 0, 0, 0, 0, 0])]) - if "error" not in result: - # If UPDATE succeeded, verify KNN reflects the new value - rows = db.execute( - "SELECT rowid, distance FROM t WHERE emb MATCH ? AND k=3", - [_f32([0, 1, 0, 0, 0, 0, 0, 0])], - ).fetchall() - assert len(rows) == 3 - # rowid 2 should still be closest (exact match) - assert rows[0][0] == 2 + with pytest.raises(sqlite3.OperationalError, match="UPDATE on vector column.*not supported for DiskANN"): + db.execute("UPDATE t SET emb = ? WHERE rowid = 1", [_f32([0, 0.9, 0.1, 0, 0, 0, 0, 0])]) # ====================================================================== diff --git a/tests/test-ivf-mutations.py b/tests/test-ivf-mutations.py index 5c61119..fce1832 100644 --- a/tests/test-ivf-mutations.py +++ b/tests/test-ivf-mutations.py @@ -573,3 +573,15 @@ def test_interleaved_ops_correctness(db): # Verify we get the right count (25 odd + 15 new - 10 deleted new = 30) expected_alive = set(range(1, 50, 2)) | set(range(50, 60)) | set(range(70, 75)) assert rowids.issubset(expected_alive) + + +def test_ivf_update_vector_blocked(db): + """UPDATE on a vector column with IVF index should error (index would become stale).""" + db.execute( + "CREATE VIRTUAL TABLE t USING vec0(emb float[4] indexed by ivf(nlist=2))" + ) + db.execute("INSERT INTO t(rowid, emb) VALUES (1, ?)", [_f32([1, 0, 0, 0])]) + db.execute("INSERT INTO t(rowid, emb) VALUES (2, ?)", [_f32([0, 1, 0, 0])]) + + with pytest.raises(sqlite3.OperationalError, match="UPDATE on vector column.*not supported for IVF"): + db.execute("UPDATE t SET emb = ? WHERE rowid = 1", [_f32([0, 0, 1, 0])])