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>
This commit is contained in:
Alex Garcia 2026-05-17 23:09:27 -07:00
parent 8105eee61e
commit 8b81f40d1e
3 changed files with 144 additions and 0 deletions

View file

@ -10472,9 +10472,22 @@ static int vec0Rename(sqlite3_vtab *pVtab, const char *zNew) {
#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE
for (int i = 0; i < p->numVectorColumns; i++) {
if (p->shadowIvfCellsNames[i]) {
sqlite3_str_appendf(s,
"ALTER TABLE \"%w\".\"%w_ivf_centroids%02d\" RENAME TO \"%w_ivf_centroids%02d\";",
p->schemaName, p->tableName, i, zNew, i);
sqlite3_str_appendf(s,
"ALTER TABLE \"%w\".\"%w_ivf_cells%02d\" RENAME TO \"%w_ivf_cells%02d\";",
p->schemaName, p->tableName, i, zNew, i);
sqlite3_str_appendf(s,
"ALTER TABLE \"%w\".\"%w_ivf_rowid_map%02d\" RENAME TO \"%w_ivf_rowid_map%02d\";",
p->schemaName, p->tableName, i, zNew, i);
// _ivf_vectors is only created when quantizer != none
// (mirror ivf_create_shadow_tables in sqlite-vec-ivf.c).
if (p->vector_columns[i].ivf.quantizer != VEC0_IVF_QUANTIZER_NONE) {
sqlite3_str_appendf(s,
"ALTER TABLE \"%w\".\"%w_ivf_vectors%02d\" RENAME TO \"%w_ivf_vectors%02d\";",
p->schemaName, p->tableName, i, zNew, i);
}
}
}
#endif

98
tests/test-ivf-rename.py Normal file
View file

@ -0,0 +1,98 @@
import sqlite3
import pytest
from helpers import _f32
def _shadow_tables(db, prefix):
"""Return sorted list of shadow table names for a given prefix."""
return sorted([
row[0] for row in db.execute(
r"select name from sqlite_master where name like ? escape '\' and type='table' order by 1",
[f"{prefix}\\__%"],
).fetchall()
])
def test_rename_ivf_no_quantizer(db):
"""Rename should rename all IVF shadow tables (_ivf_centroids, _ivf_cells,
_ivf_rowid_map). quantizer=none no _ivf_vectors table."""
db.execute("""
CREATE VIRTUAL TABLE v USING vec0(
a float[4] indexed by ivf(nlist=2, quantizer=none)
)
""")
db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 4)])
db.execute("insert into v(rowid, a) values (2, ?)", [_f32([0.9] * 4)])
before = _shadow_tables(db, "v")
assert "v_ivf_centroids00" in before
assert "v_ivf_cells00" in before
assert "v_ivf_rowid_map00" in before
assert "v_ivf_vectors00" not in before # quantizer=none -> no _ivf_vectors
assert "v_vector_chunks00" not in before
db.execute("ALTER TABLE v RENAME TO v2")
# Querying the renamed table should still work — it hits _ivf_cells,
# _ivf_centroids (when trained), and _ivf_rowid_map.
rows = db.execute(
"select rowid from v2 where a match ? and k=10",
[_f32([0.1] * 4)],
).fetchall()
assert any(r[0] == 1 for r in rows)
after = _shadow_tables(db, "v2")
assert "v2_ivf_centroids00" in after
assert "v2_ivf_cells00" in after
assert "v2_ivf_rowid_map00" in after
# No old shadow tables should remain
assert _shadow_tables(db, "v") == []
def test_rename_ivf_quantizer_binary(db):
"""Rename should also rename _ivf_vectors when quantizer != none."""
db.execute("""
CREATE VIRTUAL TABLE v USING vec0(
a float[8] indexed by ivf(nlist=2, quantizer=binary)
)
""")
db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 8)])
before = _shadow_tables(db, "v")
assert "v_ivf_centroids00" in before
assert "v_ivf_cells00" in before
assert "v_ivf_rowid_map00" in before
assert "v_ivf_vectors00" in before # quantizer=binary creates _ivf_vectors
db.execute("ALTER TABLE v RENAME TO v2")
rows = db.execute(
"select rowid from v2 where a match ? and k=10",
[_f32([0.1] * 8)],
).fetchall()
assert rows[0][0] == 1
after = _shadow_tables(db, "v2")
assert "v2_ivf_centroids00" in after
assert "v2_ivf_cells00" in after
assert "v2_ivf_rowid_map00" in after
assert "v2_ivf_vectors00" in after
assert _shadow_tables(db, "v") == []
def test_rename_ivf_drop_after(db):
"""DROP TABLE on a renamed IVF table must drop every shadow table — leftover
shadows from a half-renamed IVF index would orphan tables in the schema."""
db.execute("""
CREATE VIRTUAL TABLE v USING vec0(
a float[8] indexed by ivf(nlist=2, quantizer=binary)
)
""")
db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 8)])
db.execute("ALTER TABLE v RENAME TO v2")
db.execute("DROP TABLE v2")
assert _shadow_tables(db, "v") == []
assert _shadow_tables(db, "v2") == []

View file

@ -191,6 +191,39 @@ def test_rename_diskann(db):
assert _shadow_tables(db, "v") == []
def test_rename_rescore(db):
"""Rename should work on rescore-indexed tables (no _vector_chunks shadow)."""
db.execute("""
CREATE VIRTUAL TABLE v USING vec0(
a float[8] indexed by rescore(quantizer=bit)
)
""")
db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1] * 8)])
db.execute("insert into v(rowid, a) values (2, ?)", [_f32([0.9] * 8)])
# Rescore columns use _rescore_chunks / _rescore_vectors instead of
# _vector_chunks; the rename must skip the missing _vector_chunks ALTER
# and rename both rescore shadow tables.
before = _shadow_tables(db, "v")
assert "v_rescore_chunks00" in before
assert "v_rescore_vectors00" in before
assert "v_vector_chunks00" not in before
db.execute("ALTER TABLE v RENAME TO v2")
rows = db.execute(
"select rowid from v2 where a match ? and k=10",
[_f32([0.1] * 8)],
).fetchall()
assert rows[0][0] == 1
after = _shadow_tables(db, "v2")
assert "v2_rescore_chunks00" in after
assert "v2_rescore_vectors00" in after
assert "v2_vector_chunks00" not in after
assert _shadow_tables(db, "v") == []
def test_rename_drop_after(db):
"""DROP TABLE should work on a renamed table."""
db.execute("create virtual table v using vec0(a float[2], chunk_size=8)")