From d8ad097a3a3c64075bbdd4364f7b437cd21fde47 Mon Sep 17 00:00:00 2001 From: hyungi Date: Tue, 16 Jun 2026 13:11:55 +0900 Subject: [PATCH] =?UTF-8?q?ops(migrations):=20fresh-DB/DR=20replay=C2=B7en?= =?UTF-8?q?um=20=EC=8A=A4=EB=AA=A8=ED=81=AC=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=20(R0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit init_db 의 단일 트랜잭션 적용 경로(engine.begin)를 미러해 migrations/ 전체가 빈 DB / DR(pre-320 → catch-up) 업그레이드에서 한 트랜잭션으로 적용 가능한지 검증. pg16(pgvector/pgvector:pg16) 핀, ephemeral 컨테이너 자동 기동/정리. 현재 두 시나리오 모두 011_embedding_1024 에서 FAIL — view active_documents 가 documents.embedding 의존(DROP COLUMN CASCADE 부재). enum(326) 이전 지점. fresh replay 가 한 번도 검증된 적 없어 누적 비-replayable cruft 다수 확인. R1(스키마 baseline 스냅샷)으로 fix 후 PASS 가 게이트 기준. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/ci/migration_smoke.sh | 125 ++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100755 scripts/ci/migration_smoke.sh diff --git a/scripts/ci/migration_smoke.sh b/scripts/ci/migration_smoke.sh new file mode 100755 index 0000000..244c9a8 --- /dev/null +++ b/scripts/ci/migration_smoke.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# migration_smoke.sh — fresh-DB + DR enum-same-txn 게이트 (plan ds-backend-audit-1 R0) +# +# app/core/database.py 의 init_db() 는 모든 pending migration 을 단일 트랜잭션 +# (`async with engine.begin()`) 으로 적용한다. 이 스크립트는 그 경로를 미러해 +# migrations/ 전체가 빈 DB / DR 업그레이드에서 한 트랜잭션으로 적용 가능한지 검증한다. +# +# 시나리오: +# FRESH — 빈 DB 에 migrations/ 전체를 단일 트랜잭션으로 적용 (신규 환경 부팅 경로) +# DR — 001~319 를 커밋(과거 운영 DB 모사) 후 320~end 를 단일 트랜잭션으로 적용 +# (pre-320 백업/지연 복제를 320 경계 너머로 catch-up 업그레이드하는 재해복구 경로) +# +# enum-same-txn 결함(ALTER TYPE ADD VALUE 한 값을 같은 트랜잭션에서 사용)이 있으면 +# 두 시나리오 모두 'unsafe use of new value' 로 abort 한다. +# R1(enum-barrier) fix 후에는 두 시나리오 모두 PASS 해야 한다. +# +# prod 동일 이미지(pg16)로 핀. 의존: docker. +# 사용: scripts/ci/migration_smoke.sh (ephemeral 컨테이너 자동 기동/정리) +set -uo pipefail + +IMAGE="pgvector/pgvector:pg16" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MIG_DIR="$(cd "$SCRIPT_DIR/../../migrations" && pwd)" +CNAME="ds-mig-smoke-$$" +DB="pkm" # 358 의 ALTER DATABASE pkm 가 이 이름을 요구 + +cleanup() { docker rm -f "$CNAME" >/dev/null 2>&1 || true; } +trap cleanup EXIT + +# 버전순 마이그레이션 파일 목록 (NNN_ 3자리 zero-pad → lexical = numeric) +# bash 3.2(macOS) 호환 — mapfile 미사용 +MIGS=() +while IFS= read -r _line; do MIGS+=("$_line"); done < <(ls "$MIG_DIR"/[0-9]*.sql | sort) +[ "${#MIGS[@]}" -gt 0 ] || { echo "FATAL: migrations 없음 ($MIG_DIR)"; exit 2; } +echo "migrations: ${#MIGS[@]}건 ($(basename "${MIGS[0]}") ~ $(basename "${MIGS[$((${#MIGS[@]}-1))]}"))" + +psql_exec() { docker exec -i "$CNAME" psql -U postgres -v ON_ERROR_STOP=1 "$@"; } + +# 주어진 파일 범위를 단일 트랜잭션 스트림으로 묶어 출력 (psql stdin 용) +# 각 파일 앞에 \echo 마커 — 실패 시 마지막 마커가 깨진 마이그레이션. +emit_single_txn() { + echo '\set ON_ERROR_STOP on' + echo 'BEGIN;' + for f in "$@"; do + echo "\\echo >>>APPLY $(basename "$f")" + cat "$f"; echo + done + echo 'COMMIT;' +} + +# 자동커밋(파일별 즉시 커밋) 스트림 — DR phase1 (기존 운영 DB 모사) +emit_autocommit() { + echo '\set ON_ERROR_STOP on' + for f in "$@"; do + echo "\\echo >>>APPLY $(basename "$f")" + cat "$f"; echo + done +} + +reset_db() { + psql_exec -d postgres -c "DROP DATABASE IF EXISTS $DB" >/dev/null 2>&1 + psql_exec -d postgres -c "CREATE DATABASE $DB" >/dev/null +} + +run_scenario() { + local name="$1"; shift + local out rc last_apply + out="$( "$@" 2>&1 )"; rc=$? + last_apply="$(printf '%s\n' "$out" | grep '>>>APPLY' | tail -1 | sed 's/>>>APPLY //')" + if [ "$rc" -eq 0 ]; then + echo " [$name] PASS — 전체 적용 성공" + return 0 + else + echo " [$name] FAIL — 깨진 지점: ${last_apply:-?}" + printf '%s\n' "$out" | grep -iE 'ERROR|unsafe|HINT' | head -3 | sed 's/^/ /' + return 1 + fi +} + +scenario_fresh() { + reset_db + emit_single_txn "${MIGS[@]}" | psql_exec -d "$DB" +} + +scenario_dr() { + reset_db + local phase1=() phase2=() f base ver p1out p1rc + for f in "${MIGS[@]}"; do + base="$(basename "$f")"; ver="${base%%_*}"; ver="$((10#$ver))" + if [ "$ver" -le 319 ]; then phase1+=("$f"); else phase2+=("$f"); fi + done + # phase1: 001~319 자동커밋 (과거 운영 DB = 타입/값 모두 커밋된 상태) + p1out="$( emit_autocommit "${phase1[@]}" 2>/dev/null | psql_exec -d "$DB" 2>&1 )"; p1rc=$? + if [ "$p1rc" -ne 0 ]; then + local p1last; p1last="$(printf '%s\n' "$p1out" | grep '>>>APPLY' | tail -1 | sed 's/>>>APPLY //')" + printf '%s\n' ">>>APPLY ${p1last}" # run_scenario 가 마지막 마커를 읽도록 전달 + printf '%s\n' "$p1out" | grep -iE 'ERROR|unsafe|DETAIL' | head -2 + return 1 + fi + # phase2: 320~end 단일 트랜잭션 (catch-up 업그레이드) + emit_single_txn "${phase2[@]}" 2>/dev/null | psql_exec -d "$DB" +} + +# ── 컨테이너 기동 ── +echo "기동: $IMAGE ($CNAME)" +docker run -d --name "$CNAME" -e POSTGRES_PASSWORD=x -e POSTGRES_HOST_AUTH_METHOD=trust "$IMAGE" >/dev/null +for _ in $(seq 1 40); do docker exec "$CNAME" pg_isready -U postgres -q 2>/dev/null && break; sleep 0.5; done +echo "pg: $(docker exec "$CNAME" psql -U postgres -tAc 'show server_version' 2>/dev/null)" +echo + +fail=0 +echo "── FRESH (빈 DB 단일 트랜잭션) ──" +run_scenario FRESH scenario_fresh || fail=1 +echo +echo "── DR (001~319 커밋 후 320~end 단일 트랜잭션) ──" +run_scenario DR scenario_dr || fail=1 +echo + +if [ "$fail" -eq 0 ]; then + echo "RESULT: PASS — 빈 DB/DR 모두 단일 트랜잭션 적용 가능 (enum-barrier 적용됨)" + exit 0 +else + echo "RESULT: FAIL — 위 지점에서 단일 트랜잭션 적용 불가 (enum-same-txn 등 미수정)" + exit 1 +fi