Database Migrations with Alembic¶
This project uses Alembic for database schema migrations. Alembic provides version control for your database schema, allowing safe evolution of the database structure over time.
Overview¶
Previous approach: The project previously used SQLModel.metadata.create_all() which had limitations: - ❌ No migration history or rollback capability - ❌ Cannot safely modify existing tables - ❌ Risk of data loss when changing schemas - ❌ No team synchronization for schema changes
Current approach: Alembic migrations provide: - ✅ Version-controlled migration history - ✅ Safe schema evolution without data loss - ✅ Rollback support for failed migrations - ✅ Automatic migration generation from model changes - ✅ Team synchronization (everyone applies same migrations) - ✅ Production-safe deployment workflow
Quick Start¶
Applying Migrations¶
Apply all pending migrations to bring your database up to date:
This runs alembic upgrade head and applies all migrations that haven't been applied yet.
Creating New Migrations¶
When you modify a SQLModel (add/remove/modify fields), generate a new migration:
This will: 1. Auto-detect changes between your models and current database schema 2. Generate a new migration file in app/storage/migrations/versions/ 3. Create both upgrade() and downgrade() functions
Important: Always review the generated migration before applying it!
Viewing Migration Status¶
Check which migration is currently applied:
View full migration history:
Rolling Back Migrations¶
Roll back the most recent migration:
This runs alembic downgrade -1 to revert the last migration.
Migration Workflow¶
1. Modify Your Model¶
Edit a model file (e.g., app/models/author.py):
class Author(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
email: str | None = None # NEW FIELD
2. Generate Migration¶
3. Review Generated Migration¶
Check the generated file in app/storage/migrations/versions/:
def upgrade() -> None:
# ### commands auto generated by Alembic ###
op.add_column('author', sa.Column('email', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic ###
op.drop_column('author', 'email')
# ### end Alembic commands ###
Important checks: - Verify the operations match your intent - Check for data loss risks (e.g., dropping columns) - Ensure nullable constraints are correct - Review default values for new required fields
4. Apply Migration¶
5. If Issues Occur, Rollback¶
Advanced Usage¶
Manual Migration Creation¶
For complex migrations that can't be auto-generated:
Then manually edit the generated file to add your custom operations.
Downgrade to Specific Version¶
Upgrade to Specific Version¶
Stamp Database Without Running Migrations¶
If you have an existing database that matches a specific migration:
Or stamp to the latest:
This is useful when: - Migrating from the old create_all() approach to Alembic - Setting up a database that was manually created - Recovering from migration issues
Initial Setup (For New Developers)¶
- Clone the repository
- Start services:
- Apply migrations:
That's it! Your database schema is now up to date.
Production Deployment¶
Safe Deployment Process¶
-
Test migrations locally:
-
Review migration on staging environment
-
Backup production database before applying migrations
-
Apply migrations during maintenance window:
-
Verify application functionality
-
If issues occur:
Zero-Downtime Migrations¶
For zero-downtime deployments, follow this pattern:
Phase 1: Add new column (nullable)
Phase 2: Backfill data
# Migration 2: Populate email field
def upgrade():
# Custom data migration
op.execute("UPDATE author SET email = name || '@example.com'")
Phase 3: Make non-nullable (if needed)
# Migration 3: Add NOT NULL constraint
def upgrade():
op.alter_column('author', 'email', nullable=False)
Troubleshooting¶
"Target database is not up to date"¶
"Can't locate revision identified by 'xyz'"¶
The migration file might have been deleted. Check app/storage/migrations/versions/ directory.
"FAILED: Multiple head revisions are present"¶
This happens when multiple branches exist in migration history. Merge them:
Migration Conflicts¶
If you get conflicts because someone else created a migration:
- Pull latest code
- Regenerate your migration:
Reset Migration History (Development Only!)¶
WARNING: This will delete all migration history. Never do this in production!
# Drop all tables
# Recreate database
# Regenerate initial migration
make migration msg="Initial migration"
make migrate
Configuration¶
Database Connection¶
Alembic automatically uses the database URL from app/settings.py:
This is configured in app/storage/migrations/env.py:
Adding New Models¶
When you create a new model, import it in app/storage/migrations/env.py:
from app.models.author import Author # noqa: F401
from app.models.book import Book # noqa: F401 # ADD NEW IMPORTS
This ensures Alembic can detect the model and generate migrations for it.
Best Practices¶
✅ Do¶
- Always review generated migrations before applying
- Test migrations on development/staging before production
- Backup production database before running migrations
- Use descriptive migration messages:
make migration msg="Add user email verification" - Keep migrations small and focused on one logical change
- Commit migration files to version control
- Add custom data migrations when needed (e.g., backfilling data)
❌ Don't¶
- Never edit applied migrations - create a new migration instead
- Never delete migration files that have been applied to production
- Don't skip reviewing auto-generated migrations
- Don't make breaking changes without a rollout plan
- Don't drop columns without checking for data
- Never use
make rollbackin production without careful consideration
Common Operations¶
Add a Column¶
# Model change
class Author(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
bio: str | None = None # NEW
Remove a Column¶
# Model change - remove field
class Author(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
# bio removed
make migration msg="Remove bio from Author"
# Review the migration - it will drop the column!
make migrate
Rename a Column¶
Alembic can't auto-detect renames (it sees drop + add). Do it manually:
def upgrade() -> None:
op.alter_column('author', 'name', new_column_name='full_name')
def downgrade() -> None:
op.alter_column('author', 'full_name', new_column_name='name')
Change Column Type¶
# Model change
class Author(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(max_length=200) # Changed from default
Add Index¶
# Model change
class Author(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True) # Add index
Additional Resources¶
- Alembic Documentation
- SQLModel Documentation
- Alembic Tutorial
- Alembic Auto Generate
- SQLAlchemy Migration Operations
Support¶
If you encounter issues with migrations:
- Check this documentation
- Review the Alembic documentation
- Check migration history:
make migration-history - Verify current version:
make migration-current - Ask the team for help if stuck