Files
site_aegisone/max_bot/app/migrations_runner.py
T
angel 5bb9ffd459
Tests / test (push) Has been cancelled
Tests / test-max-bot (push) Has been cancelled
v1.8.3: fix migrations_runner — split SQL on ; for asyncpg compatibility
2026-06-02 19:04:34 +03:00

85 lines
3.4 KiB
Python

"""
Автоматические миграции БД для 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}")