171 lines
5.8 KiB
Python
171 lines
5.8 KiB
Python
import sqlite3
|
|
import threading
|
|
|
|
import pytest
|
|
|
|
from sqlmem.cache import CacheManager
|
|
|
|
|
|
@pytest.fixture
|
|
def cache(tmp_path):
|
|
c = CacheManager(db_path=tmp_path / "test_cache.db", backup_interval=9999)
|
|
yield c
|
|
c.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def source_conn():
|
|
conn = sqlite3.connect(":memory:")
|
|
conn.execute("CREATE TABLE users (name TEXT, email TEXT, status TEXT)")
|
|
conn.executemany(
|
|
"INSERT INTO users VALUES (?, ?, ?)",
|
|
[("alice", "alice@example.com", "active"), ("bob", "bob@example.com", "inactive")],
|
|
)
|
|
conn.commit()
|
|
yield conn
|
|
conn.close()
|
|
|
|
|
|
def test_table_not_cached_initially(cache):
|
|
assert cache.is_table_cached("users") is False
|
|
|
|
|
|
def test_load_table(cache, source_conn):
|
|
cache.load_table("users", ["name", "email"], source_conn)
|
|
assert cache.is_table_cached("users") is True
|
|
|
|
|
|
def test_loaded_data_correct(cache, source_conn):
|
|
cache.load_table("users", ["name", "email"], source_conn)
|
|
rows = cache.connection.execute("SELECT name, email FROM users").fetchall()
|
|
assert len(rows) == 2
|
|
assert ("alice", "alice@example.com") in rows
|
|
|
|
|
|
def test_mark_table_refreshed(cache, source_conn):
|
|
cache.load_table("users", ["name"], source_conn)
|
|
row = cache.connection.execute(
|
|
"SELECT row_count FROM _sqlmem_tables WHERE table_name = 'users'"
|
|
).fetchone()
|
|
assert row[0] == 2
|
|
|
|
|
|
def test_backup_and_reload(tmp_path, source_conn):
|
|
db_path = tmp_path / "cache.db"
|
|
c1 = CacheManager(db_path=db_path, backup_interval=9999)
|
|
c1.load_table("users", ["name"], source_conn)
|
|
c1.close()
|
|
|
|
c2 = CacheManager(db_path=db_path, backup_interval=9999)
|
|
assert c2.is_table_cached("users") is True
|
|
c2.close()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Disk-backed mode (in_memory=False)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_disk_mode_persists_without_backup(tmp_path, source_conn):
|
|
"""Disk mode writes straight to the file — no explicit backup/close needed."""
|
|
db_path = tmp_path / "cache.db"
|
|
c = CacheManager(db_path=db_path, backup_interval=9999, in_memory=False)
|
|
c.load_table("users", ["name"], source_conn)
|
|
# Data is already on disk; a brand-new disk-mode manager sees it.
|
|
c2 = CacheManager(db_path=db_path, backup_interval=9999, in_memory=False)
|
|
assert c2.is_table_cached("users") is True
|
|
c2.close()
|
|
c.close()
|
|
|
|
|
|
def test_disk_mode_file_created_immediately(tmp_path, source_conn):
|
|
db_path = tmp_path / "cache.db"
|
|
c = CacheManager(db_path=db_path, backup_interval=9999, in_memory=False)
|
|
c.load_table("users", ["name"], source_conn)
|
|
assert db_path.exists()
|
|
c.close()
|
|
|
|
|
|
def test_disk_mode_reload_in_new_instance(tmp_path, source_conn):
|
|
db_path = tmp_path / "cache.db"
|
|
c1 = CacheManager(db_path=db_path, backup_interval=9999, in_memory=False)
|
|
c1.load_table("users", ["name", "email"], source_conn)
|
|
c1.close()
|
|
|
|
c2 = CacheManager(db_path=db_path, backup_interval=9999, in_memory=False)
|
|
rows = c2.connection.execute("SELECT name FROM users").fetchall()
|
|
assert {r[0] for r in rows} == {"alice", "bob"}
|
|
c2.close()
|
|
|
|
|
|
def test_quoted_reserved_and_spaced_identifiers(tmp_path):
|
|
"""Table/column names that are reserved words or contain spaces must work."""
|
|
src = sqlite3.connect(":memory:")
|
|
src.execute('CREATE TABLE "weird tbl" ("order" TEXT, "group by" TEXT)')
|
|
src.executemany('INSERT INTO "weird tbl" VALUES (?, ?)', [("1", "a"), ("2", "b")])
|
|
src.commit()
|
|
|
|
c = CacheManager(db_path=tmp_path / "c.db", backup_interval=9999)
|
|
c.load_table("weird tbl", ["order", "group by"], src)
|
|
assert c.is_table_cached("weird tbl") is True
|
|
_, rows = c.execute_in_memory('SELECT "order", "group by" FROM "weird tbl"')
|
|
assert ("1", "a") in rows
|
|
c.close()
|
|
src.close()
|
|
|
|
|
|
def test_disk_mode_uses_separate_read_connection(tmp_path, source_conn):
|
|
"""Disk-mode reads go through a per-thread read connection, not the writer."""
|
|
c = CacheManager(db_path=tmp_path / "c.db", backup_interval=9999, in_memory=False)
|
|
c.load_table("users", ["name", "email"], source_conn)
|
|
|
|
_, rows = c.execute_in_memory("SELECT name FROM users ORDER BY name")
|
|
assert [r[0] for r in rows] == ["alice", "bob"]
|
|
assert len(c._read_conns) == 1
|
|
assert c._read_conns[0] is not c.connection # dedicated read conn
|
|
c.close()
|
|
|
|
|
|
def test_disk_mode_concurrent_reads(tmp_path, source_conn):
|
|
"""Several reader threads each get their own connection and correct results."""
|
|
c = CacheManager(db_path=tmp_path / "c.db", backup_interval=9999, in_memory=False)
|
|
c.load_table("users", ["name"], source_conn)
|
|
|
|
results: list[int] = []
|
|
errors: list[Exception] = []
|
|
|
|
def reader() -> None:
|
|
try:
|
|
_, rows = c.execute_in_memory("SELECT name FROM users")
|
|
results.append(len(rows))
|
|
except Exception as e: # noqa: BLE001
|
|
errors.append(e)
|
|
|
|
threads = [threading.Thread(target=reader) for _ in range(5)]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join(5)
|
|
|
|
assert not errors
|
|
assert results == [2] * 5
|
|
assert len(c._read_conns) == 5 # one read connection per reader thread
|
|
c.close()
|
|
|
|
|
|
def test_memory_mode_uses_shared_connection(cache, source_conn):
|
|
"""In-memory mode can't share :memory: across connections — no read conns."""
|
|
cache.load_table("users", ["name"], source_conn)
|
|
cache.execute_in_memory("SELECT name FROM users")
|
|
assert cache._read_conns == []
|
|
|
|
|
|
def test_disk_mode_reset_keeps_file(tmp_path, source_conn):
|
|
db_path = tmp_path / "cache.db"
|
|
c = CacheManager(db_path=db_path, backup_interval=9999, in_memory=False)
|
|
c.load_table("users", ["name"], source_conn)
|
|
c.reset()
|
|
# File stays (the connection is still open) but the table is gone.
|
|
assert db_path.exists()
|
|
assert c.is_table_cached("users") is False
|
|
c.close()
|