#!/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())