Only registred users can make comments
Aleksandro Matejic

Flask-Migrate: Complete Guide from Basics to Advanced Troubleshooting

Flask-Migrate

Flask-Migrate is an essential tool for managing database schema changes in Flask applications. This extension provides a simple interface to Alembic, SQLAlchemy's database migration framework, allowing you to evolve your database structure safely over time without losing data.

Why This Article?

I wrote this article after encountering various issues in my own projects. Initially, it was cluttered with numerous notes, but I decided to make it more useful for other developers who might be working with Flask. It's still not perfect and some parts might not make complete sense, but you should take it for what it is. I primarily use it as a reference guide to remind myself of solutions when I encounter similar issues. Also, some solutions were accumulated over years and might or might not still be valid.

What is Flask-Migrate?

When building Flask applications with databases, you'll inevitably need to modify your database structure - adding new tables, changing column types, or updating relationships. Simply recreating tables would destroy your existing data. Flask-Migrate solves this problem by tracking and applying incremental changes to your database schema through migration files.

Think of migrations as version control for your database. Each migration file represents a specific change, and Flask-Migrate keeps track of which changes have been applied to your database.

Why Use Flask-Migrate?

Database Evolution Without Data Loss

Traditional approaches to database changes often require dropping and recreating tables, resulting in data loss. Flask-Migrate allows you to modify your database structure while preserving existing data.

Team Collaboration

In team environments, database schema changes need to be synchronized across different developers' local databases and production environments. Migration files ensure everyone works with the same database structure.

Production Safety

Flask-Migrate provides mechanisms to test database changes before applying them to production, reducing the risk of breaking your live application.

Installation and Setup

Installing Flask-Migrate

First, install the Flask-Migrate package in your project environment:

pip install Flask-Migrate
 

Environment Configuration

Set the FLASK_APP environment variable to point to your main application file:

Linux/macOS:

export FLASK_APP=app.py
 

Windows:

set FLASK_APP=app.py
 

Application Configuration

In your Flask application's main file (typically __init__.py or app.py), configure Flask-Migrate:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'your_database_url_here'

db = SQLAlchemy(app)
migrate = Migrate(app, db)
 

Important Note: If you encounter the error "KeyError 'migrate'" when running flask db init, ensure you have the migrate = Migrate(app, db) line in your configuration. This is a common setup issue documented in Flask-Migrate issue #196.

Understanding SQLAlchemy Models

Before working with migrations, you need to understand SQLAlchemy models. These are Python classes that represent database tables:

from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

db = SQLAlchemy()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    user = db.relationship('User', backref=db.backref('posts', lazy=True))

Basic Flask-Migrate Workflow

Step 1: Initialize Migration Repository

Important: Activate your virtual environment before running any Flask-Migrate commands.

flask db init
 

This command creates a migrations directory in your project with the following structure:

migrations/
├── alembic.ini
├── env.py
├── script.py.mako
└── versions/

If the migrations/versions folder is missing, create it manually:

mkdir -p migrations/versions

Step 2: Generate Migration Files

After defining or modifying your SQLAlchemy models, generate a migration file:

flask db migrate -m "Initial migration"
 

This command:

  • Compares your current models with the existing database schema
  • Generates a migration file in the migrations/versions/ directory
  • Creates both upgrade() and downgrade() functions

Example generated migration file:

"""Initial migration

Revision ID: 75188cdfaf97
Revises: 
Create Date: 2024-01-15 10:30:45.123456

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers
revision = '75188cdfaf97'
down_revision = None
branch_labels = None
depends_on = None

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('user',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('username', sa.String(length=80), nullable=False),
    sa.Column('email', sa.String(length=120), nullable=False),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('email'),
    sa.UniqueConstraint('username')
    )
    # ### end Alembic commands ###

def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('user')
    # ### end Alembic commands ###
 

Step 3: Apply Migrations

Apply the generated migration to your database:

flask db upgrade
 

This command executes the upgrade() function in your migration files, applying the changes to your database.

Essential Flask-Migrate Commands

Migration Management Commands

Shows the current revision of your database:

flask db current
# Output: 75188cdfaf97 (head)
 

Displays the migration history:

flask db history
# Shows all migrations in chronological order
 

Shows details of a specific migration:

flask db show [revision]
 

flask db downgrade [revision]

Reverts migrations to a specific revision:

flask db downgrade base  # Revert all migrations
flask db downgrade -1    # Revert one migration
 

flask db stamp [revision]

Sets the migration state without running migrations:

flask db stamp head  # Mark database as up-to-date
 

Advanced Commands

flask db branches

Shows migration branches (useful for resolving conflicts):

flask db branches
 

flask db merge

Merges multiple migration branches:

flask db merge -m "merge conflicts" [revision1] [revision2]
 

Working with Model Changes

Adding New Columns

When you add a new column to your model:

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    # New column added
    phone = db.Column(db.String(20), nullable=True)
 

Generate and apply the migration:

flask db migrate -m "Add phone column to user table"
flask db upgrade
 

Modifying Existing Columns

Changing column properties requires careful consideration:

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    # Changed from String(80) to String(100)
    username = db.Column(db.String(100), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
 

Adding Relationships

When adding foreign key relationships:

class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.Text, nullable=False)
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    
    post = db.relationship('Post', backref='comments')
    user = db.relationship('User', backref='comments')
 

Best Practices

Migration File Review

Always review auto-generated migration files before applying them:

  1. Check the order of operations - Ensure dependencies are handled correctly
  2. Verify data handling - Make sure existing data won't be lost
  3. Test on development data - Never apply untested migrations to production

Naming Conventions

Use descriptive migration messages:

# Good
flask db migrate -m "Add user profile table with avatar and bio fields"

# Poor
flask db migrate -m "update"
 

Data Migrations

Sometimes you need to migrate existing data along with schema changes:

def upgrade():
    # Add new column as nullable
    op.add_column('user', sa.Column('full_name', sa.String(150), nullable=True))
    
    # Migrate existing data
    connection = op.get_bind()
    connection.execute(
        "UPDATE user SET full_name = CONCAT(first_name, ' ', last_name) WHERE full_name IS NULL"
    )
    
    # Make column non-nullable
    op.alter_column('user', 'full_name', nullable=False)
 

Team Workflow

  1. Coordinate migrations - Avoid simultaneous migration creation
  2. Use version control - Commit migration files to your repository
  3. Document complex changes - Add comments for non-obvious migrations
  4. Test downgrade operations - Ensure migrations can be reversed if needed

Advanced Troubleshooting

Common Error Scenarios

1. "Can't locate revision identified by '[revision_id]'"

Problem: Database contains revision IDs that don't exist in migration files.

Symptoms:

ERROR [flask_migrate] Error: Can't locate revision identified by '75188cdfaf97'

Causes:

  • Migration files were deleted without updating database state
  • Database restored from backup with different migration state
  • Corrupted migration tracking

Solution:

# Check current database state
psql -d your_database_name -c "SELECT * FROM alembic_version;"

# Clear corrupted version tracking
psql -d your_database_name -c "DELETE FROM alembic_version;"

# Set to current head
flask db stamp head

# Verify fix
flask db current
 

2. Foreign Key Constraint Violations

Problem: Migration fails due to dependency order issues.

Symptoms:

psycopg2.errors.DependentObjectsStillExist: cannot drop table playground_image because other objects depend on it
DETAIL: constraint playground_challenge_base_image_id_fkey on table playground_challenge depends on table playground_image

Understanding Table Dependencies:

playground_image (parent)
    ↑ referenced by
playground_challenge (child)
    ↑ referenced by  
playground_session (grandchild)

Solution: Edit the migration file to fix the order:

def upgrade():
    # CORRECT ORDER - Drop child tables before parent tables
    op.drop_table('playground_session')    # Grandchild first
    op.drop_table('playground_challenge')  # Child second
    op.drop_table('playground_image')      # Parent last

def downgrade():
    # CORRECT ORDER - Create parent tables before child tables
    op.create_table('playground_image')    # Parent first
    op.create_table('playground_challenge') # Child second
    op.create_table('playground_session')  # Grandchild last
 

3. Migration Conflicts in Team Environment

Problem: Multiple developers created migrations simultaneously.

Symptoms:

ERROR [alembic.runtime.migration] Can't locate revision identified by 'abc123'

Solution:

# Check migration history
flask db history

# Find conflicting revisions
flask db branches

# Merge branches if applicable
flask db merge -m "merge conflicts" [revision1] [revision2]

# Or manually fix down_revision in migration files
 

4. Model Changes Not Detected

Problem: Made changes to SQLAlchemy models but flask db migrate says "No changes in schema detected."

Causes:

  • Model not imported in migration environment
  • Changes don't affect database schema (e.g., only Python logic)
  • Circular import preventing model loading

Solution:

# Check if models are imported in migrations/env.py

# Force migration generation
flask db migrate -m "force migration" --autogenerate

# Manual migration if needed
flask db revision -m "manual migration"
# Edit the generated file manually
 

5. Column Type Mismatches

Problem: Changing incompatible column types.

Symptoms:

sqlalchemy.exc.ProgrammingError: column "id" cannot be cast automatically to type integer

Solution:

def upgrade():
    # Method 1: Using raw SQL with explicit casting
    op.execute("ALTER TABLE tablename ALTER COLUMN id TYPE INTEGER USING id::INTEGER")
    
    # Method 2: Multi-step conversion for complex changes
    op.add_column('tablename', sa.Column('id_new', sa.Integer()))
    op.execute("UPDATE tablename SET id_new = id::INTEGER")
    op.drop_column('tablename', 'id')
    op.alter_column('tablename', 'id_new', new_column_name='id')
 

6. Index Creation Failures

Problem: Attempting to create indexes that already exist.

Symptoms:

psycopg2.errors.InvalidTableDefinition: relation "ix_tablename_column" already exists

Solution:

def upgrade():
    # Check if index exists before creating
    connection = op.get_bind()
    inspector = sa.inspect(connection)
    indexes = inspector.get_indexes('tablename')
    
    if 'ix_tablename_column' not in [idx['name'] for idx in indexes]:
        op.create_index('ix_tablename_column', 'tablename', ['column'])
 

7. Data Migration with Constraints

Problem: Adding non-nullable columns to tables with existing data.

Solution:

def upgrade():
    # Add column as nullable first
    op.add_column('tablename', sa.Column('new_column', sa.String(50), nullable=True))
    
    # Populate existing data
    connection = op.get_bind()
    connection.execute(
        "UPDATE tablename SET new_column = 'default_value' WHERE new_column IS NULL"
    )
    
    # Make column non-nullable
    op.alter_column('tablename', 'new_column', nullable=False)
 

Diagnostic Tools and Commands

Database State Inspection

Check Migration State

# Current revision
flask db current

# Migration history
flask db history

# Show specific migration details
flask db show [revision]

# Check for branches/conflicts
flask db branches
 

Database Schema Inspection

# Check alembic version table (PostgreSQL)
psql -d database_name -c "SELECT * FROM alembic_version;"

# List all tables
psql -d database_name -c "\dt"

# Show table structure
psql -d database_name -c "\d tablename"

# Show foreign key constraints
psql -d database_name -c "SELECT conname, conrelid::regclass, confrelid::regclass FROM pg_constraint WHERE contype = 'f';"
 

SQLite Inspection

# For SQLite databases
sqlite3 database.db ".tables"
sqlite3 database.db ".schema tablename"
 

Emergency Recovery Procedures

Reset Migration State

# Always backup first!
pg_dump database_name > backup.sql

# Clear migration tracking
psql -d database_name -c "DELETE FROM alembic_version;"

# Set to head without running migrations
flask db stamp head

# Or set to specific revision
flask db stamp [revision_id]
 

Manual Migration State Fix

# If you know the exact revision your database is at
flask db stamp [known_revision_id]

# Then continue normal migrations
flask db upgrade
 

Production Deployment Strategies

Safe Migration Deployment

  1. Backup Strategy

    # Create backup before migration
    pg_dump production_db > pre_migration_backup.sql
    
    
  2. Test on Staging

    # Apply to staging environment first
    flask db upgrade
    # Run application tests
    # Verify data integrity
     
  3. Production Deployment

    # Apply with transaction support
    flask db upgrade
     

Rollback Procedures

  1. Immediate Rollback

    flask db downgrade [previous_revision]
     
  2. Full Database Restore

    # If downgrade fails, restore from backup
    psql production_db < pre_migration_backup.sql
     

Migration File Structure Deep Dive

Understanding Migration Files

Each migration file contains:

"""Migration description

Revision ID: unique_identifier
Revises: parent_revision_id
Create Date: timestamp

"""
from alembic import op
import sqlalchemy as sa

# Revision identifiers used by Alembic
revision = 'unique_identifier'
down_revision = 'parent_revision_id'  # Previous migration
branch_labels = None
depends_on = None

def upgrade():
    """Apply changes to move forward"""
    pass

def downgrade():
    """Reverse changes to move backward"""
    pass

Custom Migration Operations

Raw SQL Execution

def upgrade():
    # Execute custom SQL
    op.execute("CREATE INDEX CONCURRENTLY idx_user_email ON user(email);")
 

Conditional Operations

def upgrade():
    connection = op.get_bind()
    result = connection.execute("SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'old_table'")
    
    if result.scalar() > 0:
        op.drop_table('old_table')
 

Bulk Data Operations

def upgrade():
    # Use bulk operations for large datasets
    connection = op.get_bind()
    
    # Batch update in chunks
    connection.execute("""
        UPDATE user 
        SET status = 'active' 
        WHERE created_at > '2024-01-01' 
        AND status IS NULL
    """)
 

Performance Considerations

Large Table Migrations

When working with large tables:

  1. Use Concurrent Operations (PostgreSQL)

    # Add index without locking table
    op.execute("CREATE INDEX CONCURRENTLY idx_name ON table_name(column_name);")
     
  2. Batch Data Updates

    def upgrade():
        connection = op.get_bind()
        
        # Process in batches to avoid long-running transactions
        batch_size = 10000
        offset = 0
        
        while True:
            result = connection.execute(f"""
                UPDATE user SET normalized_email = LOWER(email)
                WHERE id IN (
                    SELECT id FROM user 
                    WHERE normalized_email IS NULL 
                    LIMIT {batch_size} OFFSET {offset}
                )
            """)
            
            if result.rowcount == 0:
                break
            offset += batch_size
     
  3. Monitor Long-Running Migrations

    # Monitor PostgreSQL activity
    SELECT pid, now() - pg_stat_activity.query_start AS duration, query 
    FROM pg_stat_activity 
    WHERE (now() - pg_stat_activity.query_start) > interval '5 minutes';
     

Testing Migrations

Unit Testing Migration Logic

import unittest
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

class MigrationTest(unittest.TestCase):
    def setUp(self):
        self.app = Flask(__name__)
        self.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
        self.db = SQLAlchemy(self.app)
        self.migrate = Migrate(self.app, self.db)
        
    def test_migration_forward_backward(self):
        with self.app.app_context():
            # Test upgrade
            flask_migrate.upgrade()
            
            # Verify schema
            # ... assertions ...
            
            # Test downgrade
            flask_migrate.downgrade()
            
            # Verify rollback
            # ... assertions ...
 

Integration Testing

#!/bin/bash
# migration_test.sh

# Create test database
createdb test_migration_db

# Apply all migrations
export FLASK_APP=app.py
export DATABASE_URL=postgresql://user:pass@localhost/test_migration_db

flask db upgrade

# Run application tests
python -m pytest tests/

# Test downgrade (optional)
flask db downgrade base

# Cleanup
dropdb test_migration_db
 

Advanced Configuration

Custom Migration Environment

Customize migrations/env.py for specific needs:

from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
import logging

# Custom configuration
def run_migrations_online():
    """Run migrations in 'online' mode with custom settings"""
    
    # Add custom compare types
    def compare_type(context, inspected_column, metadata_column, inspected_type, metadata_type):
        # Custom type comparison logic
        return False  # No differences detected
    
    # Configure context
    context.configure(
        connection=connection,
        target_metadata=target_metadata,
        process_revision_directives=process_revision_directives,
        compare_type=compare_type,
        # Include schemas for multi-tenant applications
        include_schemas=True,
        # Compare server defaults
        compare_server_default=True,
    )

Multi-Database Migrations

For applications using multiple databases:

# config.py
SQLALCHEMY_BINDS = {
    'users': 'postgresql://user:pass@localhost/users_db',
    'products': 'postgresql://user:pass@localhost/products_db'
}

# models.py
class User(db.Model):
    __bind_key__ = 'users'
    # ... model definition

class Product(db.Model):
    __bind_key__ = 'products'
    # ... model definition
 

Monitoring and Maintenance

Migration Performance Monitoring

# Add timing to migrations
import time
from alembic import op

def upgrade():
    start_time = time.time()
    
    # Your migration operations
    op.create_table('new_table', ...)
    
    duration = time.time() - start_time
    print(f"Migration completed in {duration:.2f} seconds")
 

Regular Maintenance Tasks

  1. Clean Up Old Migration Files (carefully)
  2. Monitor Database Size Growth
  3. Review Migration Performance
  4. Update Documentation

Conclusion

Flask-Migrate is a powerful tool for managing database schema evolution in Flask applications. By understanding both the basic workflow and advanced troubleshooting techniques, you can confidently manage database changes throughout your application's lifecycle.

The key to successful database migrations lies in careful planning, thorough testing, and understanding the underlying concepts. Always backup your data, test migrations on development environments, and review auto-generated migration files before applying them to production.

Comments 0

No comments yet. Be the first.