#!/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_exec() { compose exec -T "$DB_SERVICE" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" "$@" } 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" ] } print_status() { if ! has_schema_migrations_table; then echo "schema_migrations 테이블이 없습니다." echo "기존 운영 DB라면 먼저 sh scripts/migrate-production-db.sh baseline 으로 현재 파일들을 적용 완료로 기록하세요." migration_files | while IFS= read -r file_name; do echo "pending $file_name" done return 0 fi migration_files | while IFS= read -r file_name; do if is_applied "$file_name"; then echo "applied $file_name" else echo "pending $file_name" fi done } baseline_migrations() { ensure_schema_migrations_table found_target=0 migration_files | while IFS= read -r file_name; do if [ -n "$BASELINE_TARGET" ] && [ "$found_target" -eq 1 ]; then continue 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" ] && { [ "$file_name" = "$BASELINE_TARGET" ] || echo "$file_name" | grep -q "^${BASELINE_TARGET}_"; }; then found_target=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 migration_files | while IFS= read -r file_name; 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