"""Application des migrations Alembic au demarrage. Deux situations coexistent et doivent toutes deux fonctionner sans intervention : * **installation neuve** : la base est vide, toutes les revisions s'appliquent ; * **base existante** creee avant Alembic par `create_all` : la table est deja la, mais `alembic_version` n'existe pas. On estampille alors la revision initiale (aucune modification de donnees) avant de derouler la suite. Aucune operation destructive n'est effectuee : la seule migration de cette phase ajoute une colonne nullable. """ from __future__ import annotations import logging from pathlib import Path from alembic import command from alembic.config import Config from sqlalchemy import inspect from app.config import get_settings logger = logging.getLogger(__name__) BACKEND_ROOT = Path(__file__).resolve().parents[2] ALEMBIC_INI = BACKEND_ROOT / "alembic.ini" INITIAL_REVISION = "0001_initial_schema" MAIN_TABLE = "shared_maps" def build_config(database_url: str | None = None, *, configure_logger: bool = False) -> Config: config = Config(str(ALEMBIC_INI)) config.set_main_option("script_location", str(BACKEND_ROOT / "migrations")) config.set_main_option("sqlalchemy.url", database_url or get_settings().database_url) # Les migrations tournent dans le processus applicatif : elles ne doivent pas # reconfigurer les logs deja en place. config.attributes["configure_logger"] = configure_logger return config def run_migrations(engine=None, database_url: str | None = None) -> None: # type: ignore[no-untyped-def] # Import tardif : creer le moteur lit la configuration. En le faisant ici # plutot qu'a l'import du module, une configuration invalide remonte comme # une erreur attrapable, avec un message d'exploitation et sans traceback. from app.db.session import engine as default_engine target = engine or default_engine config = build_config(database_url) with target.connect() as connection: tables = set(inspect(connection).get_table_names()) legacy = MAIN_TABLE in tables and "alembic_version" not in tables if legacy: # Base d'avant Alembic : on declare l'etat initial deja atteint plutot # que de rejouer une creation de table qui echouerait. logger.info("Base existante detectee : estampillage de %s", INITIAL_REVISION) command.stamp(config, INITIAL_REVISION) command.upgrade(config, "head") logger.info("Migrations appliquees (head)") if __name__ == "__main__": # pragma: no cover - appele par l'entrypoint Docker import sys from app.config import ConfigurationError logging.basicConfig(level="INFO", format="%(levelname)-8s | %(message)s", stream=sys.stdout) try: run_migrations() except ConfigurationError as error: # Message d'exploitation, pas de traceback : c'est le fichier .env qu'il # faut corriger, pas le code. print(str(error), file=sys.stderr) raise SystemExit(2) from None