- app/db/models.py: 13 таблиц (ARCHITECTURE §2.10); перечисления — VARCHAR + CHECK, JSON → JSONB на Postgres, BIGINT/INTEGER-ключи для SQLite - миграция 0001 (автогенерация + ручная чистка: импорт sa.Text, дубли CHECK) - миграции применяются при старте бота программно (app.db.migrate), script_location из пакета - SqlStorage: FSM aiogram поверх fsm_state — состояние переживает рестарт (перенесено из #4) - cleanup_loop: истёкшие черновики, снимки контейнеров и попытки входа старше суток - тесты: upgrade/downgrade, дрейф моделей и миграций, CHECK, CASCADE, FSM, чистка Refs #5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
53 lines
1.5 KiB
Python
53 lines
1.5 KiB
Python
"""Apply Alembic migrations programmatically at startup — no separate step in the container.
|
|
|
|
`python -m app.db.migrate [revision]` upgrades the DB from DATABASE_URL (default: head).
|
|
"""
|
|
|
|
import asyncio
|
|
import sys
|
|
|
|
from alembic import command
|
|
from alembic.config import Config
|
|
from sqlalchemy.engine import Connection
|
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
|
|
from app.config import DatabaseSettings
|
|
from app.db.session import make_engine
|
|
|
|
SCRIPT_LOCATION = "app.db:migrations"
|
|
|
|
|
|
def alembic_config(connection: Connection | None = None) -> Config:
|
|
cfg = Config()
|
|
cfg.set_main_option("script_location", SCRIPT_LOCATION)
|
|
# env.py reuses this connection instead of opening its own (async engine, one transaction)
|
|
cfg.attributes["connection"] = connection
|
|
return cfg
|
|
|
|
|
|
async def upgrade(engine: AsyncEngine, revision: str = "head") -> None:
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(lambda sync_conn: command.upgrade(alembic_config(sync_conn), revision))
|
|
|
|
|
|
async def downgrade(engine: AsyncEngine, revision: str) -> None:
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(lambda sync_conn: command.downgrade(alembic_config(sync_conn), revision))
|
|
|
|
|
|
def main() -> None:
|
|
revision = sys.argv[1] if len(sys.argv) > 1 else "head"
|
|
|
|
async def _run() -> None:
|
|
engine = make_engine(DatabaseSettings().url)
|
|
try:
|
|
await upgrade(engine, revision)
|
|
finally:
|
|
await engine.dispose()
|
|
|
|
asyncio.run(_run())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|