- 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>
167 lines
6.2 KiB
Bash
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 "=================================================="
|