import sqlite3 import pytest from sqlmem.cache import CacheManager @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(5)] ) 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() @pytest.fixture def small_batches(monkeypatch): # Force multiple fetch batches over the 5 source rows. monkeypatch.setattr("sqlmem.cache.FETCH_BATCH_SIZE", 2) def test_batched_load_loads_all_rows(cache, source_conn, small_batches): cache.load_table("big", ["id", "val"], source_conn) _, rows = cache.execute_in_memory( "SELECT id, val FROM big ORDER BY CAST(id AS INTEGER)" ) assert len(rows) == 5 assert rows[0] == ("0", "v0") assert rows[-1] == ("4", "v4") def test_no_staging_table_left_behind(cache, source_conn, small_batches): cache.load_table("big", ["id", "val"], source_conn) names = { r[0] for r in cache.connection.execute( "SELECT name FROM sqlite_master WHERE type = 'table'" ).fetchall() } assert "big" in names assert not any(n.endswith("__sqlmem_load") for n in names) def test_reload_replaces_data_atomically(cache, source_conn, small_batches): cache.load_table("big", ["id", "val"], source_conn) source_conn.execute("DELETE FROM big") source_conn.execute("INSERT INTO big VALUES ('99', 'new')") source_conn.commit() cache.load_table("big", ["id", "val"], source_conn) _, rows = cache.execute_in_memory("SELECT id, val FROM big") assert rows == [("99", "new")] def test_load_sets_ready_state(cache, source_conn): cache.load_table("big", ["id", "val"], source_conn) assert cache.get_states()["big"] == "ready" def test_orphan_staging_dropped_on_startup(tmp_path, source_conn): # Simulate a crash mid-load: a staging table persisted into cache.db. db_path = tmp_path / "cache.db" c1 = CacheManager(db_path=db_path, backup_interval=9999) c1.load_table("big", ["id", "val"], source_conn) c1.connection.execute("CREATE TABLE big__sqlmem_load (id TEXT, val TEXT)") c1.connection.commit() c1.close() # backup writes the staging table to disk c2 = CacheManager(db_path=db_path, backup_interval=9999) names = { r[0] for r in c2.connection.execute( "SELECT name FROM sqlite_master WHERE type = 'table'" ).fetchall() } c2.close() assert "big" in names # real table survives assert not any(n.endswith("__sqlmem_load") for n in names) # orphan cleaned def test_failed_load_sets_error_state_and_cleans_staging(cache): empty_source = sqlite3.connect(":memory:") # has no 'big' table try: with pytest.raises(sqlite3.OperationalError): cache.load_table("big", ["id"], empty_source) assert cache.get_states()["big"] == "error" names = { r[0] for r in cache.connection.execute( "SELECT name FROM sqlite_master WHERE type = 'table'" ).fetchall() } assert not any(n.endswith("__sqlmem_load") for n in names) finally: empty_source.close()