여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다. - ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제 - 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다 - eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다 파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이). site·frontend·admin tsc, site vitest 105 passed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
87 lines
3.2 KiB
Python
87 lines
3.2 KiB
Python
"""스키마 마이그레이션 — 이미 만들어진 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)))
|