feat: [US-001] Add automatic Alembic migrations at backend startup
- Add alembic.ini config for data_entry module - Add migrations.py helper with: - run_migrations(): runs pending migrations at startup (returns True/False) - get_current_revision(): returns current schema version for /health - Integrate migrations in main.py startup_event() BEFORE init_data_entry_db() - Add data_entry_schema_version to /health endpoint If migrations fail, backend continues with WARNING (not crash). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
131
backend/modules/data_entry/db/migrations.py
Normal file
131
backend/modules/data_entry/db/migrations.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
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"
|
||||
Reference in New Issue
Block a user