Skip to content

Commit b95c05b

Browse files
asg017claude
andcommitted
Support INSERT OR REPLACE for vec0 virtual tables (fixes #127)
Check sqlite3_vtab_on_conflict() in vec0Update_Insert and delete the existing row before re-inserting when the conflict mode is SQLITE_REPLACE. Handles both integer and text primary keys. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 89f6203 commit b95c05b

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed

sqlite-vec.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9161,6 +9161,9 @@ int vec0_write_metadata_value(vec0_vtab *p, int metadata_column_idx, i64 rowid,
91619161
*
91629162
* @return int SQLITE_OK on success, otherwise error code on failure
91639163
*/
9164+
// Forward declaration: needed for INSERT OR REPLACE handling in vec0Update_Insert
9165+
int vec0Update_Delete(sqlite3_vtab *pVTab, sqlite3_value *idValue);
9166+
91649167
int vec0Update_Insert(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv,
91659168
sqlite_int64 *pRowid) {
91669169
UNUSED_PARAMETER(argc);
@@ -9281,6 +9284,44 @@ int vec0Update_Insert(sqlite3_vtab *pVTab, int argc, sqlite3_value **argv,
92819284
goto cleanup;
92829285
}
92839286

9287+
// Handle INSERT OR REPLACE: if the conflict resolution is REPLACE and the
9288+
// row already exists, delete the existing row first before inserting.
9289+
if (sqlite3_vtab_on_conflict(p->db) == SQLITE_REPLACE) {
9290+
sqlite3_value *idValue = argv[2 + VEC0_COLUMN_ID];
9291+
int idType = sqlite3_value_type(idValue);
9292+
int existingRowExists = 0;
9293+
9294+
if (p->pkIsText && idType == SQLITE_TEXT) {
9295+
i64 existingRowid;
9296+
rc = vec0_rowid_from_id(p, idValue, &existingRowid);
9297+
if (rc == SQLITE_OK) {
9298+
existingRowExists = 1;
9299+
} else if (rc == SQLITE_EMPTY) {
9300+
rc = SQLITE_OK; // row doesn't exist, proceed with normal insert
9301+
} else {
9302+
goto cleanup;
9303+
}
9304+
} else if (!p->pkIsText && idType == SQLITE_INTEGER) {
9305+
i64 existingRowid = sqlite3_value_int64(idValue);
9306+
i64 chunk_id_tmp, chunk_offset_tmp;
9307+
rc = vec0_get_chunk_position(p, existingRowid, NULL, &chunk_id_tmp, &chunk_offset_tmp);
9308+
if (rc == SQLITE_OK) {
9309+
existingRowExists = 1;
9310+
} else if (rc == SQLITE_EMPTY) {
9311+
rc = SQLITE_OK; // row doesn't exist, proceed with normal insert
9312+
} else {
9313+
goto cleanup;
9314+
}
9315+
}
9316+
9317+
if (existingRowExists) {
9318+
rc = vec0Update_Delete(pVTab, idValue);
9319+
if (rc != SQLITE_OK) {
9320+
goto cleanup;
9321+
}
9322+
}
9323+
}
9324+
92849325
// Step #1: Insert/get a rowid for this row, from the _rowids table.
92859326
rc = vec0Update_InsertRowidStep(p, argv[2 + VEC0_COLUMN_ID], &rowid);
92869327
if (rc != SQLITE_OK) {

tests/test-insert-delete.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,3 +537,117 @@ def test_wal_concurrent_reader_during_write(tmp_path):
537537

538538
writer.close()
539539
reader.close()
540+
541+
542+
def test_insert_or_replace_integer_pk(db):
543+
"""INSERT OR REPLACE should update vector when rowid already exists."""
544+
db.execute("create virtual table v using vec0(emb float[4], chunk_size=8)")
545+
546+
db.execute(
547+
"insert into v(rowid, emb) values (1, ?)", [_f32([1.0, 2.0, 3.0, 4.0])]
548+
)
549+
# Replace with new vector
550+
db.execute(
551+
"insert or replace into v(rowid, emb) values (1, ?)",
552+
[_f32([10.0, 20.0, 30.0, 40.0])],
553+
)
554+
555+
# Should still have exactly 1 row
556+
count = db.execute("select count(*) from v").fetchone()[0]
557+
assert count == 1
558+
559+
# Vector should be the replaced value
560+
row = db.execute("select emb from v where rowid = 1").fetchone()
561+
assert row[0] == _f32([10.0, 20.0, 30.0, 40.0])
562+
563+
564+
def test_insert_or_replace_new_row(db):
565+
"""INSERT OR REPLACE with a new rowid should just insert normally."""
566+
db.execute("create virtual table v using vec0(emb float[4], chunk_size=8)")
567+
568+
db.execute(
569+
"insert or replace into v(rowid, emb) values (1, ?)",
570+
[_f32([1.0, 2.0, 3.0, 4.0])],
571+
)
572+
573+
count = db.execute("select count(*) from v").fetchone()[0]
574+
assert count == 1
575+
576+
row = db.execute("select emb from v where rowid = 1").fetchone()
577+
assert row[0] == _f32([1.0, 2.0, 3.0, 4.0])
578+
579+
580+
def test_insert_or_replace_text_pk(db):
581+
"""INSERT OR REPLACE should work with text primary keys."""
582+
db.execute(
583+
"create virtual table v using vec0("
584+
"id text primary key, emb float[4], chunk_size=8"
585+
")"
586+
)
587+
588+
db.execute(
589+
"insert into v(id, emb) values ('doc_a', ?)",
590+
[_f32([1.0, 2.0, 3.0, 4.0])],
591+
)
592+
db.execute(
593+
"insert or replace into v(id, emb) values ('doc_a', ?)",
594+
[_f32([10.0, 20.0, 30.0, 40.0])],
595+
)
596+
597+
count = db.execute("select count(*) from v").fetchone()[0]
598+
assert count == 1
599+
600+
row = db.execute("select emb from v where id = 'doc_a'").fetchone()
601+
assert row[0] == _f32([10.0, 20.0, 30.0, 40.0])
602+
603+
604+
def test_insert_or_replace_with_auxiliary(db):
605+
"""INSERT OR REPLACE should also replace auxiliary column values."""
606+
db.execute(
607+
"create virtual table v using vec0("
608+
"emb float[4], +label text, chunk_size=8"
609+
")"
610+
)
611+
612+
db.execute(
613+
"insert into v(rowid, emb, label) values (1, ?, 'old')",
614+
[_f32([1.0, 2.0, 3.0, 4.0])],
615+
)
616+
db.execute(
617+
"insert or replace into v(rowid, emb, label) values (1, ?, 'new')",
618+
[_f32([10.0, 20.0, 30.0, 40.0])],
619+
)
620+
621+
count = db.execute("select count(*) from v").fetchone()[0]
622+
assert count == 1
623+
624+
row = db.execute("select emb, label from v where rowid = 1").fetchone()
625+
assert row[0] == _f32([10.0, 20.0, 30.0, 40.0])
626+
assert row[1] == "new"
627+
628+
629+
def test_insert_or_replace_knn_uses_new_vector(db):
630+
"""After INSERT OR REPLACE, KNN should find the new vector, not the old one."""
631+
db.execute("create virtual table v using vec0(emb float[4], chunk_size=8)")
632+
633+
db.execute(
634+
"insert into v(rowid, emb) values (1, ?)", [_f32([1.0, 0.0, 0.0, 0.0])]
635+
)
636+
db.execute(
637+
"insert into v(rowid, emb) values (2, ?)", [_f32([0.0, 1.0, 0.0, 0.0])]
638+
)
639+
640+
# Replace row 1's vector to be very close to row 2
641+
db.execute(
642+
"insert or replace into v(rowid, emb) values (1, ?)",
643+
[_f32([0.0, 0.9, 0.0, 0.0])],
644+
)
645+
646+
# KNN for [0, 1, 0, 0] should return row 2 first (exact), then row 1 (close)
647+
rows = db.execute(
648+
"select rowid, distance from v where emb match ? and k = 2",
649+
[_f32([0.0, 1.0, 0.0, 0.0])],
650+
).fetchall()
651+
assert rows[0][0] == 2
652+
assert rows[1][0] == 1
653+
assert rows[1][1] < 0.11 # should be close (L2 distance ≈ 0.1)

0 commit comments

Comments
 (0)