62 lines
2.3 KiB
Python
62 lines
2.3 KiB
Python
"""Regression tests for B-39: SQLite must run in WAL mode with a busy_timeout,
|
|||
|
|
since this app has five concurrent background tasks plus every HTTP handler
|
||
|
|
sharing one database file, and the default rollback-journal mode lets a writer
|
||
|
|
block every reader and fails a second writer immediately instead of waiting."""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||
|
|
|
||
|
|
from app.db.base import _SQLITE_BUSY_TIMEOUT_MS, _register_sqlite_pragmas
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
async def sqlite_engine(tmp_path):
|
||
|
|
# WAL needs a real file (it writes a companion -wal/-shm file alongside it) —
|
||
|
|
# ":memory:" wouldn't exercise the same path.
|
||
|
|
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path}/test.db")
|
||
|
|
yield engine
|
||
|
|
await engine.dispose()
|
||
|
|
|
||
|
|
|
||
|
|
async def _pragma(engine, name: str):
|
||
|
|
async with engine.connect() as conn:
|
||
|
|
result = await conn.exec_driver_sql(f"PRAGMA {name}")
|
||
|
|
return result.fetchone()[0]
|
||
|
|
|
||
|
|
|
||
|
|
async def test_register_sqlite_pragmas_enables_wal_and_busy_timeout(sqlite_engine):
|
||
|
|
_register_sqlite_pragmas(sqlite_engine)
|
||
|
|
|
||
|
|
assert (await _pragma(sqlite_engine, "journal_mode")).lower() == "wal"
|
||
|
|
assert await _pragma(sqlite_engine, "busy_timeout") == _SQLITE_BUSY_TIMEOUT_MS
|
||
|
|
assert await _pragma(sqlite_engine, "synchronous") == 1 # NORMAL
|
||
|
|
|
||
|
|
|
||
|
|
async def test_register_sqlite_pragmas_applies_to_every_new_connection(sqlite_engine):
|
||
|
|
"""The pool can open more than one underlying DBAPI connection over the
|
||
|
|
engine's lifetime — the pragmas must be re-applied to each one, not just
|
||
|
|
the first, or a later connection would silently fall back to SQLite's
|
||
|
|
defaults."""
|
||
|
|
_register_sqlite_pragmas(sqlite_engine)
|
||
|
|
|
||
|
|
async with sqlite_engine.connect() as first:
|
||
|
|
await first.exec_driver_sql("PRAGMA journal_mode")
|
||
|
|
|
||
|
|
async with sqlite_engine.connect() as second:
|
||
|
|
result = await second.exec_driver_sql("PRAGMA busy_timeout")
|
||
|
|
assert result.fetchone()[0] == _SQLITE_BUSY_TIMEOUT_MS
|
||
|
|
|
||
|
|
|
||
|
|
def test_register_sqlite_pragmas_is_a_noop_for_other_dialects():
|
||
|
|
"""Must not touch (or crash on) a non-sqlite engine — e.g. a future
|
||
|
|
PostgreSQL DATABASE_URL, which neither needs nor understands these
|
||
|
|
pragmas."""
|
||
|
|
|
||
|
|
class _FakeDialect:
|
||
|
|
name = "postgresql"
|
||
|
|
|
||
|
|
class _FakeEngine:
|
||
|
|
dialect = _FakeDialect()
|
||
|
|
|
||
|
|
_register_sqlite_pragmas(_FakeEngine()) # must not raise
|