-
Notifications
You must be signed in to change notification settings - Fork 18
Description
Problem
The FastAPI project currently lacks a robust database migration management system. Without proper migration tooling, schema changes are difficult to track, version control, and deploy consistently across environments. This creates risks during database evolution and makes collaboration challenging when multiple developers work on schema changes.
Additional Challenge: The project uses async SQLAlchemy with aiosqlite, which requires special consideration when setting up Alembic to ensure compatibility with the async database engine and session management.
Project Architecture Note: The project has an unconventional but functional structure:
models/player_model.py→ Pydantic models for API validationschemas/player_schema.py→ SQLAlchemy ORM models (actual database tables)
This is opposite to the typical FastAPI convention where "schemas" are Pydantic and "models" are SQLAlchemy, but Alembic will work with the existing structure.
Proposed Solution
Implement Alembic as the database migration tool for the FastAPI project. Alembic will provide:
- Automatic migration generation from SQLAlchemy model changes
- Version-controlled schema changes that can be tracked in Git
- Bidirectional migrations (upgrade/downgrade capabilities)
- Environment-specific configurations for development, staging, and production
- Seamless SQLAlchemy integration leveraging existing ORM models
Why Alembic over Prisma Client?
After evaluation, Alembic is the recommended choice for the following reasons:
Alembic Advantages:
- Native SQLAlchemy integration (built by the same author)
- Pure Python implementation, no additional toolchain required
- Mature ecosystem with extensive community support
- Fine-grained control over migration scripts
- Handles complex migration scenarios (data migrations, custom SQL)
- Works with any SQLAlchemy-supported database
Prisma Client Limitations:
- Requires Node.js runtime alongside Python
- Uses its own schema definition language (Prisma Schema), not SQLAlchemy models
- Would require maintaining two sources of truth for database schema
- Less mature Python support (Prisma primarily TypeScript-focused)
- Additional complexity managing cross-language tooling
Suggested Approach
1. Project Structure
project_root/
├── alembic/
│ ├── versions/ # Migration scripts (to be created)
│ ├── env.py # Migration environment config (to be created)
│ ├── script.py.mako # Migration template (to be created)
│ └── README
├── alembic.ini # Alembic configuration (to be created)
├── databases/
│ ├── __init__.py
│ └── player_database.py # Async SQLAlchemy setup ✓
├── models/
│ ├── __init__.py
│ └── player_model.py # Pydantic models for API ✓
├── schemas/
│ ├── __init__.py
│ └── player_schema.py # SQLAlchemy ORM models ✓
├── storage/
│ └── players-sqlite3.db
├── main.py
└── requirements.txt
Note: The project uses an unconventional naming where "schemas" contains SQLAlchemy ORM and "models" contains Pydantic. This is functional and Alembic will work with it.
2. Installation and Setup
- Add
alembictorequirements.txt - Initialize Alembic in project root:
alembic init alembic - Configure
alembic.iniwith database connection string - Update
alembic/env.pyto import SQLAlchemy models fromschemas/player_schema.py
3. Configuration
- Set up environment variable support for database URLs (using existing
STORAGE_PATHenv var) - Configure
target_metadatainenv.pyto reference SQLAlchemy Base.metadata - Important: Configure Alembic to work with async SQLAlchemy engine using
run_asyncmode - Implement offline and online migration modes
- Add logging configuration for migration tracking
4. Migration Workflow
- Auto-generate migrations:
alembic revision --autogenerate -m "description" - Review generated scripts: Manually verify migration logic
- Apply migrations:
alembic upgrade head - Rollback if needed:
alembic downgrade -1
5. Key Files to Modify/Create
alembic/env.py:
import asyncio
import os
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
# Import your Base and ORM models
from databases.player_database import Base
from schemas.player_schema import Player # Import SQLAlchemy ORM models
# Get database URL from environment
storage_path = os.getenv("STORAGE_PATH", "./storage/players-sqlite3.db")
database_url = f"sqlite+aiosqlite:///{storage_path}"
# Alembic Config object
config = context.config
config.set_main_option("sqlalchemy.url", database_url)
# Set up logging
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Set target metadata for autogenerate
target_metadata = Base.metadata
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode."""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
"""Run migrations in 'online' mode with async engine."""
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()alembic.ini:
- Configure
sqlalchemy.urlto useSTORAGE_PATHenvironment variable - Set up logging levels and output formats
- Note: The URL will be dynamically set in
env.pyfrom the environment variable
schemas/player_schema.py:
- No changes needed - already properly defined with SQLAlchemy columns
- Current implementation includes camelCase column names (e.g.,
name="firstName") - Alembic will detect these column definitions for migration generation
main.py:
- Consider adding startup migration check (optional):
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
"""Lifespan event handler for FastAPI."""
logger.info("Application starting up...")
# Optional: Check if migrations are up to date
# Note: This is informational only, doesn't auto-migrate
# from alembic.config import Config
# from alembic import command
# alembic_cfg = Config("alembic.ini")
# command.check(alembic_cfg)
logger.info("Lifespan event handler execution complete.")
yield
logger.info("Application shutting down...")databases/player_database.py:
- No changes needed - already properly configured with Base export
- Ensure
Baseis imported before running migrations - The async engine configuration is compatible with Alembic's offline mode
6. Integration with FastAPI
- Add migration check on application startup in
main.py(optional) - Document migration workflow in project README
- Update
compose.yamlto run migrations in containerized environments - Set up CI/CD to run migrations automatically or with manual approval
- Ensure
storage/directory is properly configured in.gitignorefor local databases
Acceptance Criteria
- Alembic is installed and initialized in the project
-
alembic.iniis configured with proper database connection settings -
alembic/env.pyis configured to work with async SQLAlchemy and import ORM models fromschemas/player_schema.py - Environment variable
STORAGE_PATHis properly integrated with Alembic configuration - Initial migration is created and successfully applied to development database
- Async compatibility is verified - migrations run successfully with
aiosqlitedriver - CamelCase column names in
player_schema.pyare properly handled in migrations - Documentation is added explaining:
- Project's unconventional naming (schemas=ORM, models=Pydantic)
- How to create new migrations
- How to apply migrations
- How to rollback migrations
- Best practices for reviewing auto-generated migrations
- How async SQLAlchemy works with Alembic
- Migration workflow is tested in local development environment
- Docker configuration in
compose.yamlis updated to run migrations on container startup -
.gitignoreis updated to exclude environment-specific Alembic files if needed - Team is trained on basic Alembic commands and workflow
References
Follow-up Issues
After implementation, consider creating issues for:
- Setting up automated migration testing in CI/CD pipeline
- Implementing migration rollback strategy for production
- Creating migration templates for common patterns (adding indexes, constraints, etc.)