""" Alembic migrations helper for Data Entry module. Provides automatic migration execution at backend startup. """ import logging import os from pathlib import Path logger = logging.getLogger(__name__) def run_migrations() -> bool: """ Run pending Alembic migrations at startup. Returns: True if migrations ran successfully (or no pending migrations), False if migrations failed (backend should continue with WARNING). """ try: from alembic.config import Config from alembic import command from alembic.runtime.migration import MigrationContext from sqlalchemy import create_engine # Get the path to alembic.ini data_entry_module = Path(__file__).parent.parent alembic_ini_path = data_entry_module / "alembic.ini" if not alembic_ini_path.exists(): logger.warning(f"[MIGRATIONS] alembic.ini not found at {alembic_ini_path}") return False # Get database path from environment or default db_path = Path(os.getenv( "SQLITE_DATABASE_PATH", "data/receipts/receipts.db" )).resolve() # Ensure database directory exists db_path.parent.mkdir(parents=True, exist_ok=True) # Create Alembic config alembic_cfg = Config(str(alembic_ini_path)) # Override database URL sync_db_url = f"sqlite:///{db_path}" alembic_cfg.set_main_option("sqlalchemy.url", sync_db_url) # Set script location relative to alembic.ini alembic_cfg.set_main_option( "script_location", str(data_entry_module / "migrations") ) # Get current revision before upgrade engine = create_engine(sync_db_url) with engine.connect() as connection: context = MigrationContext.configure(connection) current_rev = context.get_current_revision() engine.dispose() logger.info(f"[MIGRATIONS] Current revision: {current_rev or 'None (fresh database)'}") logger.info(f"[MIGRATIONS] Database path: {db_path}") # Run upgrade to head logger.info("[MIGRATIONS] Checking for pending migrations...") command.upgrade(alembic_cfg, "head") # Get new revision after upgrade engine = create_engine(sync_db_url) with engine.connect() as connection: context = MigrationContext.configure(connection) new_rev = context.get_current_revision() engine.dispose() if current_rev != new_rev: logger.info(f"[MIGRATIONS] Applied: {current_rev or 'None'} -> {new_rev}") else: logger.info(f"[MIGRATIONS] No pending migrations. Current: {new_rev}") return True except ImportError as e: logger.warning(f"[MIGRATIONS] Alembic not installed: {e}") logger.warning("[MIGRATIONS] Skipping migrations - install alembic to enable") return False except Exception as e: logger.error(f"[MIGRATIONS] Migration error: {e}", exc_info=True) logger.warning("[MIGRATIONS] Backend will continue without migrations") return False def get_current_revision() -> str: """ Get the current Alembic revision. Returns: Current revision string, or 'unknown' if cannot be determined. """ try: from alembic.runtime.migration import MigrationContext from sqlalchemy import create_engine # Get database path from environment or default db_path = Path(os.getenv( "SQLITE_DATABASE_PATH", "data/receipts/receipts.db" )).resolve() if not db_path.exists(): return "no_database" sync_db_url = f"sqlite:///{db_path}" engine = create_engine(sync_db_url) with engine.connect() as connection: context = MigrationContext.configure(connection) revision = context.get_current_revision() engine.dispose() return revision or "none" except ImportError: return "alembic_not_installed" except Exception as e: logger.debug(f"[MIGRATIONS] Could not get revision: {e}") return "unknown"