"""스키마 마이그레이션 — 이미 만들어진 DB 를 init.sql 최신으로 끌어올린다.""" import argparse import asyncio import os import sys from pathlib import Path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) os.environ.setdefault("APP_ENV", "local") from sqlalchemy import text # noqa: E402 from common.database.db_session_manager import DB_SESSION_MNG # noqa: E402 from common.enums import DBWRType # noqa: E402 from common.database.model.models import places # noqa: E402 # parents[3] = 레포 루트 (scripts → backend → solution → 루트). MIGRATIONS_DIR = Path(__file__).resolve().parents[3] / "postgres-init" / "migrations" _LEDGER_DDL = """ CREATE TABLE IF NOT EXISTS public.schema_migrations ( version VARCHAR(255) PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """ def pending(applied: set[str]) -> list[Path]: """아직 안 돌린 파일.""" files = sorted(p for p in MIGRATIONS_DIR.glob("*.sql")) return [p for p in files if p.stem not in applied] async def main(dry_run: bool) -> int: if not MIGRATIONS_DIR.is_dir(): print(f"마이그레이션 폴더가 없습니다: {MIGRATIONS_DIR}") return 1 db = await DB_SESSION_MNG.start_session(places.DBType(), DBWRType.DB_WRITE.value) try: await db.execute(text(_LEDGER_DDL)) await db.commit() rows = await db.execute(text("SELECT version FROM public.schema_migrations")) applied = {r[0] for r in rows} todo = pending(applied) if not todo: print(f"적용할 마이그레이션이 없습니다 (적용됨 {len(applied)}건)") return 0 print(f"적용 대상 {len(todo)}건:") for path in todo: print(f" {path.stem}") if dry_run: print("\n--dry-run — 아무것도 적용하지 않았습니다.") return 0 for path in todo: sql = path.read_text(encoding="utf-8") print(f"\n▶ {path.stem}") try: # 파일 하나를 한 트랜잭션으로 돌린다 — 중간에 실패하면 그 파일은 통째로 되돌아간다. raw = await (await db.connection()).get_raw_connection() await raw.driver_connection.execute(sql) await db.execute( text("INSERT INTO public.schema_migrations (version) VALUES (:v)"), {"v": path.stem}, ) await db.commit() print(" 적용됨") except Exception as ex: await db.rollback() print(f" 실패 — {ex}") print(" ★ 여기서 멈춥니다. 뒤 파일은 돌리지 않습니다(순서가 뜻을 갖는다).") return 1 print(f"\n완료 — {len(todo)}건 적용") return 0 finally: await DB_SESSION_MNG.end_session(places.DBType(), DBWRType.DB_WRITE.value) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--dry-run", action="store_true", help="적용하지 않고 대상만 출력") args = parser.parse_args() raise SystemExit(asyncio.run(main(args.dry_run)))