Files
mtgonline/backend/app/core/database.py
T
akadmin b7056fe0d9 Fix SQLite autoincrement and test configuration
- Removed autoincrement=True from BigInteger primary keys (UserDeck, UserDeckCard, DeckPrecedent, etc.)
- SQLite doesn't support autoincrement on BigInteger columns
- Updated conftest.py test engine configuration
- All admin tests passing (5/5)
- All auth tests passing (7/7)
- Game model tests passing (13/13)
- Deck tests still failing due to SQLite RETURNING clause issue
2026-08-28 03:43:14 +00:00

198 lines
5.6 KiB
Python

"""
Database engine and session management.
Provides a single SQLAlchemy async engine supporting both SQLite (development)
and PostgreSQL (production) via a single DATABASE_URL configuration value.
## Usage
from app.core.database import get_db, create_tables, Base
# In FastAPI dependency:
async def endpoint(db: AsyncSession = Depends(get_db)):
...
# Create all tables:
await create_tables()
## Architecture
- Single engine for all database operations (no dual databases)
- Sync DeclarativeBase for model definitions
- Async engine and sessions for async operations
- Automatic dialect detection (SQLite vs PostgreSQL)
## Backward Compatibility
The following exports are provided for backward compatibility with code that
still references the old dual-database naming convention:
- ``mtg_engine`` → alias for ``engine``
- ``mtg_async_session`` → alias for ``async_session``
- ``mtg_get_db`` → alias for ``get_db``
- ``mirror_get_db`` → alias for ``get_db``
These should be removed as callers are migrated to the new unified API.
"""
import logging
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from sqlalchemy.orm import DeclarativeBase
from app.core.config import get_config
logger = logging.getLogger(__name__)
config = get_config()
# ---------------------------------------------------------------------------
# Engine setup — single engine, auto-detect SQLite vs PostgreSQL
# ---------------------------------------------------------------------------
engine = create_async_engine(
config.DATABASE_URL,
echo=config.DEBUG,
# SQLite doesn't support pool_size / max_overflow — only apply for PG
**(
{
"pool_pre_ping": True,
"pool_size": 20,
"max_overflow": 10,
}
if not config.is_sqlite
else {
"pool_pre_ping": False,
# Disable insertmanyvalues to avoid RETURNING clause issues with aiosqlite
"execution_options": {
"insertmanyvalues_page_size": 0,
},
}
),
)
async_session = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
# ---------------------------------------------------------------------------
# Base class for ORM models
# ---------------------------------------------------------------------------
class Base(DeclarativeBase):
"""Base class for all ORM models.
Uses sync DeclarativeBase (not async-specific) for simplicity.
All models inherit from this class.
"""
pass
# ---------------------------------------------------------------------------
# FastAPI dependency — provides a database session
# ---------------------------------------------------------------------------
async def get_db() -> AsyncGenerator[AsyncSession, None]:
"""FastAPI dependency that provides a database session.
Yields an async session and handles commit/rollback/close automatically.
Usage in FastAPI endpoints:
from fastapi import Depends
from app.core.database import get_db
@app.get("/items")
async def list_items(db: AsyncSession = Depends(get_db)):
...
Yields:
An async SQLAlchemy session.
"""
async with async_session() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
# ---------------------------------------------------------------------------
# Table creation helper
# ---------------------------------------------------------------------------
async def create_tables() -> None:
"""Create all database tables from model metadata.
Imports all model modules to ensure Base.metadata is populated,
then creates all tables. This is intended for development and testing.
In production, use Alembic migrations instead.
Example:
>>> await create_tables()
"""
# Import all models to register them with Base.metadata
import app.models # noqa: F401
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info(
f"Database tables created successfully "
f"(dialect: {'SQLite' if config.is_sqlite else 'PostgreSQL'})"
)
# ---------------------------------------------------------------------------
# Backward-compatible aliases
#
# The old codebase referenced dual databases (mtg_engine, mtg_async_session,
# mtg_get_db, mirror_get_db). These aliases map them to the single unified
# engine so existing imports continue to work. They will be removed once all
# callers have been migrated.
# ---------------------------------------------------------------------------
# Alias: mtg_engine → engine (was the "primary" database engine)
mtg_engine = engine
# Alias: mtg_async_session → async_session (was the "primary" session factory)
mtg_async_session = async_session
# Alias: mtg_get_db → get_db (was the "primary" session dependency)
mtg_get_db = get_db
# Alias: mirror_get_db → get_db (was the "mirror" database session dependency)
mirror_get_db = get_db
# ---------------------------------------------------------------------------
# Exports
# ---------------------------------------------------------------------------
__all__ = [
"Base",
"engine",
"async_session",
"get_db",
"create_tables",
# Backward-compatible aliases
"mtg_engine",
"mtg_async_session",
"mtg_get_db",
"mirror_get_db",
]