""" Async SQLAlchemy Engine & Session factory for PostgreSQL (Neon) """ from __future__ import annotations import logging from typing import Optional from sqlalchemy.ext.asyncio import ( AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine, ) from config import settings logger = logging.getLogger(__name__) engine: Optional[AsyncEngine] = None async_session_factory: Optional[async_sessionmaker[AsyncSession]] = None def _build_engine() -> AsyncEngine: if not settings.database_url: raise RuntimeError( "DATABASE_URL is not set. " "Provide a Neon / PostgreSQL async connection string, e.g. " "postgresql+asyncpg://user:pass@host/db?sslmode=require" ) url = settings.database_url # Ensure async driver if url.startswith("postgresql://"): url = url.replace("postgresql://", "postgresql+asyncpg://", 1) elif url.startswith("postgres://"): url = url.replace("postgres://", "postgresql+asyncpg://", 1) # 🔥 CRITICAL FIX: Remove sslmode from URL and use connect_args instead # This fixes: TypeError: connect() got an unexpected keyword argument 'sslmode' ssl_mode = None if "?sslmode=" in url: # Extract and remove sslmode from URL url_parts = url.split("?") base_url = url_parts[0] params = url_parts[1] if len(url_parts) > 1 else "" # Parse params and remove sslmode param_list = [] for param in params.split("&"): if param.startswith("sslmode="): ssl_mode = param.split("=")[1] else: param_list.append(param) # Rebuild URL without sslmode if param_list: url = f"{base_url}?{'&'.join(param_list)}" else: url = base_url # Configure SSL via connect_args (asyncpg compatible) connect_args = {} if ssl_mode: # Map sslmode to asyncpg's ssl parameter if ssl_mode in ("require", "verify-ca", "verify-full", "prefer"): connect_args["ssl"] = True elif ssl_mode == "disable": connect_args["ssl"] = False elif ssl_mode == "allow": connect_args["ssl"] = True # asyncpg prefers SSL when available # If no sslmode was found but we're using Neon, enable SSL by default if not ssl_mode and "neon.tech" in url: connect_args["ssl"] = True logger.info("🔒 Neon detected: enabling SSL by default") if connect_args: logger.info(f"🔧 Using connect_args: {connect_args}") return create_async_engine( url, echo=False, pool_size=10, max_overflow=20, pool_pre_ping=True, pool_recycle=300, connect_args=connect_args, # 👈 THE FIX ) async def init_database() -> None: """Create engine + session factory + tables. Call once at startup.""" global engine, async_session_factory if engine is not None: return engine = _build_engine() async_session_factory = async_sessionmaker( bind=engine, class_=AsyncSession, expire_on_commit=False, autoflush=False, autocommit=False, ) # إنشاء الجداول تلقائيًا (بديل مجاني عن Alembic على Render) from database.base import Base import database.models # noqa: F401 — لازم عشان تتحمل كل الجداول async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) logger.info("✅ SQLAlchemy async engine initialized + tables created") async def close_database() -> None: """Dispose engine on shutdown.""" global engine, async_session_factory if engine is not None: await engine.dispose() engine = None async_session_factory = None logger.info("✅ SQLAlchemy engine disposed")