117 lines
3.8 KiB
Python
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()
|