psql이 파이프 입력을 읽어 baseline·migrate가 첫 파일만 처리되던 문제를 /dev/null 연결과 for 루프로 해결한다. Co-authored-by: Cursor <cursoragent@cursor.com>
237 lines
5.8 KiB
Bash
237 lines
5.8 KiB
Bash
#!/bin/sh
|
|
set -eu
|
|
|
|
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
DB_SERVICE=${DB_SERVICE:-sori-studio-db}
|
|
MIGRATIONS_DIR="$ROOT_DIR/db/migrations"
|
|
CONTAINER_MIGRATIONS_DIR=${CONTAINER_MIGRATIONS_DIR:-/docker-entrypoint-initdb.d}
|
|
SCHEMA_MIGRATIONS_TABLE=${SCHEMA_MIGRATIONS_TABLE:-schema_migrations}
|
|
MODE=${1:-migrate}
|
|
BASELINE_TARGET=${2:-}
|
|
COMPOSE_ENV_FILE=
|
|
|
|
cd "$ROOT_DIR"
|
|
|
|
POSTGRES_DB=${POSTGRES_DB:-sori_studio}
|
|
POSTGRES_USER=${POSTGRES_USER:-sori_studio}
|
|
|
|
# 환경 파일(.env.production → .env) 선택
|
|
pick_compose_env_file() {
|
|
if [ -n "${ENV_FILE:-}" ] && [ -r "$ENV_FILE" ]; then
|
|
COMPOSE_ENV_FILE=$ENV_FILE
|
|
return 0
|
|
fi
|
|
|
|
if [ -r .env.production ]; then
|
|
COMPOSE_ENV_FILE=.env.production
|
|
return 0
|
|
fi
|
|
|
|
if [ -r .env ]; then
|
|
COMPOSE_ENV_FILE=.env
|
|
return 0
|
|
fi
|
|
|
|
COMPOSE_ENV_FILE=
|
|
}
|
|
|
|
# 환경 파일 로드(없으면 컨테이너 환경 변수 사용)
|
|
load_env_file() {
|
|
pick_compose_env_file
|
|
|
|
if [ -z "$COMPOSE_ENV_FILE" ]; then
|
|
echo "환경 파일(.env.production 또는 .env)이 없습니다. 실행 중인 DB 컨테이너 환경 변수를 사용합니다." >&2
|
|
return 0
|
|
fi
|
|
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "./$COMPOSE_ENV_FILE"
|
|
set +a
|
|
}
|
|
|
|
compose() {
|
|
if [ -n "$COMPOSE_ENV_FILE" ]; then
|
|
docker compose --env-file "$COMPOSE_ENV_FILE" "$@"
|
|
return 0
|
|
fi
|
|
|
|
docker compose "$@"
|
|
}
|
|
|
|
# 실행 중인 DB 컨테이너에서 PostgreSQL 계정 정보 읽기
|
|
load_db_env_from_container() {
|
|
if ! compose exec -T "$DB_SERVICE" true >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
container_db=$(compose exec -T "$DB_SERVICE" printenv POSTGRES_DB 2>/dev/null | tr -d '\r' || true)
|
|
container_user=$(compose exec -T "$DB_SERVICE" printenv POSTGRES_USER 2>/dev/null | tr -d '\r' || true)
|
|
|
|
if [ -n "$container_db" ]; then
|
|
POSTGRES_DB=$container_db
|
|
fi
|
|
|
|
if [ -n "$container_user" ]; then
|
|
POSTGRES_USER=$container_user
|
|
fi
|
|
}
|
|
|
|
# psql이 while 루프 stdin을 읽지 않도록 /dev/null 연결
|
|
psql_exec() {
|
|
compose exec -T "$DB_SERVICE" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" "$@" </dev/null
|
|
}
|
|
|
|
query_sql() {
|
|
psql_exec -At -c "$1" | tr -d '\r'
|
|
}
|
|
|
|
run_sql() {
|
|
psql_exec -c "$1"
|
|
}
|
|
|
|
ensure_database_ready() {
|
|
compose up -d "$DB_SERVICE"
|
|
|
|
attempt=1
|
|
while [ "$attempt" -le 20 ]; do
|
|
if compose exec -T "$DB_SERVICE" pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
attempt=$((attempt + 1))
|
|
sleep 1
|
|
done
|
|
|
|
echo "DB가 준비되지 않았습니다." >&2
|
|
exit 1
|
|
}
|
|
|
|
has_schema_migrations_table() {
|
|
[ "$(query_sql "SELECT to_regclass('public.$SCHEMA_MIGRATIONS_TABLE') IS NOT NULL;")" = "t" ]
|
|
}
|
|
|
|
has_application_schema() {
|
|
[ "$(query_sql "SELECT to_regclass('public.posts') IS NOT NULL;")" = "t" ]
|
|
}
|
|
|
|
ensure_schema_migrations_table() {
|
|
run_sql "CREATE TABLE IF NOT EXISTS $SCHEMA_MIGRATIONS_TABLE (
|
|
file_name TEXT PRIMARY KEY,
|
|
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);"
|
|
}
|
|
|
|
migration_files() {
|
|
find "$MIGRATIONS_DIR" -maxdepth 1 -type f -name '*.sql' -exec basename {} \; | sort
|
|
}
|
|
|
|
is_applied() {
|
|
file_name=$1
|
|
[ "$(query_sql "SELECT EXISTS (SELECT 1 FROM $SCHEMA_MIGRATIONS_TABLE WHERE file_name = '$file_name');")" = "t" ]
|
|
}
|
|
|
|
matches_baseline_target() {
|
|
file_name=$1
|
|
|
|
if [ -z "$BASELINE_TARGET" ]; then
|
|
return 0
|
|
fi
|
|
|
|
if [ "$file_name" = "$BASELINE_TARGET" ]; then
|
|
return 0
|
|
fi
|
|
|
|
echo "$file_name" | grep -q "^${BASELINE_TARGET}_"
|
|
}
|
|
|
|
print_status() {
|
|
if ! has_schema_migrations_table; then
|
|
echo "schema_migrations 테이블이 없습니다."
|
|
echo "기존 운영 DB라면 먼저 sh scripts/migrate-production-db.sh baseline 으로 현재 파일들을 적용 완료로 기록하세요."
|
|
|
|
for file_name in $(migration_files); do
|
|
echo "pending $file_name"
|
|
done
|
|
return 0
|
|
fi
|
|
|
|
for file_name in $(migration_files); do
|
|
if is_applied "$file_name"; then
|
|
echo "applied $file_name"
|
|
else
|
|
echo "pending $file_name"
|
|
fi
|
|
done
|
|
}
|
|
|
|
baseline_migrations() {
|
|
ensure_schema_migrations_table
|
|
stop_after_current=0
|
|
|
|
for file_name in $(migration_files); do
|
|
if [ "$stop_after_current" -eq 1 ]; then
|
|
break
|
|
fi
|
|
|
|
run_sql "INSERT INTO $SCHEMA_MIGRATIONS_TABLE (file_name) VALUES ('$file_name') ON CONFLICT (file_name) DO NOTHING;"
|
|
echo "baseline $file_name"
|
|
|
|
if [ -n "$BASELINE_TARGET" ] && matches_baseline_target "$file_name"; then
|
|
stop_after_current=1
|
|
fi
|
|
done
|
|
|
|
if [ -n "$BASELINE_TARGET" ] && ! migration_files | grep -q "^${BASELINE_TARGET}_\\|^${BASELINE_TARGET}$"; then
|
|
echo "기준 마이그레이션을 찾을 수 없습니다: $BASELINE_TARGET" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "baseline 완료"
|
|
}
|
|
|
|
migrate_database() {
|
|
if ! has_schema_migrations_table && has_application_schema; then
|
|
echo "운영 DB에 기존 스키마가 있지만 schema_migrations 적용 이력이 없습니다." >&2
|
|
echo "데이터 보호를 위해 001부터 자동 실행하지 않습니다." >&2
|
|
echo "현재 운영 DB가 최신 상태라면 sh scripts/migrate-production-db.sh baseline 으로 기준점을 먼저 기록하세요." >&2
|
|
echo "상태 확인: sh scripts/migrate-production-db.sh status" >&2
|
|
exit 1
|
|
fi
|
|
|
|
ensure_schema_migrations_table
|
|
|
|
for file_name in $(migration_files); do
|
|
if is_applied "$file_name"; then
|
|
continue
|
|
fi
|
|
|
|
echo "apply $file_name"
|
|
psql_exec -f "$CONTAINER_MIGRATIONS_DIR/$file_name"
|
|
run_sql "INSERT INTO $SCHEMA_MIGRATIONS_TABLE (file_name) VALUES ('$file_name') ON CONFLICT (file_name) DO NOTHING;"
|
|
done
|
|
|
|
echo "마이그레이션 실행 완료"
|
|
}
|
|
|
|
load_env_file
|
|
load_db_env_from_container
|
|
ensure_database_ready
|
|
load_db_env_from_container
|
|
|
|
case "$MODE" in
|
|
status)
|
|
print_status
|
|
;;
|
|
baseline)
|
|
baseline_migrations
|
|
;;
|
|
migrate)
|
|
migrate_database
|
|
;;
|
|
*)
|
|
echo "사용법: sh scripts/migrate-production-db.sh [status|baseline|migrate] [기준번호]" >&2
|
|
exit 1
|
|
;;
|
|
esac
|