# Database Migrations This guide covers database migrations using Alembic, the migration tool for SQLAlchemy. Learn how to manage database schema changes safely and efficiently in development and production. ## Overview The FastAPI Boilerplate uses [Alembic](https://alembic.sqlalchemy.org/) for database migrations. Alembic provides: - **Version-controlled schema changes** - Track every database modification - **Automatic migration generation** - Generate migrations from model changes - **Reversible migrations** - Upgrade and downgrade database versions - **Environment-specific configurations** - Different settings for dev/staging/production - **Safe schema evolution** - Apply changes incrementally ## Simple Setup: Automatic Table Creation For simple projects or development, the boilerplate includes `create_tables_on_start` parameter that automatically creates all tables on application startup: ```python # This is enabled by default in create_application() app = create_application( router=router, settings=settings, create_tables_on_start=True # Default: True ) ``` **When to use:** - ✅ **Development** - Quick setup without migration management - ✅ **Simple projects** - When you don't need migration history - ✅ **Prototyping** - Fast iteration without migration complexity - ✅ **Testing** - Clean database state for each test run **When NOT to use:** - ❌ **Production** - No migration history or rollback capability - ❌ **Team development** - Can't track schema changes between developers - ❌ **Data migrations** - Only handles schema, not data transformations - ❌ **Complex deployments** - No control over when/how schema changes apply ```python # Disable for production environments app = create_application( router=router, settings=settings, create_tables_on_start=False # Use migrations instead ) ``` For production deployments and team development, use proper Alembic migrations as described below. ## Configuration ### Alembic Setup Alembic is configured in `src/alembic.ini`: ```ini [alembic] # Path to migration files script_location = migrations # Database URL with environment variable substitution sqlalchemy.url = postgresql://%(POSTGRES_USER)s:%(POSTGRES_PASSWORD)s@%(POSTGRES_SERVER)s:%(POSTGRES_PORT)s/%(POSTGRES_DB)s # Other configurations file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(rev)s_%%(slug)s timezone = UTC ``` ### Environment Configuration Migration environment is configured in `src/migrations/env.py`: ```python # src/migrations/env.py from alembic import context from sqlalchemy import engine_from_config, pool from app.core.db.database import Base from app.core.config import settings # Import all models to ensure they're registered from app.models import * # This imports all models config = context.config # Override database URL from environment config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) target_metadata = Base.metadata ``` ## Migration Workflow ### 1. Creating Migrations Generate migrations automatically when you change models: ```bash # Navigate to src directory cd src # Generate migration from model changes uv run alembic revision --autogenerate -m "Add user profile fields" ``` **What happens:** - Alembic compares current models with database schema - Generates a new migration file in `src/migrations/versions/` - Migration includes upgrade and downgrade functions ### 2. Review Generated Migration Always review auto-generated migrations before applying: ```python # Example migration file: src/migrations/versions/20241215_1430_add_user_profile_fields.py """Add user profile fields Revision ID: abc123def456 Revises: previous_revision_id Create Date: 2024-12-15 14:30:00.000000 """ from alembic import op import sqlalchemy as sa # revision identifiers revision = 'abc123def456' down_revision = 'previous_revision_id' branch_labels = None depends_on = None def upgrade() -> None: # Add new columns op.add_column('user', sa.Column('bio', sa.String(500), nullable=True)) op.add_column('user', sa.Column('website', sa.String(255), nullable=True)) # Create index op.create_index('ix_user_website', 'user', ['website']) def downgrade() -> None: # Remove changes (reverse order) op.drop_index('ix_user_website', 'user') op.drop_column('user', 'website') op.drop_column('user', 'bio') ``` ### 3. Apply Migration Apply migrations to update database schema: ```bash # Apply all pending migrations uv run alembic upgrade head # Apply specific number of migrations uv run alembic upgrade +2 # Apply to specific revision uv run alembic upgrade abc123def456 ``` ### 4. Verify Migration Check migration status and current version: ```bash # Show current database version uv run alembic current # Show migration history uv run alembic history # Show pending migrations uv run alembic show head ``` ## Common Migration Scenarios ### Adding New Model 1. **Create the model** in `src/app/models/`: ```python # src/app/models/category.py from sqlalchemy import String, DateTime from sqlalchemy.orm import Mapped, mapped_column from datetime import datetime from app.core.db.database import Base class Category(Base): __tablename__ = "category" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True, init=False) name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) slug: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) description: Mapped[str] = mapped_column(String(255), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) ``` 2. **Import in __init__.py**: ```python # src/app/models/__init__.py from .user import User from .post import Post from .tier import Tier from .rate_limit import RateLimit from .category import Category # Add new import ``` 3. **Generate migration**: ```bash uv run alembic revision --autogenerate -m "Add category model" ``` ### Adding Foreign Key 1. **Update model with foreign key**: ```python # Add to Post model category_id: Mapped[Optional[int]] = mapped_column(ForeignKey("category.id"), nullable=True) ``` 2. **Generate migration**: ```bash uv run alembic revision --autogenerate -m "Add category_id to posts" ``` 3. **Review and apply**: ```python # Generated migration will include: def upgrade() -> None: op.add_column('post', sa.Column('category_id', sa.Integer(), nullable=True)) op.create_foreign_key('fk_post_category_id', 'post', 'category', ['category_id'], ['id']) op.create_index('ix_post_category_id', 'post', ['category_id']) ``` ### Data Migrations Sometimes you need to migrate data, not just schema: ```python # Example: Populate default category for existing posts def upgrade() -> None: # Add the column op.add_column('post', sa.Column('category_id', sa.Integer(), nullable=True)) # Data migration connection = op.get_bind() # Create default category connection.execute( "INSERT INTO category (name, slug, description) VALUES ('General', 'general', 'Default category')" ) # Get default category ID result = connection.execute("SELECT id FROM category WHERE slug = 'general'") default_category_id = result.fetchone()[0] # Update existing posts connection.execute( f"UPDATE post SET category_id = {default_category_id} WHERE category_id IS NULL" ) # Make column non-nullable after data migration op.alter_column('post', 'category_id', nullable=False) ``` ### Renaming Columns ```python def upgrade() -> None: # Rename column op.alter_column('user', 'full_name', new_column_name='name') def downgrade() -> None: # Reverse the rename op.alter_column('user', 'name', new_column_name='full_name') ``` ### Dropping Tables ```python def upgrade() -> None: # Drop table (be careful!) op.drop_table('old_table') def downgrade() -> None: # Recreate table structure op.create_table('old_table', sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(50), nullable=True), sa.PrimaryKeyConstraint('id') ) ``` ## Production Migration Strategy ### 1. Development Workflow ```bash # 1. Make model changes # 2. Generate migration uv run alembic revision --autogenerate -m "Descriptive message" # 3. Review migration file # 4. Test migration uv run alembic upgrade head # 5. Test downgrade (optional) uv run alembic downgrade -1 uv run alembic upgrade head ``` ### 2. Staging Deployment ```bash # 1. Deploy code with migrations # 2. Backup database pg_dump -h staging-db -U user dbname > backup_$(date +%Y%m%d_%H%M%S).sql # 3. Apply migrations uv run alembic upgrade head # 4. Verify application works # 5. Run tests ``` ### 3. Production Deployment ```bash # 1. Schedule maintenance window # 2. Create database backup pg_dump -h prod-db -U user dbname > prod_backup_$(date +%Y%m%d_%H%M%S).sql # 3. Apply migrations (with monitoring) uv run alembic upgrade head # 4. Verify health checks pass # 5. Monitor application metrics ``` ## Docker Considerations ### Development with Docker Compose For local development, migrations run automatically: ```yaml # docker-compose.yml services: web: # ... other config depends_on: - db command: | sh -c " uv run alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload " ``` ### Production Docker In production, run migrations separately: ```dockerfile # Dockerfile migration stage FROM python:3.11-slim as migration COPY requirements.txt . RUN pip install -r requirements.txt COPY src/ /app/ WORKDIR /app CMD ["alembic", "upgrade", "head"] ``` ```yaml # docker-compose.prod.yml services: migrate: build: context: . target: migration env_file: - .env depends_on: - db command: alembic upgrade head web: # ... web service config depends_on: - migrate ``` ## Migration Best Practices ### 1. Always Review Generated Migrations ```python # Check for issues like: # - Missing imports # - Incorrect nullable settings # - Missing indexes # - Data loss operations ``` ### 2. Use Descriptive Messages ```bash # Good uv run alembic revision --autogenerate -m "Add user email verification fields" # Bad uv run alembic revision --autogenerate -m "Update user model" ``` ### 3. Handle Nullable Columns Carefully ```python # When adding non-nullable columns to existing tables: def upgrade() -> None: # 1. Add as nullable first op.add_column('user', sa.Column('phone', sa.String(20), nullable=True)) # 2. Populate with default data op.execute("UPDATE user SET phone = '' WHERE phone IS NULL") # 3. Make non-nullable op.alter_column('user', 'phone', nullable=False) ``` ### 4. Test Rollbacks ```bash # Test that your downgrade works uv run alembic downgrade -1 uv run alembic upgrade head ``` ### 5. Use Transactions for Complex Migrations ```python def upgrade() -> None: # Complex migration with transaction connection = op.get_bind() trans = connection.begin() try: # Multiple operations op.create_table(...) op.add_column(...) connection.execute("UPDATE ...") trans.commit() except: trans.rollback() raise ``` ## Next Steps - **[CRUD Operations](crud.md)** - Working with migrated database schema - **[API Development](../api/index.md)** - Building endpoints for your models - **[Testing](../testing.md)** - Testing database migrations