2026-07-27 14:53:22 +02:00
|
|
|
from sqlalchemy import event
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
|
2026-07-21 10:25:23 +02:00
|
|
|
from sqlalchemy.orm import DeclarativeBase
|
|
|
|
|
|
|
|
|
|
from app.config import settings
|
|
|
|
|
|
2026-07-27 14:53:22 +02:00
|
|
|
# How long a writer waits for a lock held by another writer before SQLite raises
|
|
|
|
|
# "database is locked" (B-39). A few seconds is enough to ride out this app's own
|
|
|
|
|
# five concurrent background tasks (scheduler, confirmation poller, RBF bumper,
|
|
|
|
|
# two reconcilers) plus HTTP handlers briefly overlapping a write.
|
|
|
|
|
_SQLITE_BUSY_TIMEOUT_MS = 5000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _register_sqlite_pragmas(target_engine: AsyncEngine) -> None:
|
|
|
|
|
"""Without WAL, SQLite's default (rollback-journal) mode lets a writer block
|
|
|
|
|
every reader for the duration of its transaction, and a second writer arriving
|
|
|
|
|
while one is already active fails immediately rather than waiting at all —
|
|
|
|
|
realistic under this app's concurrency, and nothing previously handled it.
|
|
|
|
|
WAL lets readers and writers proceed without blocking each other, and
|
|
|
|
|
busy_timeout gives a second writer a real window to wait for the first
|
|
|
|
|
instead of an instant `OperationalError`.
|
|
|
|
|
|
|
|
|
|
No-op for any dialect other than sqlite (e.g. a future PostgreSQL
|
|
|
|
|
DATABASE_URL), which neither needs nor understands these pragmas.
|
|
|
|
|
"""
|
|
|
|
|
if target_engine.dialect.name != "sqlite":
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
@event.listens_for(target_engine.sync_engine, "connect")
|
|
|
|
|
def _set_sqlite_pragmas(dbapi_connection, connection_record) -> None:
|
|
|
|
|
cursor = dbapi_connection.cursor()
|
|
|
|
|
try:
|
|
|
|
|
cursor.execute("PRAGMA journal_mode=WAL")
|
|
|
|
|
cursor.execute("PRAGMA synchronous=NORMAL")
|
|
|
|
|
cursor.execute(f"PRAGMA busy_timeout={_SQLITE_BUSY_TIMEOUT_MS}")
|
|
|
|
|
finally:
|
|
|
|
|
cursor.close()
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 10:25:23 +02:00
|
|
|
engine = create_async_engine(settings.database_url)
|
2026-07-27 14:53:22 +02:00
|
|
|
_register_sqlite_pragmas(engine)
|
2026-07-21 10:25:23 +02:00
|
|
|
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
|
|
|
pass
|