diff --git a/README.md b/README.md index 0e44a46..75e0d59 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,8 @@ cp .env.example .env # при необходимости включите в .env подтверждение почты # ALABUGA_REQUIRE_EMAIL_CONFIRMATION=true -# применяем миграции -alembic upgrade head - -# создаём демо-данные (команда выполняется из корня репозитория) +# база поднимется сама: при старте приложения автоматически выполняется Alembic upgrade head. +# Для ручной подготовки можно вызвать команду ниже — она прогоняет миграции и добавляет демо-данные. cd .. python -m scripts.seed_data cd backend diff --git a/backend/app/main.py b/backend/app/main.py index 537878f..711a903 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,13 +2,20 @@ from __future__ import annotations +from pathlib import Path + +from alembic import command +from alembic.config import Config +from alembic.script import ScriptDirectory from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import inspect, text from app.api.routes import admin, auth, journal, missions, onboarding, store, users from app.core.config import settings from app.db.session import engine -from app.models.base import Base + +ALEMBIC_CONFIG = Path(__file__).resolve().parents[1] / "alembic.ini" app = FastAPI(title=settings.project_name) @@ -22,11 +29,49 @@ app.add_middleware( ) +def run_migrations() -> None: + """Гарантируем, что база обновлена до последней схемы Alembic.""" + + config = Config(str(ALEMBIC_CONFIG)) + config.set_main_option("sqlalchemy.url", str(settings.database_url)) + inspector = inspect(engine) + script = ScriptDirectory.from_config(config) + head_revision = script.get_current_head() + + tables = inspector.get_table_names() + current_revision: str | None = None + + if "alembic_version" in tables: + with engine.begin() as conn: + row = conn.execute(text("SELECT version_num FROM alembic_version LIMIT 1")).fetchone() + current_revision = row[0] if row else None + + if "alembic_version" not in tables or current_revision is None: + if tables: + # База создана через Base.metadata.create_all. Добавляем отсутствующие поля вручную + # и фиксируем версию как актуальную, чтобы последующие миграции применялись штатно. + user_columns = {col["name"] for col in inspector.get_columns("users")} + with engine.begin() as conn: + if "preferred_branch" not in user_columns: + conn.execute(text("ALTER TABLE users ADD COLUMN preferred_branch VARCHAR(160)")) + if "motivation" not in user_columns: + conn.execute(text("ALTER TABLE users ADD COLUMN motivation TEXT")) + conn.execute(text("CREATE TABLE IF NOT EXISTS alembic_version (version_num VARCHAR(32) NOT NULL)")) + conn.execute(text("DELETE FROM alembic_version")) + conn.execute(text("INSERT INTO alembic_version (version_num) VALUES (:rev)"), {"rev": head_revision}) + return + # Таблиц ещё нет — создадим их миграциями. + command.upgrade(config, "head") + return + + command.upgrade(config, "head") + + @app.on_event("startup") def on_startup() -> None: - """Создаём таблицы, если миграции ещё не применены.""" + """Прогоняем миграции перед обработкой запросов.""" - Base.metadata.create_all(bind=engine) + run_migrations() app.include_router(auth.router) diff --git a/scripts/seed_data.py b/scripts/seed_data.py index 7fe4c55..f22b18e 100644 --- a/scripts/seed_data.py +++ b/scripts/seed_data.py @@ -13,7 +13,7 @@ sys.path.append(str(ROOT / 'backend')) from app.core.config import settings from app.core.security import get_password_hash -from app.db.session import SessionLocal, engine +from app.db.session import SessionLocal from app.models.artifact import Artifact, ArtifactRarity from app.models.branch import Branch, BranchMission from app.models.mission import Mission, MissionCompetencyReward, MissionDifficulty @@ -21,24 +21,18 @@ from app.models.rank import Rank, RankCompetencyRequirement, RankMissionRequirem from app.models.onboarding import OnboardingSlide from app.models.store import StoreItem from app.models.user import Competency, CompetencyCategory, User, UserCompetency, UserRole +from app.main import run_migrations DATA_SENTINEL = settings.sqlite_path.parent / ".seeded" -def ensure_database() -> None: - """Создаём таблицы, если их ещё нет.""" - - from app.models.base import Base - - Base.metadata.create_all(bind=engine) - - def seed() -> None: if DATA_SENTINEL.exists(): print("Database already seeded, skipping") return - ensure_database() + # Перед наполнением БД убеждаемся, что применены все миграции. + run_migrations() session: Session = SessionLocal() try: