fix startup
This commit is contained in:
parent
8f25fc81fd
commit
3f22dfcad4
1 changed files with 65 additions and 20 deletions
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue