# -*- coding: utf-8 -*- """비동기 DB 세션 + Base + 공통 타임스탬프 믹스인. castad 는 공통 base 가 없어 컬럼을 매번 반복하지만, 여기선 TimestampMixin 으로 정리. """ from collections.abc import AsyncGenerator from datetime import datetime from sqlalchemy import DateTime, func from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from .config import settings engine = create_async_engine(settings.DATABASE_URL, echo=False, future=True) SessionLocal = async_sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False, autoflush=False) class Base(DeclarativeBase): pass class TimestampMixin: created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) async def get_session() -> AsyncGenerator[AsyncSession, None]: async with SessionLocal() as session: try: yield session except Exception: await session.rollback() raise async def create_tables() -> None: from . import models # noqa: F401 (테이블 메타데이터 등록) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) await _lightweight_migrate(conn) # create_all 은 기존 테이블에 새 컬럼을 추가하지 않는다. 개발용 SQLite 를 위한 최소 마이그레이션. # (운영 MySQL 은 Alembic 등 정식 마이그레이션 사용 권장) _ADD_COLUMNS = { "user": [ ("profile_image_url", "TEXT"), ("bio", "VARCHAR(200)"), ], } async def _lightweight_migrate(conn) -> None: from sqlalchemy import text dialect = conn.dialect.name if dialect != "sqlite": return # 운영 DB 는 정식 마이그레이션으로 처리 for table, cols in _ADD_COLUMNS.items(): rows = (await conn.exec_driver_sql(f"PRAGMA table_info({table})")).fetchall() existing = {r[1] for r in rows} for name, ddl in cols: if name not in existing: await conn.exec_driver_sql(f"ALTER TABLE {table} ADD COLUMN {name} {ddl}")