0d3c841577
R0 가 입증했듯 migrations/ 전체 replay 는 011(view active_documents 가 documents.embedding 의존, DROP COLUMN CASCADE 부재)·326(enum-same-txn) 등 누적 비-replayable 로 깨져 신규/DR 환경 init_db 부팅이 불가능했다. 표준 squash baseline 로 해소: - migrations/_baseline/0358_schema_baseline.sql: prod 스키마 스냅샷(pg_dump --schema-only --no-owner --no-privileges, psql 메타·search_path='' 정리 = asyncpg exec_driver_sql 호환). - init_db._load_baseline_if_fresh: documents 테이블 부재(fresh) 시 baseline 적재 + schema_migrations 1..358 스탬프 → 이후 post-baseline(359/360)만 적용. ★기존 DB(documents 존재)는 skip = prod 무영향(additive). baseline 부재 시 기존 replay 경로(하위호환). - migration_smoke: baseline 경로 검증. ★실측 — 이전 FAIL(011 abort) → 이제 FRESH/INCREMENTAL 모두 PASS (pg16.14). cutoff(_BASELINE_CUTOFF=358) 갱신 시 baseline 재생성. 검증: py_compile + migration_smoke PASS. ★boot-path 변경이라 deploy 전 staging 부팅 검증 필수. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
5.3 KiB
Bash
Executable File
139 lines
5.3 KiB
Bash
Executable File
#!/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
|
|
}
|
|
|
|
BASELINE_CUTOFF=358
|
|
BASELINE_FILE="$MIG_DIR/_baseline/0358_schema_baseline.sql"
|
|
|
|
# post-baseline(버전 > cutoff) 마이그 파일만 출력
|
|
_post_baseline() {
|
|
local f base ver
|
|
for f in "${MIGS[@]}"; do
|
|
base="$(basename "$f")"; ver="${base%%_*}"; ver="$((10#$ver))"
|
|
[ "$ver" -gt "$BASELINE_CUTOFF" ] && printf '%s\n' "$f"
|
|
done
|
|
}
|
|
|
|
# FRESH — init_db fresh 경로 미러: baseline 적재 + post-baseline 을 단일 트랜잭션
|
|
scenario_fresh() {
|
|
reset_db
|
|
local post=(); while IFS= read -r f; do post+=("$f"); done < <(_post_baseline)
|
|
{
|
|
echo '\set ON_ERROR_STOP on'; echo 'BEGIN;'
|
|
echo "\\echo >>>APPLY _baseline"
|
|
cat "$BASELINE_FILE"; echo
|
|
for f in "${post[@]}"; do
|
|
echo "\\echo >>>APPLY $(basename "$f")"; cat "$f"; echo
|
|
done
|
|
echo 'COMMIT;'
|
|
} | psql_exec -d "$DB"
|
|
}
|
|
|
|
# INCREMENTAL — 기존 운영 DB(at cutoff) 모사: baseline 커밋 후 post-baseline 을 별 트랜잭션
|
|
scenario_dr() {
|
|
reset_db
|
|
if ! { echo '\set ON_ERROR_STOP on'; cat "$BASELINE_FILE"; } | psql_exec -d "$DB" >/dev/null 2>&1; then
|
|
printf '%s\n' ">>>APPLY _baseline"; echo "baseline 적재 실패"; return 1
|
|
fi
|
|
local post=(); while IFS= read -r f; do post+=("$f"); done < <(_post_baseline)
|
|
emit_single_txn "${post[@]}" 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 (baseline 적재 + post-baseline 단일 트랜잭션 = init_db fresh 경로) ──"
|
|
run_scenario FRESH scenario_fresh || fail=1
|
|
echo
|
|
echo "── INCREMENTAL (baseline 커밋 후 post-baseline 별 트랜잭션 = 기존 DB 증분) ──"
|
|
run_scenario DR scenario_dr || fail=1
|
|
echo
|
|
|
|
if [ "$fail" -eq 0 ]; then
|
|
echo "RESULT: PASS — fresh/incremental 모두 baseline+post-baseline 적용 가능"
|
|
exit 0
|
|
else
|
|
echo "RESULT: FAIL — baseline/post-baseline 적용 불가 (위 지점)"
|
|
exit 1
|
|
fi
|