From 4ca89f22cc0ecb234e407d22903d4ff92bf4d3cf Mon Sep 17 00:00:00 2001 From: hyungi Date: Sat, 27 Jun 2026 06:54:54 +0900 Subject: [PATCH] =?UTF-8?q?fix(migration):=20validator=20=EA=B0=80=20?= =?UTF-8?q?=EC=9D=B8=EB=9D=BC=EC=9D=B8=20=EC=A3=BC=EC=84=9D=EC=9D=84=20fal?= =?UTF-8?q?se-positive=20=E2=80=94=20fresh=20DB=20=EB=B6=80=ED=8A=B8?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=9E=A9=20=EA=B9=A8=EC=A7=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _validate_sql_content + schema_migrations 검사 둘 다 전체 줄 주석만 제거하고 인라인 주석(SQL -- ...)은 안 지워서, 365_scan_jobs 의 '-- commit 시 ...' 와 '-- ... schema_migrations 를 건드리지 않음' 설명 주석 단어를 트랜잭션 제어문/테이블 수정으로 오탐 → fresh DB/DR 복원 부트스트랩 실패(verification env 실측). 주석 제외(줄별 -- 이후 제거)로 근본 수정 + 두 검사 _validate_sql_content 로 통합. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/core/database.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/app/core/database.py b/app/core/database.py index cbf4052..b7ebc88 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -57,12 +57,12 @@ def _parse_migration_files(migrations_dir: Path) -> list[tuple[int, str, Path]]: def _validate_sql_content(name: str, sql: str) -> None: """migration SQL에 BEGIN/COMMIT이 포함되어 있으면 에러 (외부 트랜잭션 깨짐 방지)""" - # 주석(-- ...) 라인 제거 후 검사 - lines = [ - line for line in sql.splitlines() - if not line.strip().startswith("--") - ] - stripped = "\n".join(lines).upper() + # 주석(전체 줄 + 인라인 `-- ...`) 제거 후 검사. ★인라인 주석을 안 지우면 설명 주석의 + # 'commit/begin' 단어(예 365_scan_jobs 의 `-- commit 시 documents.title 로 전파`)를 + # 트랜잭션 제어문으로 false-positive 로 잡아 fresh DB/DR 부트스트랩이 깨진다(verification + # 실측 2026-06). 줄별로 `--` 이후를 잘라 주석 텍스트를 검사에서 제외. + cleaned = [re.sub(r"--.*$", "", line) for line in sql.splitlines()] + stripped = "\n".join(cleaned).upper() for keyword in ("BEGIN", "COMMIT", "ROLLBACK"): # 단어 경계로 매칭 (예: BEGIN_SOMETHING은 제외) if re.search(rf"\b{keyword}\b", stripped): @@ -70,6 +70,13 @@ def _validate_sql_content(name: str, sql: str) -> None: f"migration {name}에 {keyword} 포함됨 — " f"migration SQL에는 트랜잭션 제어문을 넣지 마세요" ) + # schema_migrations 수정 금지 (runner 가 스탬프 관리) — 주석 제외(stripped) 검사. + # (구: _run_migrations 의 raw `"schema_migrations" in sql.lower()` 가 주석 미제외라 + # 365 의 '-- ... schema_migrations 를 건드리지 않음' 주석을 false-positive 로 잡았음.) + if "SCHEMA_MIGRATIONS" in stripped: + raise RuntimeError( + f"Migration {name} must not modify schema_migrations table" + ) # R1: baseline 스냅샷이 대표하는 마지막 마이그레이션 버전 (이하 버전은 baseline 에 포함). @@ -167,11 +174,7 @@ async def _run_migrations(conn) -> None: for version, name, path in pending: sql = path.read_text(encoding="utf-8") - _validate_sql_content(name, sql) - if "schema_migrations" in sql.lower(): - raise ValueError( - f"Migration {name} must not modify schema_migrations table" - ) + _validate_sql_content(name, sql) # BEGIN/COMMIT + schema_migrations 검사(주석 제외) logger.info(f"[migration] {name} 실행 중...") # raw driver SQL 사용 — text() 의 :name bind parameter 해석으로 # SQL 주석/literal 에 콜론이 들어가면 InvalidRequestError 발생.