#!/bin/bash # migrate_to_postgres.sh — SQLite → PostgreSQL 데이터 마이그레이션 # # 사용법: # bash migrate_to_postgres.sh [sqlite_path] [pg_url] # # 예시: # bash migrate_to_postgres.sh \ # sqlite:///./guardia.db \ # postgresql+asyncpg://guardia:guardia@localhost:5432/guardia # # 전제조건: # - Python 3.11+, pip install alembic asyncpg psycopg2-binary sqlalchemy # - PostgreSQL 서버 실행 중, DB/사용자 사전 생성 # # 절차: # 1. PostgreSQL 스키마 생성 (Alembic 마이그레이션) # 2. SQLite 데이터 덤프 (pg_dump 불가 → Python으로 직접 이관) # 3. 검증 (레코드 수 비교) set -euo pipefail SQLITE_URL="${1:-sqlite:///./guardia.db}" PG_URL="${2:-postgresql+psycopg2://guardia:guardia@localhost:5432/guardia}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ITSM_DIR="$(dirname "$SCRIPT_DIR")" echo "==================================================" echo " GUARDiA PostgreSQL 마이그레이션" echo "==================================================" echo " SQLite: $SQLITE_URL" echo " PostgreSQL: ${PG_URL//:*@/:***@}" # 비밀번호 마스킹 echo "" # ── Step 1: Alembic 스키마 마이그레이션 ───────────────────────────────────── echo "[1/3] PostgreSQL 스키마 생성 중..." cd "$ITSM_DIR" if [ -f "alembic.ini" ]; then # asyncpg를 psycopg2로 변환 (Alembic은 동기 연결 사용) PG_SYNC_URL="${PG_URL/asyncpg/psycopg2}" ALEMBIC_INI_BACKUP="alembic.ini.bak" cp alembic.ini "$ALEMBIC_INI_BACKUP" sed -i "s|sqlalchemy.url = .*|sqlalchemy.url = $PG_SYNC_URL|" alembic.ini python -m alembic upgrade head mv "$ALEMBIC_INI_BACKUP" alembic.ini echo " ✓ Alembic 마이그레이션 완료" else echo " ⚠ alembic.ini를 찾을 수 없습니다. 스키마 생성은 Python으로 대체합니다." python -c " import asyncio, os os.environ['DATABASE_URL'] = '$PG_URL' from database import engine, Base import models # noqa: F401 async def create(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) asyncio.run(create()) print(' ✓ 스키마 생성 완료 (SQLAlchemy)') " fi # ── Step 2: 데이터 이관 ───────────────────────────────────────────────────── echo "[2/3] 데이터 이관 중..." python -c " import asyncio, os, sys SQLITE_URL = '$SQLITE_URL' PG_URL = '$PG_URL' from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import sessionmaker from sqlalchemy import text, inspect # SQLite URL 정규화 (asyncio 드라이버 사용) if SQLITE_URL.startswith('sqlite:///'): SQLITE_ASYNC = SQLITE_URL.replace('sqlite:///', 'sqlite+aiosqlite:///') else: SQLITE_ASYNC = SQLITE_URL src_engine = create_async_engine(SQLITE_ASYNC, echo=False) dst_engine = create_async_engine(PG_URL, echo=False) SrcSession = sessionmaker(src_engine, class_=AsyncSession, expire_on_commit=False) DstSession = sessionmaker(dst_engine, class_=AsyncSession, expire_on_commit=False) SKIP_TABLES = set() async def migrate(): async with src_engine.connect() as src_conn: table_names = await src_conn.run_sync( lambda conn: inspect(conn).get_table_names() ) total_copied = 0 for table in table_names: if table in SKIP_TABLES: continue try: async with src_engine.connect() as src_conn: result = await src_conn.execute(text(f'SELECT * FROM {table}')) rows = result.mappings().all() if not rows: continue async with dst_engine.begin() as dst_conn: # TRUNCATE (재실행 안전성) await dst_conn.execute(text(f'TRUNCATE TABLE {table} RESTART IDENTITY CASCADE')) await dst_conn.execute( text(f'INSERT INTO {table} ({chr(44).join(rows[0].keys())}) VALUES ({chr(44).join([\":\" + k for k in rows[0].keys()])})'), [dict(r) for r in rows], ) total_copied += len(rows) print(f' ✓ {table}: {len(rows)}건 이관') except Exception as e: print(f' ✗ {table} 이관 실패: {e}', file=sys.stderr) print(f' 총 {total_copied}건 이관 완료') asyncio.run(migrate()) " # ── Step 3: 검증 ───────────────────────────────────────────────────────────── echo "[3/3] 데이터 검증 중..." python -c " import asyncio from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy import text, inspect SQLITE_ASYNC = '$SQLITE_URL'.replace('sqlite:///', 'sqlite+aiosqlite:///') PG_URL = '$PG_URL' src_engine = create_async_engine(SQLITE_ASYNC) dst_engine = create_async_engine(PG_URL) async def verify(): ok = True async with src_engine.connect() as src_conn: tables = await src_conn.run_sync(lambda c: inspect(c).get_table_names()) for table in tables: try: async with src_engine.connect() as c: src_cnt = (await c.execute(text(f'SELECT COUNT(*) FROM {table}'))).scalar() async with dst_engine.connect() as c: dst_cnt = (await c.execute(text(f'SELECT COUNT(*) FROM {table}'))).scalar() match = '✓' if src_cnt == dst_cnt else '✗' print(f' {match} {table}: SQLite={src_cnt} PostgreSQL={dst_cnt}') if src_cnt != dst_cnt: ok = False except Exception as e: print(f' - {table}: 검증 스킵 ({e})') if ok: print(' ✅ 검증 통과 — 마이그레이션 성공') else: print(' ❌ 일부 테이블 불일치 — 수동 확인 필요') exit(1) asyncio.run(verify()) " echo "" echo "==================================================" echo " 마이그레이션 완료!" echo " DATABASE_URL 환경변수를 변경하여 PostgreSQL로 전환하세요." echo " 예: export DATABASE_URL='$PG_URL'" echo "=================================================="