Files
SQLmem/tests/test_indexes.py
T
2026-06-05 18:17:55 +02:00

117 lines
3.8 KiB
Python

import sqlite3
import pytest
from sqlalchemy import create_engine
import sqlmem.engine as eng_mod
from sqlmem import CachingEngine
from sqlmem.cache import CacheManager
def index_names(conn, table=None):
sql = "SELECT name FROM sqlite_master WHERE type = 'index'"
return {r[0] for r in conn.execute(sql).fetchall()}
# --- cache level ------------------------------------------------------------
@pytest.fixture
def source_conn():
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE big (id TEXT, val TEXT)")
conn.executemany(
"INSERT INTO big VALUES (?, ?)", [(str(i), f"v{i}") for i in range(100)]
)
conn.commit()
yield conn
conn.close()
@pytest.fixture
def cache(tmp_path):
c = CacheManager(db_path=tmp_path / "cache.db", backup_interval=9999)
yield c
c.close()
def test_index_created_on_load(cache, source_conn):
cache.add_index("big", ["val"])
cache.load_table("big", ["id", "val"], source_conn)
assert "sqlmem_idx_big_val" in index_names(cache.connection)
def test_index_used_by_query_planner(cache, source_conn):
cache.add_index("big", ["val"])
cache.load_table("big", ["id", "val"], source_conn)
plan = cache.connection.execute(
"EXPLAIN QUERY PLAN SELECT id FROM big WHERE val = 'v50'"
).fetchall()
assert any("sqlmem_idx_big_val" in str(row) for row in plan)
def test_index_skipped_when_columns_not_cached(cache, source_conn):
cache.add_index("big", ["missing_col"])
cache.load_table("big", ["id", "val"], source_conn) # must not raise
assert "sqlmem_idx_big_missing_col" not in index_names(cache.connection)
def test_index_recreated_on_reload(cache, source_conn):
cache.add_index("big", ["val"])
cache.load_table("big", ["id", "val"], source_conn)
cache.load_table("big", ["id", "val"], source_conn) # reload (staging swap)
assert "sqlmem_idx_big_val" in index_names(cache.connection)
# --- engine level -----------------------------------------------------------
@pytest.fixture
def source_engine(tmp_path):
db_path = tmp_path / "source.db"
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE products (id TEXT, name TEXT, price TEXT)")
conn.executemany(
"INSERT INTO products VALUES (?, ?, ?)",
[(str(i), f"n{i}", f"{i}.00") for i in range(20)],
)
conn.commit()
conn.close()
engine = create_engine(f"sqlite:///{db_path}")
yield engine
engine.dispose()
@pytest.fixture
def patched_cache(tmp_path, monkeypatch):
monkeypatch.setattr(eng_mod, "CACHE_DB_PATH", tmp_path / "cache.db")
monkeypatch.setattr(eng_mod, "BACKUP_INTERVAL_SECONDS", 9999)
def test_index_column_auto_loaded_even_if_not_selected(source_engine, patched_cache):
engine = CachingEngine(source_engine, indexes={"products": ["name"]})
engine.execute("SELECT id FROM products") # does not select 'name'
cols = {
r[1]
for r in engine._cache.connection.execute("PRAGMA table_info(products)").fetchall()
}
assert "name" in cols # pulled in so the index can be built
assert "sqlmem_idx_products_name" in index_names(engine._cache.connection)
engine.close()
def test_composite_index(source_engine, patched_cache):
engine = CachingEngine(source_engine, indexes={"products": [["name", "price"]]})
engine.execute("SELECT id FROM products")
assert "sqlmem_idx_products_name_price" in index_names(engine._cache.connection)
engine.close()
def test_index_survives_invalidate_and_reload(source_engine, patched_cache):
engine = CachingEngine(source_engine, indexes={"products": ["name"]})
engine.execute("SELECT id, name FROM products")
engine.invalidate("products")
engine.execute("SELECT id, name FROM products")
assert "sqlmem_idx_products_name" in index_names(engine._cache.connection)
engine.close()