diff --git a/sqlite-vec.c b/sqlite-vec.c index 40fe0bf..83b4006 100644 --- a/sqlite-vec.c +++ b/sqlite-vec.c @@ -10362,6 +10362,163 @@ static int vec0Rollback(sqlite3_vtab *pVTab) { return SQLITE_OK; } +/** + * xRename implementation for vec0. + * Renames all shadow tables to match the new virtual table name, + * then updates cached table names and finalizes stale prepared statements. + */ +static int vec0Rename(sqlite3_vtab *pVtab, const char *zNew) { + vec0_vtab *p = (vec0_vtab *)pVtab; + int rc = SQLITE_OK; + + // Build a single SQL string with ALTER TABLE RENAME for every shadow table. + sqlite3_str *s = sqlite3_str_new(p->db); + + // Core shadow tables (always present) + sqlite3_str_appendf(s, + "ALTER TABLE \"%w\".\"%w_info\" RENAME TO \"%w_info\";", + p->schemaName, p->tableName, zNew); + sqlite3_str_appendf(s, + "ALTER TABLE \"%w\".\"%w_rowids\" RENAME TO \"%w_rowids\";", + p->schemaName, p->tableName, zNew); + sqlite3_str_appendf(s, + "ALTER TABLE \"%w\".\"%w_chunks\" RENAME TO \"%w_chunks\";", + p->schemaName, p->tableName, zNew); + + // Auxiliary shadow table (only if auxiliary columns exist) + if (p->numAuxiliaryColumns > 0) { + sqlite3_str_appendf(s, + "ALTER TABLE \"%w\".\"%w_auxiliary\" RENAME TO \"%w_auxiliary\";", + p->schemaName, p->tableName, zNew); + } + + // Per-vector-column shadow tables + for (int i = 0; i < p->numVectorColumns; i++) { + sqlite3_str_appendf(s, + "ALTER TABLE \"%w\".\"%w_vector_chunks%02d\" RENAME TO \"%w_vector_chunks%02d\";", + p->schemaName, p->tableName, i, zNew, i); + +#if SQLITE_VEC_ENABLE_RESCORE + if (p->shadowRescoreChunksNames[i]) { + sqlite3_str_appendf(s, + "ALTER TABLE \"%w\".\"%w_rescore_chunks%02d\" RENAME TO \"%w_rescore_chunks%02d\";", + p->schemaName, p->tableName, i, zNew, i); + sqlite3_str_appendf(s, + "ALTER TABLE \"%w\".\"%w_rescore_vectors%02d\" RENAME TO \"%w_rescore_vectors%02d\";", + p->schemaName, p->tableName, i, zNew, i); + } +#endif + +#if SQLITE_VEC_ENABLE_DISKANN + if (p->shadowVectorsNames[i]) { + sqlite3_str_appendf(s, + "ALTER TABLE \"%w\".\"%w_vectors%02d\" RENAME TO \"%w_vectors%02d\";", + p->schemaName, p->tableName, i, zNew, i); + sqlite3_str_appendf(s, + "ALTER TABLE \"%w\".\"%w_diskann_nodes%02d\" RENAME TO \"%w_diskann_nodes%02d\";", + p->schemaName, p->tableName, i, zNew, i); + sqlite3_str_appendf(s, + "ALTER TABLE \"%w\".\"%w_diskann_buffer%02d\" RENAME TO \"%w_diskann_buffer%02d\";", + p->schemaName, p->tableName, i, zNew, i); + } +#endif + } + +#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_cells%02d\" RENAME TO \"%w_ivf_cells%02d\";", + p->schemaName, p->tableName, i, zNew, i); + } + } +#endif + + // Per-metadata-column shadow tables + for (int i = 0; i < p->numMetadataColumns; i++) { + sqlite3_str_appendf(s, + "ALTER TABLE \"%w\".\"%w_metadatachunks%02d\" RENAME TO \"%w_metadatachunks%02d\";", + p->schemaName, p->tableName, i, zNew, i); + if (p->metadata_columns[i].kind == VEC0_METADATA_COLUMN_KIND_TEXT) { + sqlite3_str_appendf(s, + "ALTER TABLE \"%w\".\"%w_metadatatext%02d\" RENAME TO \"%w_metadatatext%02d\";", + p->schemaName, p->tableName, i, zNew, i); + } + } + + char *zSql = sqlite3_str_finish(s); + if (!zSql) { + return SQLITE_NOMEM; + } + + rc = sqlite3_exec(p->db, zSql, 0, 0, 0); + sqlite3_free(zSql); + if (rc != SQLITE_OK) { + return rc; + } + + // Finalize all prepared statements — they reference old table names. + vec0_free_resources(p); + + // Update cached table name + sqlite3_free(p->tableName); + p->tableName = sqlite3_mprintf("%s", zNew); + if (!p->tableName) return SQLITE_NOMEM; + + // Update cached shadow table names + sqlite3_free(p->shadowRowidsName); + p->shadowRowidsName = sqlite3_mprintf("%s_rowids", zNew); + + sqlite3_free(p->shadowChunksName); + p->shadowChunksName = sqlite3_mprintf("%s_chunks", zNew); + + for (int i = 0; i < p->numVectorColumns; i++) { + sqlite3_free(p->shadowVectorChunksNames[i]); + p->shadowVectorChunksNames[i] = + sqlite3_mprintf("%s_vector_chunks%02d", zNew, i); + +#if SQLITE_VEC_ENABLE_RESCORE + if (p->shadowRescoreChunksNames[i]) { + sqlite3_free(p->shadowRescoreChunksNames[i]); + p->shadowRescoreChunksNames[i] = + sqlite3_mprintf("%s_rescore_chunks%02d", zNew, i); + sqlite3_free(p->shadowRescoreVectorsNames[i]); + p->shadowRescoreVectorsNames[i] = + sqlite3_mprintf("%s_rescore_vectors%02d", zNew, i); + } +#endif + +#if SQLITE_VEC_ENABLE_DISKANN + if (p->shadowVectorsNames[i]) { + sqlite3_free(p->shadowVectorsNames[i]); + p->shadowVectorsNames[i] = + sqlite3_mprintf("%s_vectors%02d", zNew, i); + sqlite3_free(p->shadowDiskannNodesNames[i]); + p->shadowDiskannNodesNames[i] = + sqlite3_mprintf("%s_diskann_nodes%02d", zNew, i); + } +#endif + } + +#if SQLITE_VEC_EXPERIMENTAL_IVF_ENABLE + for (int i = 0; i < p->numVectorColumns; i++) { + if (p->shadowIvfCellsNames[i]) { + sqlite3_free(p->shadowIvfCellsNames[i]); + p->shadowIvfCellsNames[i] = + sqlite3_mprintf("%s_ivf_cells%02d", zNew, i); + } + } +#endif + + for (int i = 0; i < p->numMetadataColumns; i++) { + sqlite3_free(p->shadowMetadataChunksNames[i]); + p->shadowMetadataChunksNames[i] = + sqlite3_mprintf("%s_metadatachunks%02d", zNew, i); + } + + return SQLITE_OK; +} + static sqlite3_module vec0Module = { /* iVersion */ 3, /* xCreate */ vec0Create, @@ -10382,7 +10539,7 @@ static sqlite3_module vec0Module = { /* xCommit */ vec0Commit, /* xRollback */ vec0Rollback, /* xFindFunction */ 0, - /* xRename */ 0, // https://github.com/asg017/sqlite-vec/issues/43 + /* xRename */ vec0Rename, /* xSavepoint */ 0, /* xRelease */ 0, /* xRollbackTo */ 0, diff --git a/tests/test-rename.py b/tests/test-rename.py new file mode 100644 index 0000000..6da9d32 --- /dev/null +++ b/tests/test-rename.py @@ -0,0 +1,173 @@ +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_basic(db): + """ALTER TABLE RENAME should rename vec0 table and all shadow tables.""" + db.execute("create virtual table v using vec0(a float[2], chunk_size=8)") + db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1, 0.2])]) + db.execute("insert into v(rowid, a) values (2, ?)", [_f32([0.3, 0.4])]) + + assert _shadow_tables(db, "v") == [ + "v_chunks", + "v_info", + "v_rowids", + "v_vector_chunks00", + ] + + db.execute("ALTER TABLE v RENAME TO v2") + + # Old name should no longer work + with pytest.raises(sqlite3.OperationalError): + db.execute("select * from v") + + # New name should work and return the same data + rows = db.execute( + "select rowid, distance from v2 where a match ? and k=10", + [_f32([0.1, 0.2])], + ).fetchall() + assert len(rows) == 2 + assert rows[0][0] == 1 # closest match + + # Shadow tables should all be renamed + assert _shadow_tables(db, "v2") == [ + "v2_chunks", + "v2_info", + "v2_rowids", + "v2_vector_chunks00", + ] + + # No old shadow tables should remain + assert _shadow_tables(db, "v") == [] + + +def test_rename_insert_after(db): + """Inserts and queries should work after rename.""" + db.execute("create virtual table v using vec0(a float[2], chunk_size=8)") + db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1, 0.2])]) + db.execute("ALTER TABLE v RENAME TO v2") + + # Insert into renamed table + db.execute("insert into v2(rowid, a) values (2, ?)", [_f32([0.3, 0.4])]) + + rows = db.execute( + "select rowid from v2 where a match ? and k=10", + [_f32([0.3, 0.4])], + ).fetchall() + assert len(rows) == 2 + assert rows[0][0] == 2 + + +def test_rename_delete_after(db): + """Deletes should work after rename.""" + db.execute("create virtual table v using vec0(a float[2], chunk_size=8)") + db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1, 0.2])]) + db.execute("insert into v(rowid, a) values (2, ?)", [_f32([0.3, 0.4])]) + db.execute("ALTER TABLE v RENAME TO v2") + + db.execute("delete from v2 where rowid = 1") + rows = db.execute( + "select rowid from v2 where a match ? and k=10", + [_f32([0.3, 0.4])], + ).fetchall() + assert len(rows) == 1 + assert rows[0][0] == 2 + + +def test_rename_with_auxiliary(db): + """Rename should also rename the _auxiliary shadow table.""" + db.execute( + "create virtual table v using vec0(a float[2], +name text, chunk_size=8)" + ) + db.execute( + "insert into v(rowid, a, name) values (1, ?, 'hello')", + [_f32([0.1, 0.2])], + ) + + assert _shadow_tables(db, "v") == [ + "v_auxiliary", + "v_chunks", + "v_info", + "v_rowids", + "v_vector_chunks00", + ] + + db.execute("ALTER TABLE v RENAME TO v2") + + # Auxiliary data should be accessible + rows = db.execute( + "select rowid, name from v2 where a match ? and k=10", + [_f32([0.1, 0.2])], + ).fetchall() + assert rows[0][0] == 1 + assert rows[0][1] == "hello" + + assert _shadow_tables(db, "v2") == [ + "v2_auxiliary", + "v2_chunks", + "v2_info", + "v2_rowids", + "v2_vector_chunks00", + ] + assert _shadow_tables(db, "v") == [] + + +def test_rename_with_metadata(db): + """Rename should also rename metadata shadow tables.""" + db.execute( + "create virtual table v using vec0(a float[2], tag text, chunk_size=8)" + ) + db.execute( + "insert into v(rowid, a, tag) values (1, ?, 'a')", + [_f32([0.1, 0.2])], + ) + + assert _shadow_tables(db, "v") == [ + "v_chunks", + "v_info", + "v_metadatachunks00", + "v_metadatatext00", + "v_rowids", + "v_vector_chunks00", + ] + + db.execute("ALTER TABLE v RENAME TO v2") + + rows = db.execute( + "select rowid, tag from v2 where a match ? and k=10", + [_f32([0.1, 0.2])], + ).fetchall() + assert rows[0][0] == 1 + assert rows[0][1] == "a" + + assert _shadow_tables(db, "v2") == [ + "v2_chunks", + "v2_info", + "v2_metadatachunks00", + "v2_metadatatext00", + "v2_rowids", + "v2_vector_chunks00", + ] + 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)") + db.execute("insert into v(rowid, a) values (1, ?)", [_f32([0.1, 0.2])]) + db.execute("ALTER TABLE v RENAME TO v2") + db.execute("DROP TABLE v2") + + # Nothing should remain + assert _shadow_tables(db, "v2") == []