""" Автоматические миграции БД для max_bot. При старте приложения: 1. Создаёт таблицу _applied_migrations (если не существует) 2. Проверяет какие SQL-файлы уже применены 3. Применяет новые миграции по порядку ВАЖНО: asyncpg не поддерживает несколько SQL-команд в одном text(). Поэтому каждый SQL-файл разбивается на отдельные команды по ';' . """ import os import re import logging from sqlalchemy import text from app.database import engine logger = logging.getLogger(__name__) MIGRATIONS_DIR = os.path.join(os.path.dirname(__file__), "migrations") def _split_sql(sql: str) -> list: """Разбивает SQL-файл на отдельные команды, пропуская комментарии и пустые строки.""" statements = [] for stmt in sql.split(";"): # Убираем комментарии (-- ...) и пустые строки lines = [] for line in stmt.split("\n"): stripped = line.strip() if stripped.startswith("--"): continue if stripped: lines.append(stripped) clean = " ".join(lines) if clean: statements.append(clean) return statements async def run_migrations(): """Запуск всех неприменённых миграций из директории migrations/.""" async with engine.begin() as conn: # Создаём таблицу для отслеживания миграций await conn.execute(text(""" CREATE TABLE IF NOT EXISTS _applied_migrations ( id SERIAL PRIMARY KEY, filename VARCHAR(255) UNIQUE NOT NULL, applied_at TIMESTAMP DEFAULT NOW() ) """)) # Получаем список уже применённых миграций result = await conn.execute(text("SELECT filename FROM _applied_migrations")) applied = {row[0] for row in result.all()} # Сканируем директорию миграций if not os.path.isdir(MIGRATIONS_DIR): logger.info("Директория migrations/ не найдена, пропускаем") return sql_files = sorted(f for f in os.listdir(MIGRATIONS_DIR) if f.endswith(".sql")) for filename in sql_files: if filename in applied: continue filepath = os.path.join(MIGRATIONS_DIR, filename) with open(filepath, "r", encoding="utf-8") as f: sql = f.read() statements = _split_sql(sql) logger.info(f"Применение миграции: {filename} ({len(statements)} команд)") try: for stmt in statements: await conn.execute(text(stmt)) await conn.execute( text("INSERT INTO _applied_migrations (filename) VALUES (:fn)"), {"fn": filename} ) logger.info(f"Миграция применена: {filename}") except Exception as e: logger.warning(f"Миграция {filename} уже применена или ошибка: {e}")