zioinfo-mail/workspace/guardia-itsm/cicd/migrate_to_postgres.sh
DESKTOP-TKLFCPR\ython cfe2901a55 refactor(structure): consolidate all projects under workspace/
- itsm/    -> workspace/guardia-itsm/
- manager/ -> workspace/guardia-manager/
- app/     -> workspace/guardia-messenger/
- manual/  -> workspace/guardia-docs/

workspace/zioinfo-web/ unchanged.
git mv preserves full commit history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:50:56 +09:00

167 lines
6.2 KiB
Bash

#!/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 "=================================================="