Database Migrations¶
Complete guide for managing database schema changes using Alembic in Poolula Platform.
Overview¶
Alembic is the database migration tool for SQLModel/SQLAlchemy applications.
Why use migrations:
-
Track schema changes over time
-
Apply changes consistently across environments
-
Rollback changes if needed
-
Share schema updates with team
-
Document database evolution
Migration Workflow¶
Example: Adding a new column to the Property model
# 1. Edit core/database/models.py
class Property(SQLModel, table=True):
...
zoning: Optional[str] = Field(default=None) # NEW
# 2. Generate migration
.venv/bin/alembic revision --autogenerate -m "Add zoning column to properties"
# 3. Review generated file in alembic/versions/
# 4. Apply migration
.venv/bin/alembic upgrade head
Quick Start¶
Check Current Migration Status¶
Apply All Pending Migrations¶
# Upgrade to latest version
.venv/bin/alembic upgrade head
# Output:
# INFO [alembic.runtime.migration] Running upgrade -> fcf071ccc323, initial schema
View Migration History¶
# Show all migrations
.venv/bin/alembic history
# Show verbose history
.venv/bin/alembic history --verbose
Alembic Setup¶
Project Structure¶
poolula-platform/
├── alembic/
│ ├── env.py # Alembic configuration
│ ├── script.py.mako # Migration template
│ └── versions/ # Migration files
│ └── fcf071ccc323_initial_schema.py
├── alembic.ini # Alembic settings
└── core/database/
├── models.py # SQLModel models (source of truth)
└── connection.py # Database connection
Configuration¶
alembic.ini:
[alembic]
script_location = alembic
sqlalchemy.url = sqlite:///./poolula.db
# Logging
[loggers]
keys = root,sqlalchemy,alembic
alembic/env.py:
# Imports SQLModel metadata
from core.database.models import SQLModel
target_metadata = SQLModel.metadata
Creating Migrations¶
Autogenerate Migration (Recommended)¶
Alembic compares your SQLModel models to the current database schema and generates migration code.
# Create migration with descriptive message
.venv/bin/alembic revision --autogenerate -m "Add zoning column to properties"
# Output:
# Generating /path/to/alembic/versions/abc123_add_zoning_column.py ... done
Always review autogenerated migrations! They may need manual adjustments.
Manual Migration¶
For complex changes that autogenerate can't handle:
# Create empty migration template
.venv/bin/alembic revision -m "Custom data transformation"
# Edit the generated file manually
Migration File Anatomy¶
"""Add zoning column to properties
Revision ID: abc123def456
Revises: fcf071ccc323
Create Date: 2024-11-15 10:30:00.123456
"""
from alembic import op
import sqlalchemy as sa
# Revision identifiers
revision = 'abc123def456'
down_revision = 'fcf071ccc323' # Previous migration
branch_labels = None
depends_on = None
def upgrade():
"""Apply schema changes"""
op.add_column('properties',
sa.Column('zoning', sa.String(length=50), nullable=True)
)
def downgrade():
"""Revert schema changes"""
op.drop_column('properties', 'zoning')
Key parts:
-
revision: Unique ID for this migration -
down_revision: Points to previous migration (creates chain) -
upgrade(): SQL to apply changes -
downgrade(): SQL to revert changes
Applying Migrations¶
Upgrade to Latest¶
# Apply all pending migrations
.venv/bin/alembic upgrade head
# Output shows each migration applied:
# INFO [alembic.runtime.migration] Running upgrade fcf071ccc323 -> abc123def456, add zoning column
Upgrade to Specific Version¶
# Upgrade to specific revision
.venv/bin/alembic upgrade abc123def456
# Upgrade by relative number
.venv/bin/alembic upgrade +2 # Apply next 2 migrations
Downgrade (Rollback)¶
# Rollback one migration
.venv/bin/alembic downgrade -1
# Rollback to specific version
.venv/bin/alembic downgrade fcf071ccc323
# Rollback all migrations (dangerous!)
.venv/bin/alembic downgrade base
Dry Run (Show SQL Without Executing)¶
# Show SQL that would be executed
.venv/bin/alembic upgrade head --sql
# Useful for reviewing changes before applying
Common Migration Scenarios¶
Adding a Column¶
Model change:
class Property(SQLModel, table=True):
...
property_manager: Optional[str] = Field(default=None) # NEW
Generate migration:
Generated migration (example):
def upgrade():
op.add_column('properties',
sa.Column('property_manager', sa.String(), nullable=True)
)
Dropping a Column¶
Model change:
Migration:
⚠️ Warning: Dropping columns is destructive. Data will be lost.
Renaming a Column¶
Autogenerate can't detect renames! You must edit the migration manually.
Model change:
class Property(SQLModel, table=True):
...
# Renamed: property_type → usage_type
usage_type: Optional[str] = Field(default=None)
Generate migration, then edit:
Edit the generated file:
def upgrade():
# Autogenerate will show DROP + ADD
# Replace with:
op.alter_column('properties', 'property_type', new_column_name='usage_type')
def downgrade():
op.alter_column('properties', 'usage_type', new_column_name='property_type')
Adding a Table¶
Create new model:
class Tenant(SQLModel, table=True):
__tablename__ = "tenants"
id: UUID = Field(default_factory=uuid4, primary_key=True)
name: str = Field(max_length=200)
email: str = Field(max_length=200)
property_id: UUID = Field(foreign_key="properties.id")
Generate migration:
Changing Column Type¶
Model change:
# Change from Optional[str] to int
class Property(SQLModel, table=True):
...
year_built: Optional[int] = Field(default=None) # Was str
⚠️ Requires data migration:
Edit migration to handle data:
def upgrade():
# 1. Add new column
op.add_column('properties',
sa.Column('year_built_new', sa.Integer(), nullable=True)
)
# 2. Copy/convert data
op.execute("""
UPDATE properties
SET year_built_new = CAST(year_built AS INTEGER)
WHERE year_built IS NOT NULL AND year_built != ''
""")
# 3. Drop old column
op.drop_column('properties', 'year_built')
# 4. Rename new column
op.alter_column('properties', 'year_built_new', new_column_name='year_built')
Adding Index¶
def upgrade():
op.create_index('ix_properties_status', 'properties', ['status'])
def downgrade():
op.drop_index('ix_properties_status')
Adding Foreign Key¶
def upgrade():
op.create_foreign_key(
'fk_transactions_property', # Constraint name
'transactions', # Source table
'properties', # Target table
['property_id'], # Source column
['id'] # Target column
)
Data Migrations¶
Sometimes you need to transform data, not just schema.
Example: Populate new column from existing data
def upgrade():
# 1. Add column
op.add_column('properties',
sa.Column('full_address', sa.String(), nullable=True)
)
# 2. Populate from existing data
op.execute("""
UPDATE properties
SET full_address = address || ', ' || city || ', ' || state
WHERE address IS NOT NULL
""")
# 3. Make column non-nullable (optional)
op.alter_column('properties', 'full_address', nullable=False)
Use raw SQL with op.execute() for data transformations.
Migration Best Practices¶
1. Always Review Autogenerated Migrations¶
# After generating, review the file
.venv/bin/alembic revision --autogenerate -m "Description"
# Open the generated file and check:
cat alembic/versions/abc123_description.py
Check for:
-
Correct operations (add/drop/alter)
-
Proper
upgrade()anddowngrade()functions -
Unintended changes (autogenerate can be overzealous)
2. Test Migrations Before Applying¶
# Upgrade
.venv/bin/alembic upgrade head
# Verify it worked
python -c "from core.database.connection import check_connection; check_connection()"
# Test downgrade
.venv/bin/alembic downgrade -1
# Upgrade again
.venv/bin/alembic upgrade head
3. One Logical Change Per Migration¶
Good:
Bad:
Reason: Easier to review, rollback, and debug.
4. Write Reversible Migrations¶
Always implement downgrade():
def upgrade():
op.add_column('properties', sa.Column('new_field', sa.String()))
def downgrade():
op.drop_column('properties', 'new_field') # ✓ Can rollback
5. Backup Before Destructive Changes¶
# Before dropping columns or tables
python scripts/backup.py
# Then apply migration
.venv/bin/alembic upgrade head
6. Use Descriptive Migration Messages¶
Good:
-
"Add tenant_id foreign key to transactions"
-
"Change property_status from string to enum"
-
"Create payment_history table"
Bad:
-
"Update database"
-
"Changes"
-
"Migration 2"
Troubleshooting¶
"Target database is not up to date"¶
Error:
Solution:
"Can't locate revision identified by 'xyz'"¶
Error:
Solution:
Migration file was deleted or repo is out of sync:
# Show current version
.venv/bin/alembic current
# Manually set version (dangerous - only if you know what you're doing)
.venv/bin/alembic stamp head
Migration Conflicts (Multiple Heads)¶
Error:
Solution:
Create a merge migration:
Autogenerate Doesn't Detect Change¶
Common causes:
-
Column has same name and type
-
Alembic can't detect renames
-
Change is in data, not schema
Solution:
Create manual migration:
SQLite Limitations¶
SQLite doesn't support:
-
ALTER COLUMN(change type, add constraint) -
DROP COLUMNin some cases
Workaround: Use table recreation pattern:
def upgrade():
# 1. Create new table with desired schema
op.create_table('properties_new', ...)
# 2. Copy data
op.execute('INSERT INTO properties_new SELECT * FROM properties')
# 3. Drop old table
op.drop_table('properties')
# 4. Rename new table
op.rename_table('properties_new', 'properties')
Better: Switch to PostgreSQL for production if you need advanced ALTER operations.
Migration Workflow Example¶
Complete workflow for adding a feature:
1. Create Feature Branch¶
2. Update Models¶
# core/database/models.py
class Tenant(SQLModel, table=True):
__tablename__ = "tenants"
id: UUID = Field(default_factory=uuid4, primary_key=True)
name: str
email: str
property_id: UUID = Field(foreign_key="properties.id")
lease_start: date
lease_end: date
3. Generate Migration¶
4. Review Migration¶
# Open generated file
cat alembic/versions/abc123_add_tenants_table.py
# Check upgrade/downgrade functions
5. Test Migration¶
# Apply migration
.venv/bin/alembic upgrade head
# Verify table was created
sqlite3 poolula.db ".schema tenants"
# Test rollback
.venv/bin/alembic downgrade -1
# Verify table was dropped
sqlite3 poolula.db ".tables"
# Re-apply
.venv/bin/alembic upgrade head
6. Commit Changes¶
git add core/database/models.py alembic/versions/abc123_add_tenants_table.py
git commit -m "Add Tenant model and database table
- Created Tenant SQLModel with lease tracking
- Generated Alembic migration for tenants table
- Foreign key relationship to properties
"
7. Share with Team¶
Production Considerations¶
Staging First¶
# 1. Test on staging database
DATABASE_URL=postgresql://staging .venv/bin/alembic upgrade head
# 2. Verify application works
# 3. Apply to production
DATABASE_URL=postgresql://production .venv/bin/alembic upgrade head
Zero-Downtime Migrations¶
Strategy for production:
- Add new column (nullable)
-
Deploy code that writes to both old and new columns
-
Backfill data
- Make column non-nullable
-
Deploy code that only uses new column
-
Drop old column
Migration in CI/CD¶
# .github/workflows/deploy.yml
- name: Apply database migrations
run: |
.venv/bin/alembic upgrade head
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Advanced Topics¶
Branching¶
For parallel development with migrations:
# Create branch from specific revision
.venv/bin/alembic revision -m "Feature A" --head=abc123
# Merge branches later
.venv/bin/alembic merge -m "Merge features" def456 ghi789
Offline SQL Generation¶
Generate SQL scripts for DBA review:
# Generate SQL file
.venv/bin/alembic upgrade head --sql > migration.sql
# DBA reviews and applies manually
Custom Migration Path¶
Skip specific migrations:
# Upgrade to specific version, skipping problematic migration
.venv/bin/alembic upgrade abc123
.venv/bin/alembic stamp xyz789 # Mark as if we applied xyz789
.venv/bin/alembic upgrade head
Related Documentation¶
-
Testing Guide - Running tests after migrations
-
Database Models - SQLModel schemas
-
Data Import - Seeding migrated database
-
Deployment - Production migration workflow