# backend/app/db.py from sqlalchemy import event, text from sqlmodel import Session, SQLModel, create_engine from .config import get_settings settings = get_settings() _is_sqlite = settings.database_url.startswith("sqlite") # SQLite + FastAPI: check_same_thread=False 필요. timeout = busy_timeout(초) — 쓰기 잠금 대기. connect_args = {"check_same_thread": False, "timeout": 30} if _is_sqlite else {} engine = create_engine(settings.database_url, echo=False, connect_args=connect_args) if _is_sqlite: # WAL: 동기화(쓰기) 중에도 API(읽기)가 막히지 않게 — 'database is locked' 500 방지. @event.listens_for(engine, "connect") def _sqlite_pragmas(dbapi_conn, _rec): # noqa: ANN001 cur = dbapi_conn.cursor() cur.execute("PRAGMA journal_mode=WAL") cur.execute("PRAGMA busy_timeout=30000") cur.execute("PRAGMA synchronous=NORMAL") cur.close() # 개발용 추가 컬럼(create_all 은 기존 테이블에 컬럼을 추가하지 못함). # 운영은 alembic 마이그레이션을 쓰고, 여기서는 기존 SQLite DB 데이터를 보존한 채 # 누락 컬럼만 멱등하게 ALTER ADD 한다. 전부 additive + 기본값 보유라 안전. _DEV_COLUMNS: list[tuple[str, str, str]] = [ ("email", "cc", "TEXT NOT NULL DEFAULT ''"), ("email", "thread_id", "TEXT NOT NULL DEFAULT ''"), ("email", "body_html", "TEXT NOT NULL DEFAULT ''"), ("email", "received_at", "TIMESTAMP"), # 수신 시각(최신순 정렬·표시). NULL 허용. ("email", "folder", "TEXT NOT NULL DEFAULT 'inbox'"), # 시스템 폴더/버킷 슬러그 ("email", "folder_name", "TEXT NOT NULL DEFAULT ''"), # Outlook 커스텀 폴더 표시명 ("email", "inference", "TEXT NOT NULL DEFAULT ''"), # Outlook 집중/기타(focused/other) ("outbound_mail", "cc", "TEXT NOT NULL DEFAULT ''"), ("outbound_mail", "bcc", "TEXT NOT NULL DEFAULT ''"), ("outbound_mail", "attachments", "JSON NOT NULL DEFAULT '[]'"), ("cal_event", "rrule", "TEXT NOT NULL DEFAULT ''"), ("cal_event", "reminders", "JSON NOT NULL DEFAULT '[]'"), ("cal_event", "attendees", "JSON NOT NULL DEFAULT '[]'"), ("cal_event", "response_status", "TEXT NOT NULL DEFAULT ''"), ("task_comment", "edited_at", "DATETIME"), ] def _ensure_dev_columns() -> None: if not settings.database_url.startswith("sqlite"): return with engine.begin() as conn: for table, column, ddl in _DEV_COLUMNS: rows = conn.exec_driver_sql(f"PRAGMA table_info({table})").fetchall() if not rows: # 테이블 자체가 없으면 create_all 이 만들어 준다 continue existing = {r[1] for r in rows} if column in existing: continue conn.execute(text(f'ALTER TABLE {table} ADD COLUMN {column} {ddl}')) def init_db() -> None: SQLModel.metadata.create_all(engine) _ensure_dev_columns() def get_session(): with Session(engine) as session: yield session