Skip to content

Commit 8b81f40

Browse files
asg017claude
andcommitted
Rename all IVF shadow tables in vec0Rename
vec0Rename only emitted ALTER TABLE on `<name>_ivf_cells%02d`, so renaming an IVF-indexed vec0 table left `_ivf_centroids`, `_ivf_rowid_map`, and `_ivf_vectors` (when quantizer != none) with the old prefix. Subsequent queries against the renamed table broke, and DROP TABLE left those three shadows orphaned in the schema. Same shape as the DiskANN/rescore bug fixed in #294, just for the IVF branch. Mirror ivf_create_shadow_tables: emit ALTER for all four IVF shadows, gating `_ivf_vectors` on quantizer != VEC0_IVF_QUANTIZER_NONE. Adds test-ivf-rename.py (auto-skipped on default builds via conftest's test-ivf prefix rule) covering quantizer=none, quantizer=binary, and DROP-after-rename. Also adds a rescore rename regression test to test-rename.py to lock down the (already-correct) rescore path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8105eee commit 8b81f40

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

‎sqlite-vec.c‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10472,9 +10472,22 @@ static int vec0Rename(sqlite3_vtab *pVtab, const char *zNew) {
1047210472
#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE
1047310473
for (int i = 0; i < p->numVectorColumns; i++) {
1047410474
if (p->shadowIvfCellsNames[i]) {
10475+
sqlite3_str_appendf(s,
10476+
"ALTER TABLE \"%w\".\"%w_ivf_centroids%02d\" RENAME TO \"%w_ivf_centroids%02d\";",
10477+
p->schemaName, p->tableName, i, zNew, i);
1047510478
sqlite3_str_appendf(s,
1047610479
"ALTER TABLE \"%w\".\"%w_ivf_cells%02d\" RENAME TO \"%w_ivf_cells%02d\";",
1047710480
p->schemaName, p->tableName, i, zNew, i);
10481+
sqlite3_str_appendf(s,
10482+
"ALTER TABLE \"%w\".\"%w_ivf_rowid_map%02d\" RENAME TO \"%w_ivf_rowid_map%02d\";",
10483+
p->schemaName, p->tableName, i, zNew, i);
10484+
// _ivf_vectors is only created when quantizer != none
10485+
// (mirror ivf_create_shadow_tables in sqlite-vec-ivf.c).
10486+
if (p->vector_columns[i].ivf.quantizer != VEC0_IVF_QUANTIZER_NONE) {
10487+
sqlite3_str_appendf(s,
10488+
"ALTER TABLE \"%w\".\"%w_ivf_vectors%02d\" RENAME TO \"%w_ivf_vectors%02d\";",
10489+
p->schemaName, p->tableName, i, zNew, i);
10490+
}
1047810491
}
1047910492
}
1048010493
#endif

‎tests/test-ivf-rename.py‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import sqlite3
2+
import pytest
3+
from helpers import _f32
4+
5+
6+
def _shadow_tables(db, prefix):
7+
"""Return sorted list of shadow table names for a given prefix."""
8+
return sorted([
9+
row[0] for row in db.execute(
10+
r"select name from sqlite_master where name like ? escape '\' and type='table' order by 1",
11+
[f"{prefix}\\__%"],
12+
).fetchall()
13+
])
14+
15+
16+
def test_rename_ivf_no_quantizer(db):
17+
"""Rename should rename all IVF shadow tables (_ivf_centroids, _ivf_cells,
18+
_ivf_rowid_map). quantizer=none — no _ivf_vectors table."""
19+
db.execute("""
20+
CREATE VIRTUAL TABLE v USING vec0(
21+
a float[4] indexed by ivf(nlist=2, quantizer=none)
22+
)
23+
""")
24+
db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 4)])
25+
db.execute("insert into v(rowid, a) values (2, ?)", [_f32([0.9] * 4)])
26+
27+
before = _shadow_tables(db, "v")
28+
assert "v_ivf_centroids00" in before
29+
assert "v_ivf_cells00" in before
30+
assert "v_ivf_rowid_map00" in before
31+
assert "v_ivf_vectors00" not in before # quantizer=none -> no _ivf_vectors
32+
assert "v_vector_chunks00" not in before
33+
34+
db.execute("ALTER TABLE v RENAME TO v2")
35+
36+
# Querying the renamed table should still work — it hits _ivf_cells,
37+
# _ivf_centroids (when trained), and _ivf_rowid_map.
38+
rows = db.execute(
39+
"select rowid from v2 where a match ? and k=10",
40+
[_f32([0.1] * 4)],
41+
).fetchall()
42+
assert any(r[0] == 1 for r in rows)
43+
44+
after = _shadow_tables(db, "v2")
45+
assert "v2_ivf_centroids00" in after
46+
assert "v2_ivf_cells00" in after
47+
assert "v2_ivf_rowid_map00" in after
48+
49+
# No old shadow tables should remain
50+
assert _shadow_tables(db, "v") == []
51+
52+
53+
def test_rename_ivf_quantizer_binary(db):
54+
"""Rename should also rename _ivf_vectors when quantizer != none."""
55+
db.execute("""
56+
CREATE VIRTUAL TABLE v USING vec0(
57+
a float[8] indexed by ivf(nlist=2, quantizer=binary)
58+
)
59+
""")
60+
db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 8)])
61+
62+
before = _shadow_tables(db, "v")
63+
assert "v_ivf_centroids00" in before
64+
assert "v_ivf_cells00" in before
65+
assert "v_ivf_rowid_map00" in before
66+
assert "v_ivf_vectors00" in before # quantizer=binary creates _ivf_vectors
67+
68+
db.execute("ALTER TABLE v RENAME TO v2")
69+
70+
rows = db.execute(
71+
"select rowid from v2 where a match ? and k=10",
72+
[_f32([0.1] * 8)],
73+
).fetchall()
74+
assert rows[0][0] == 1
75+
76+
after = _shadow_tables(db, "v2")
77+
assert "v2_ivf_centroids00" in after
78+
assert "v2_ivf_cells00" in after
79+
assert "v2_ivf_rowid_map00" in after
80+
assert "v2_ivf_vectors00" in after
81+
82+
assert _shadow_tables(db, "v") == []
83+
84+
85+
def test_rename_ivf_drop_after(db):
86+
"""DROP TABLE on a renamed IVF table must drop every shadow table — leftover
87+
shadows from a half-renamed IVF index would orphan tables in the schema."""
88+
db.execute("""
89+
CREATE VIRTUAL TABLE v USING vec0(
90+
a float[8] indexed by ivf(nlist=2, quantizer=binary)
91+
)
92+
""")
93+
db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 8)])
94+
db.execute("ALTER TABLE v RENAME TO v2")
95+
db.execute("DROP TABLE v2")
96+
97+
assert _shadow_tables(db, "v") == []
98+
assert _shadow_tables(db, "v2") == []

‎tests/test-rename.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,39 @@ def test_rename_diskann(db):
191191
assert _shadow_tables(db, "v") == []
192192

193193

194+
def test_rename_rescore(db):
195+
"""Rename should work on rescore-indexed tables (no _vector_chunks shadow)."""
196+
db.execute("""
197+
CREATE VIRTUAL TABLE v USING vec0(
198+
a float[8] indexed by rescore(quantizer=bit)
199+
)
200+
""")
201+
db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 8)])
202+
db.execute("insert into v(rowid, a) values (2, ?)", [_f32([0.9] * 8)])
203+
204+
# Rescore columns use _rescore_chunks / _rescore_vectors instead of
205+
# _vector_chunks; the rename must skip the missing _vector_chunks ALTER
206+
# and rename both rescore shadow tables.
207+
before = _shadow_tables(db, "v")
208+
assert "v_rescore_chunks00" in before
209+
assert "v_rescore_vectors00" in before
210+
assert "v_vector_chunks00" not in before
211+
212+
db.execute("ALTER TABLE v RENAME TO v2")
213+
214+
rows = db.execute(
215+
"select rowid from v2 where a match ? and k=10",
216+
[_f32([0.1] * 8)],
217+
).fetchall()
218+
assert rows[0][0] == 1
219+
220+
after = _shadow_tables(db, "v2")
221+
assert "v2_rescore_chunks00" in after
222+
assert "v2_rescore_vectors00" in after
223+
assert "v2_vector_chunks00" not in after
224+
assert _shadow_tables(db, "v") == []
225+
226+
194227
def test_rename_drop_after(db):
195228
"""DROP TABLE should work on a renamed table."""
196229
db.execute("create virtual table v using vec0(a float[2], chunk_size=8)")

0 commit comments

Comments
 (0)