import os from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.orm import DeclarativeBase def _to_asyncpg(url: str) -> str: if url.startswith("postgresql://"): return url.replace("postgresql://", "postgresql+asyncpg://", 1) return url # Runtime connection — the scoped `kds` DB role, used for every request via # get_db(). Only has SELECT/INSERT/UPDATE/DELETE on kds_tickets, # kds_course_bumps and the specific kitchen_settings columns KDS needs (see # deploy_kds grants) — it cannot read the rest of kitchen_db. DATABASE_URL = _to_asyncpg(os.getenv( "DATABASE_URL", "postgresql://kitchen:kitchen_secret@localhost:5432/kitchen_gp" )) # Migration connection — the privileged `kitchen` DB role. Used ONLY at # startup to create KDS's own tables and ALTER kitchen_settings (adding KDS # columns). Never used for request handling. Falls back to DATABASE_URL for # local/dev convenience, but production must set this separately once the # `kds` role is scoped down (see port log E17/task: KDS DB role split). MIGRATION_DATABASE_URL = _to_asyncpg( os.getenv("MIGRATION_DATABASE_URL") or os.getenv( "DATABASE_URL", "postgresql://kitchen:kitchen_secret@localhost:5432/kitchen_gp" ) ) # `engine` stays the name existing migration files (`from database import # engine`) already import — no changes needed there. It's bound to the # privileged connection. engine = create_async_engine(MIGRATION_DATABASE_URL, echo=False) runtime_engine = create_async_engine( DATABASE_URL, echo=False, pool_size=10, # Default is 5 max_overflow=20, # Default is 10 - allows burst to 30 connections pool_pre_ping=True # Verify connections are alive before use ) AsyncSessionLocal = async_sessionmaker( runtime_engine, class_=AsyncSession, expire_on_commit=False ) class Base(DeclarativeBase): pass async def get_db(): async with AsyncSessionLocal() as session: try: yield session finally: await session.close()