diff --git a/backend/src/app.py b/backend/src/app.py index 3fbb822f..8da8c96b 100755 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -127,12 +127,13 @@ def ensure_initial_admin_user() -> None: db.close() # #endregion ensure_initial_admin_user # #region run_alembic_migrations [C:2] [TYPE Function] -# @BRIEF Applies pending Alembic migrations before application initialization. -# @POST All Alembic migrations up to 'head' are applied to DATABASE_URL. -# @SIDE_EFFECT Executes ALTER TABLE / CREATE TABLE / etc. via Alembic migration chain. -# @RATIONALE Keeps DB schema in sync automatically at container startup — -# eliminates manual `alembic upgrade head` and drift between -# inline _ensure_*() migrations and Alembic migration files. +# @BRIEF Applies all pending Alembic migrations against DATABASE_URL. +# @POST All Alembic migrations up to 'head' are applied. +# @SIDE_EFFECT Executes ALTER TABLE / CREATE TABLE via Alembic chain. +# @RATIONALE Single source of truth for schema changes. All column additions, +# drops, renames, and data migrations go through Alembic. +# init_db() runs after to create any tables that don't have +# a standalone Alembic migration yet. def run_alembic_migrations() -> None: with belief_scope("run_alembic_migrations"): try: @@ -142,29 +143,28 @@ def run_alembic_migrations() -> None: logger.reason("Alembic migrations applied up to head") except Exception as exc: logger.explore( - "Alembic migration failed — app may have schema drift", + "Alembic migration failed — check migration chain or alembic.ini", extra={"error": str(exc)}, ) - # Don't crash — init_db() + _ensure_*() serve as safety net + raise # #endregion run_alembic_migrations # #region startup_event [C:3] [TYPE Function] -# @BRIEF Handles application startup tasks: Alembic migrations, schema init, admin bootstrap, scheduler. +# @BRIEF Handles application startup: Alembic → create_all → admin bootstrap → scheduler. # @RELATION CALLS -> [run_alembic_migrations] # @RELATION CALLS -> [init_db] -# @PRE None. -# @POST Schema is up-to-date (Alembic + inline safety net), admin user exists if configured, scheduler is started. +# @POST Schema is up-to-date via Alembic, missing tables created, admin exists, scheduler started. @app.on_event("startup") async def startup_event(): seed_trace_id() with belief_scope("startup_event"): ensure_encryption_key() - # 1. Alembic — applies complex migrations (add/drop/rename columns, data migrations) + # 1. Alembic — applies ALL pending migrations (add/drop/rename columns, data migrations) run_alembic_migrations() - # 2. init_db — creates missing tables + inline _ensure_*() additive safety net + # 2. init_db — creates new tables via create_all() (does NOT alter existing ones) init_db() ensure_initial_admin_user() scheduler = get_scheduler_service() diff --git a/backend/src/core/database.py b/backend/src/core/database.py index 510fbedd..05284217 100644 --- a/backend/src/core/database.py +++ b/backend/src/core/database.py @@ -760,29 +760,23 @@ def _ensure_dictionary_entries_columns(bind_engine): # #region init_db [C:3] [TYPE Function] -# @BRIEF Initializes the database by creating all tables. +# @BRIEF Creates any missing tables via create_all(). # @PRE engine, tasks_engine and auth_engine are initialized. -# @POST Database tables created in all databases. -# @SIDE_EFFECT Creates physical database files if they don't exist. -# @RELATION CALLS -> [_ensure_filter_source_enum_values] -# @RELATION CALLS -> [_ensure_dataset_review_session_columns] -# @RELATION CALLS -> [_ensure_dictionary_entries_columns] +# Alembic migrations (run_alembic_migrations) have already been applied. +# @POST All tables from SQLAlchemy models exist in all databases. +# @SIDE_EFFECT Creates physical database tables if they don't exist. +# @RATIONALE Schema migrations are handled by Alembic (run_alembic_migrations runs +# before init_db in startup_event). create_all() only creates NEW tables +# that don't exist yet — it does NOT alter existing ones. All column +# additions must go through Alembic migrations. +# @REJECTED _ensure_*() inline additive migrations removed — they were duplicating +# Alembic logic, were not versioned, and created hidden schema drift. +# All schema changes must now go through Alembic. def init_db(): with belief_scope("init_db"): Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=tasks_engine) Base.metadata.create_all(bind=auth_engine) - _ensure_user_dashboard_preferences_columns(engine) - _ensure_llm_validation_results_columns(engine) - _ensure_user_dashboard_preferences_health_columns(engine) - _ensure_git_server_configs_columns(engine) - _ensure_auth_users_columns(auth_engine) - _ensure_roles_is_admin_column(auth_engine) - _ensure_filter_source_enum_values(engine) - _ensure_dataset_review_session_columns(engine) - _ensure_translation_jobs_columns(engine) - _ensure_translation_schedules_columns(engine) - _ensure_dictionary_entries_columns(engine) # #endregion init_db