You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

68 lines
3.1 KiB
Python

# 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)
("email", "dismissed", "BOOLEAN NOT NULL DEFAULT 0"), # 아리 정리에서 기각(다이제스트 제외)
("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