85 lines
3.4 KiB
Python
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}")
|