Merge pull request 'refactor: Separate async migration logic into dedicated module and streamline migration functions for improved clarity and maintainability' (#29) from ph4 into prod

Reviewed-on: #29
This commit is contained in:
mo 2025-06-01 17:33:14 +02:00
commit f882b86f05
3 changed files with 90 additions and 51 deletions

View File

@ -1,7 +1,6 @@
from logging.config import fileConfig from logging.config import fileConfig
import os import os
import sys import sys
import asyncio
from sqlalchemy import engine_from_config from sqlalchemy import engine_from_config
from sqlalchemy import pool from sqlalchemy import pool
from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.ext.asyncio import create_async_engine
@ -16,8 +15,7 @@ import app.models # Ensure all models are loaded and registered to app.database
from app.database import Base as DatabaseBase # Explicitly get Base from database.py from app.database import Base as DatabaseBase # Explicitly get Base from database.py
from app.config import settings # Import settings to get DATABASE_URL from app.config import settings # Import settings to get DATABASE_URL
# this is the Alembic Config object, which provides # Get alembic config
# access to the values within the .ini file in use.
config = context.config config = context.config
# Set the sqlalchemy.url from your application settings # Set the sqlalchemy.url from your application settings
@ -26,7 +24,6 @@ if not settings.DATABASE_URL:
raise ValueError("DATABASE_URL not found in settings for Alembic.") raise ValueError("DATABASE_URL not found in settings for Alembic.")
config.set_main_option('sqlalchemy.url', settings.DATABASE_URL) config.set_main_option('sqlalchemy.url', settings.DATABASE_URL)
# Interpret the config file for Python logging. # Interpret the config file for Python logging.
# This line sets up loggers basically. # This line sets up loggers basically.
if config.config_file_name is not None: if config.config_file_name is not None:
@ -43,50 +40,11 @@ target_metadata = DatabaseBase.metadata # Use metadata from app.database.Base
# my_important_option = config.get_main_option("my_important_option") # my_important_option = config.get_main_option("my_important_option")
# ... etc. # ... etc.
def do_run_migrations(connection): def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode."""
url = config.get_main_option("sqlalchemy.url")
context.configure( context.configure(
connection=connection, url=url,
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True
)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online_async(alembic_cfg=None):
"""Run migrations in 'online' mode asynchronously."""
# If running from Alembic directly
if alembic_cfg:
if alembic_cfg.config_file_name is not None:
fileConfig(alembic_cfg.config_file_name)
connectable = create_async_engine(
settings.DATABASE_URL,
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
if not settings.DATABASE_URL:
raise ValueError("DATABASE_URL not found in settings for Alembic.")
context.configure(
url=settings.DATABASE_URL,
target_metadata=target_metadata, target_metadata=target_metadata,
literal_binds=True, literal_binds=True,
dialect_opts={"paramstyle": "named"}, dialect_opts={"paramstyle": "named"},
@ -95,8 +53,32 @@ def run_migrations_offline():
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()
async def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
connectable = create_async_engine(
settings.DATABASE_URL,
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(_run_migrations)
await connectable.dispose()
def _run_migrations(connection):
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True
)
with context.begin_transaction():
context.run_migrations()
# This section only runs when executing alembic commands directly (not when imported) # This section only runs when executing alembic commands directly (not when imported)
if context.is_offline_mode(): if context.is_offline_mode():
run_migrations_offline() run_migrations_offline()
elif 'ALEMBIC_CONFIG' in os.environ: # Only run if called from alembic command else:
asyncio.run(run_migrations_online_async(context.config)) import asyncio
asyncio.run(run_migrations_online())

57
be/alembic/migrations.py Normal file
View File

@ -0,0 +1,57 @@
"""
Async migrations handler for FastAPI application.
This file is separate from env.py to avoid Alembic context issues.
"""
import os
import sys
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import pool
# Ensure the app directory is in the Python path
sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
from app.database import Base as DatabaseBase
from app.config import settings
async def run_migrations():
"""Run database migrations asynchronously."""
from alembic.runtime.migration import MigrationContext
from alembic.operations import Operations
# Create async engine
engine = create_async_engine(
settings.DATABASE_URL,
poolclass=pool.NullPool,
)
async with engine.connect() as connection:
# Get current database schema version
def do_get_current_rev():
migration_context = MigrationContext.configure(
connection,
opts={
'target_metadata': DatabaseBase.metadata,
'compare_type': True,
'compare_server_default': True
}
)
return migration_context.get_current_revision()
current_rev = await connection.run_sync(do_get_current_rev)
# Run migrations
def do_upgrade():
migration_context = MigrationContext.configure(
connection,
opts={
'target_metadata': DatabaseBase.metadata,
'compare_type': True,
'compare_server_default': True
}
)
with Operations.context(migration_context):
migration_context.run_migrations()
await connection.run_sync(do_upgrade)
await engine.dispose()

View File

@ -228,8 +228,8 @@ async def run_migrations():
sys.path.insert(0, alembic_path) sys.path.insert(0, alembic_path)
# Import and run migrations # Import and run migrations
from env import run_migrations_online_async from migrations import run_migrations as run_db_migrations
await run_migrations_online_async() await run_db_migrations()
logger.info("Database migrations completed successfully.") logger.info("Database migrations completed successfully.")
except Exception as e: except Exception as e: