fix startup

This commit is contained in:
Nicolò Boschi 2025-11-13 14:16:58 +01:00
parent 8f25fc81fd
commit 3f22dfcad4

View file

@ -8,10 +8,14 @@ conflicts when multiple instances start simultaneously.
Important: All migrations must be backward-compatible to allow Important: All migrations must be backward-compatible to allow
safe rolling deployments. safe rolling deployments.
No alembic.ini required - all configuration is done programmatically.
""" """
import logging import logging
import os import os
import shutil
from pathlib import Path from pathlib import Path
from typing import Optional
from alembic import command from alembic import command
from alembic.config import Config from alembic.config import Config
@ -19,9 +23,10 @@ from alembic.config import Config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def run_migrations(database_url: str) -> None:
def run_migrations(database_url: str, script_location: Optional[str] = None) -> None:
""" """
Run database migrations to the latest version. Run database migrations to the latest version using programmatic Alembic configuration.
This function is safe to call on every application startup: This function is safe to call on every application startup:
- Alembic checks the current schema version in the database - Alembic checks the current schema version in the database
@ -29,30 +34,58 @@ def run_migrations(database_url: str) -> None:
- PostgreSQL transactions prevent concurrent migration conflicts - PostgreSQL transactions prevent concurrent migration conflicts
- If schema is already up-to-date, this is a fast no-op - If schema is already up-to-date, this is a fast no-op
Args:
database_url: SQLAlchemy database URL (e.g., "postgresql://user:pass@host/db")
script_location: Path to alembic migrations directory (e.g., "/path/to/alembic").
If None, defaults to memora/alembic directory.
Raises: Raises:
RuntimeError: If migrations fail to complete RuntimeError: If migrations fail to complete
FileNotFoundError: If alembic.ini is not found FileNotFoundError: If script_location doesn't exist
Example:
# Using default location (memora package)
run_migrations("postgresql://user:pass@host/db")
# Using custom location (when importing from another project)
run_migrations(
"postgresql://user:pass@host/db",
script_location="/path/to/copied/_alembic"
)
""" """
try: try:
# Find alembic.ini - it should be at the project root # Determine script location
if script_location is None:
# Default: use the alembic directory in the memora package
# This file is in: memora/memora/migrations.py # This file is in: memora/memora/migrations.py
# Project root is: memora/ # Default location is: memora/alembic
project_root = Path(__file__).parent.parent package_root = Path(__file__).parent.parent
alembic_ini = project_root / "alembic.ini" script_location = str(package_root / "alembic")
if not alembic_ini.exists(): script_path = Path(script_location)
if not script_path.exists():
raise FileNotFoundError( raise FileNotFoundError(
f"alembic.ini not found at {alembic_ini}. " f"Alembic script location not found at {script_location}. "
"Database migrations cannot be run." "Database migrations cannot be run."
) )
logger.info(f"Running database migrations to head...") logger.info(f"Running database migrations to head...")
logger.info(f"Database URL: {database_url}") logger.info(f"Database URL: {database_url}")
logger.info(f"Script location: {script_location}")
# Create Alembic configuration from ini file # Create Alembic configuration programmatically (no alembic.ini needed)
alembic_cfg = Config(str(alembic_ini)) alembic_cfg = Config()
# Set the script location (where alembic versions are stored)
alembic_cfg.set_main_option("script_location", script_location)
# Set the database URL
alembic_cfg.set_main_option("sqlalchemy.url", database_url) alembic_cfg.set_main_option("sqlalchemy.url", database_url)
# Configure logging (optional, but helps with debugging)
# Uses Python's logging system instead of alembic.ini
alembic_cfg.set_main_option("prepend_sys_path", ".")
# Run migrations to head (latest version) # Run migrations to head (latest version)
# Note: Alembic may call sys.exit() on errors instead of raising exceptions # Note: Alembic may call sys.exit() on errors instead of raising exceptions
# We rely on the outer try/except and logging to catch issues # We rely on the outer try/except and logging to catch issues
@ -61,7 +94,7 @@ def run_migrations(database_url: str) -> None:
logger.info("Database migrations completed successfully") logger.info("Database migrations completed successfully")
except FileNotFoundError: except FileNotFoundError:
logger.error("alembic.ini not found, database migrations cannot be run") logger.error(f"Alembic script location not found at {script_location}")
raise raise
except SystemExit as e: except SystemExit as e:
# Catch sys.exit() calls from Alembic # Catch sys.exit() calls from Alembic
@ -72,10 +105,14 @@ def run_migrations(database_url: str) -> None:
raise RuntimeError("Database migration failed") from e raise RuntimeError("Database migration failed") from e
def check_migration_status() -> tuple[str | None, str | None]: def check_migration_status(database_url: Optional[str] = None, script_location: Optional[str] = None) -> tuple[str | None, str | None]:
""" """
Check current database schema version and latest available version. Check current database schema version and latest available version.
Args:
database_url: SQLAlchemy database URL. If None, uses MEMORA_API_DATABASE_URL env var.
script_location: Path to alembic migrations directory. If None, uses default location.
Returns: Returns:
Tuple of (current_revision, head_revision) Tuple of (current_revision, head_revision)
Returns (None, None) if unable to determine versions Returns (None, None) if unable to determine versions
@ -85,9 +122,11 @@ def check_migration_status() -> tuple[str | None, str | None]:
from alembic.script import ScriptDirectory from alembic.script import ScriptDirectory
from sqlalchemy import create_engine from sqlalchemy import create_engine
# Get database URL
if database_url is None:
database_url = os.getenv("MEMORA_API_DATABASE_URL") database_url = os.getenv("MEMORA_API_DATABASE_URL")
if not database_url: if not database_url:
logger.warning("MEMORA_API_DATABASE_URL not set, cannot check migration status") logger.warning("Database URL not provided and MEMORA_API_DATABASE_URL not set, cannot check migration status")
return None, None return None, None
# Get current revision from database # Get current revision from database
@ -97,13 +136,19 @@ def check_migration_status() -> tuple[str | None, str | None]:
current_rev = context.get_current_revision() current_rev = context.get_current_revision()
# Get head revision from migration scripts # Get head revision from migration scripts
project_root = Path(__file__).parent.parent if script_location is None:
alembic_ini = project_root / "alembic.ini" package_root = Path(__file__).parent.parent
script_location = str(package_root / "alembic")
if not alembic_ini.exists(): script_path = Path(script_location)
if not script_path.exists():
logger.warning(f"Script location not found at {script_location}")
return current_rev, None return current_rev, None
alembic_cfg = Config(str(alembic_ini)) # Create config programmatically
alembic_cfg = Config()
alembic_cfg.set_main_option("script_location", script_location)
script = ScriptDirectory.from_config(alembic_cfg) script = ScriptDirectory.from_config(alembic_cfg)
head_rev = script.get_current_head() head_rev = script.get_current_head()