Files
2026-05-17 05:22:06 +03:00

181 lines
6.5 KiB
Python

#!/usr/bin/env python3
"""
MySQL → PostgreSQL Data Migration Script
Reads from MySQL, transforms data, writes to PostgreSQL.
Handles ENUM→VARCHAR, TINYINT→BOOLEAN, JSON→JSONB, AUTO_INCREMENT→SERIAL.
"""
import mysql.connector
import asyncpg
import asyncio
import json
import os
from dotenv import load_dotenv
load_dotenv()
MYSQL_CONFIG = {
"host": os.getenv("MYSQL_HOST", "localhost"),
"port": int(os.getenv("MYSQL_PORT", "3306")),
"user": os.getenv("MYSQL_USER", "root"),
"password": os.getenv("MYSQL_PASSWORD", ""),
"database": os.getenv("MYSQL_DATABASE", "aegisone"),
}
PG_DSN = os.getenv("PG_DSN", "postgresql://aegisone:aegisone_pass@localhost:5432/aegisone")
TABLES = [
"users", "customers", "objects", "object_assignments",
"tasks", "task_comments", "task_photos", "reports",
"questionnaire_sessions", "questionnaire_answers", "questionnaire_items",
"object_passports", "sla_contracts", "incidents",
"engineer_kpi", "shs_records", "blog_posts",
"formula_coefficients", "cases", "login_attempts", "audit_log",
]
ENUM_MAP = {
"users.role": {"owner": "owner", "engineer": "engineer", "technician": "technician"},
"objects.status": {"active": "active", "inactive": "inactive", "audit": "audit", "prospective": "prospective"},
"tasks.priority": {"P1": "P1", "P2": "P2", "P3": "P3", "P4": "P4"},
"tasks.status": {"open": "open", "in_progress": "in_progress", "completed": "completed", "cancelled": "cancelled"},
"reports.report_type": {"inspection": "inspection", "service": "service", "emergency": "emergency", "audit": "audit", "monthly": "monthly"},
"reports.result_status": {"fixed": "fixed", "partial": "partial", "revisit": "revisit", "ok": "ok"},
"reports.status": {"draft": "draft", "final": "final", "cancelled": "cancelled"},
"questionnaire_sessions.status": {"draft": "draft", "completed": "completed", "cancelled": "cancelled"},
"questionnaire_items.type": {"text": "text", "number": "number", "select": "select", "radio": "radio", "checkbox": "checkbox", "textarea": "textarea"},
"sla_contracts.service_level": {"start": "start", "business": "business", "enterprise": "enterprise"},
"sla_contracts.status": {"active": "active", "expired": "expired", "cancelled": "cancelled", "negotiation": "negotiation"},
"incidents.severity": {"P1": "P1", "P2": "P2", "P3": "P3"},
"incidents.status": {"open": "open", "in_progress": "in_progress", "resolved": "resolved", "closed": "closed"},
"blog_posts.category": {"audit": "audit", "sla": "sla", "incident": "incident", "supervision": "supervision", "documentation": "documentation", "risk": "risk", "cases": "cases"},
"blog_posts.status": {"draft": "draft", "published": "published", "archived": "archived"},
"object_assignments.role": {"engineer": "engineer", "technician": "technician"},
"customers.status": {"active": "active", "inactive": "inactive", "prospect": "prospect"},
"task_photos.type": {"before": "before", "after": "after", "evidence": "evidence", "other": "other"},
}
BOOL_COLUMNS = {
"users": ["is_active"],
"tasks": ["sla_compliant"],
"reports": ["client_confirmed"],
"questionnaire_items": ["required", "is_active"],
"object_passports": [],
"sla_contracts": [],
"incidents": ["sla_breached"],
"engineer_kpi": [],
"shs_records": [],
"blog_posts": [],
"formula_coefficients": ["is_active"],
"cases": ["is_active"],
"customers": [],
"objects": [],
"object_assignments": [],
"task_comments": [],
"task_photos": [],
"questionnaire_sessions": [],
"questionnaire_answers": [],
"login_attempts": [],
"audit_log": [],
}
SKIP_COLUMNS = {
"users": [],
"customers": [],
"objects": [],
"object_assignments": [],
"tasks": [],
"task_comments": [],
"task_photos": [],
"reports": [],
"questionnaire_sessions": [],
"questionnaire_answers": [],
"questionnaire_items": [],
"object_passports": [],
"sla_contracts": [],
"incidents": [],
"engineer_kpi": [],
"shs_records": [],
"blog_posts": [],
"formula_coefficients": [],
"cases": [],
"login_attempts": [],
"audit_log": [],
}
def transform_row(table: str, row: dict) -> dict:
result = {}
for key, value in row.items():
if key in SKIP_COLUMNS.get(table, []):
continue
if value is None:
result[key] = None
continue
enum_key = f"{table}.{key}"
if enum_key in ENUM_MAP:
result[key] = ENUM_MAP[enum_key].get(value, value)
elif table in BOOL_COLUMNS and key in BOOL_COLUMNS[table]:
result[key] = bool(value)
elif isinstance(value, (bytes, bytearray)):
result[key] = value.decode("utf-8")
elif isinstance(value, dict) or isinstance(value, list):
result[key] = json.dumps(value, ensure_ascii=False)
else:
result[key] = value
return result
async def migrate_table(mysql_conn, pg_conn, table: str):
cursor = mysql_conn.cursor(dictionary=True)
cursor.execute(f"SELECT * FROM {table}")
rows = cursor.fetchall()
if not rows:
print(f" {table}: нет данных")
return
columns = list(rows[0].keys())
placeholders = ", ".join([f"${i+1}" for i in range(len(columns))])
col_names = ", ".join(columns)
inserted = 0
errors = 0
for row in rows:
try:
transformed = transform_row(table, row)
values = [transformed.get(c) for c in columns]
await pg_conn.execute(
f"INSERT INTO {table} ({col_names}) VALUES ({placeholders}) ON CONFLICT DO NOTHING",
*values,
)
inserted += 1
except Exception as e:
errors += 1
print(f" Ошибка в {table}: {e}")
print(f" {table}: {inserted} записей, {errors} ошибок")
async def main():
print("Подключение к MySQL...")
mysql_conn = mysql.connector.connect(**MYSQL_CONFIG)
print("Подключение к PostgreSQL...")
pg_conn = await asyncpg.connect(PG_DSN)
total_tables = len(TABLES)
for i, table in enumerate(TABLES, 1):
print(f"[{i}/{total_tables}] Миграция {table}...")
try:
await migrate_table(mysql_conn, pg_conn, table)
except Exception as e:
print(f" Ошибка миграции {table}: {e}")
await pg_conn.close()
mysql_conn.close()
print("\nМиграция завершена.")
if __name__ == "__main__":
asyncio.run(main())