Files
headquarter/apps/api/src/database.py
T
Fusion caf73e39ba fix: use subprocess for alembic migrations to avoid async/sync issues
SQLAlchemy 2.0 async engines conflict with alembic's sync context manager.
Instead of trying to bridge async/sync, use subprocess to run
'alembic upgrade head' directly. This is simpler and more reliable.

- Remove psycopg2-binary dependency (no longer needed)
- Simplify init_database to use subprocess.run()
- Remove all sync engine code
2026-05-18 23:16:01 +02:00

117 lines
3.8 KiB
Python

import asyncio
import logging
import subprocess
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import NullPool
from src.config import Settings, build_database_url
logger = logging.getLogger(__name__)
settings = Settings()
database_url = settings.database_url
# SQLite requires aiosqlite and different connect args
connect_args = {}
if database_url.startswith("sqlite"):
connect_args = {"check_same_thread": False}
engine = create_async_engine(
database_url,
future=True,
poolclass=NullPool,
connect_args=connect_args,
)
SessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def init_database(
max_retries: int = 5,
retry_delay: float = 2.0,
) -> bool:
"""Initialize the database by running pending migrations.
Uses subprocess to run 'alembic upgrade head' to avoid
async/sync context manager issues with SQLAlchemy 2.0.
Returns True if migrations succeeded, False otherwise.
"""
for attempt in range(1, max_retries + 1):
try:
# Test basic connectivity
from sqlalchemy import text
test_conn = await engine.connect()
try:
await test_conn.execute(text("SELECT 1"))
finally:
await test_conn.close()
logger.info("Database connection established.")
# Run migrations via subprocess
logger.info("Running database migrations...")
result = await asyncio.get_event_loop().run_in_executor(
None,
lambda: subprocess.run(
["alembic", "upgrade", "head"],
capture_output=True,
text=True,
cwd="/app",
),
)
if result.returncode == 0:
logger.info("Database migrations completed successfully.")
logger.debug("Alembic output: %s", result.stdout)
return True
else:
logger.error("Migration failed: %s", result.stderr)
if attempt < max_retries:
wait = retry_delay * (2 ** (attempt - 1))
logger.info("Retrying in %.1f seconds...", wait)
await asyncio.sleep(wait)
else:
return False
except Exception as exc:
error_msg = str(exc).lower()
if "connection" in error_msg or "could not connect" in error_msg:
logger.warning(
"Database connection failed (attempt %d/%d): %s",
attempt,
max_retries,
exc,
)
elif "authentication" in error_msg or "password" in error_msg:
logger.error(
"Database authentication failed: %s. "
"Check POSTGRES_USER and POSTGRES_PASSWORD environment variables.",
exc,
)
return False
else:
logger.error(
"Database initialization error (attempt %d/%d): %s",
attempt,
max_retries,
exc,
)
if attempt < max_retries:
wait = retry_delay * (2 ** (attempt - 1))
logger.info("Retrying in %.1f seconds...", wait)
await asyncio.sleep(wait)
else:
logger.error(
"Failed to initialize database after %d attempts. "
"Ensure the database is running and accessible.",
max_retries,
)
return False
return False
__all__ = ["SessionLocal", "build_database_url", "engine", "settings", "init_database"]