Squashing Migrations — Complete Guide
In this tutorial, you will learn about Squashing Migrations. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn squashing migrations: combine hundreds of old Migration files into a single baseline migration for faster fresh database setups, cleaner history, and improved CI/CD pipeline performance in long-lived projects.
What You Learn
You will learn what migration squashing is, why it is necessary for long-lived projects, how to squash migrations in Alembic, Django, Flyway, and Prisma, how to handle dependencies between squashed and unsquashed migrations, and best practices for squashing in team environments.
Why It Matters
After months or years of development, projects accumulate hundreds of migration files. Creating a fresh database requires applying all of them, which takes minutes. CI/CD pipelines slow down. New developers wait for migrations to complete. Squashing combines old migrations into a single baseline, reducing fresh setup time from minutes to seconds.
Real-World Use
DodaTech's backend had 347 migration files accumulated over 3 years. Fresh database setup took 4 minutes and 23 seconds. After squashing the first 300 migrations into a single baseline, fresh setup took 8 seconds. CI pipeline time dropped from 12 minutes to 8 minutes.
When to Squash
const squashCriteria = {
migrationCount: 347, // Over 100 migrations = consider squashing
freshSetupTime: '4m 23s', // Over 30 seconds = consider squashing
oldestMigration: '2023-01-15', // Over 6 months old = consider squashing
CI_pipelineTime: '12m', // Migration portion contributes significantly
};
function shouldSquash(metrics) {
return (
metrics.migrationCount > 100 ||
metrics.freshSetupTime > 30 ||
metrics.CI_pipelineTime > 5
);
}
Expected output: Squashing is recommended when migration count exceeds 100, fresh setup takes over 30 seconds, or CI pipeline time is dominated by migrations. Older migrations (over 6 months) are unlikely to be rolled back individually.
Alembic Squashing
# Check current migration count
alembic history | wc -l
# Output: 347
# Squash migrations up to a specific revision
alembic merge -m "squash" $(alembic heads | awk '{print $1}')
# Alternative: Create a squash migration manually
alembic revision --autogenerate -m "squash_001_to_300"
# migrations/squash_001_to_300.py
"""Squash migrations 001 through 300.
Combine all schema changes from the first 300 migrations
into a single baseline migration.
"""
from alembic import op
import sqlalchemy as sa
revision = 'squash_revision_001'
down_revision = None # This becomes the new base
def upgrade():
# Combined schema from all 300 migrations
op.create_table(
'users',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('email', sa.String(255), nullable=False),
sa.Column('name', sa.String(200)),
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
)
op.create_index('idx_users_email', 'users', ['email'], unique=True)
op.create_table(
'orders',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('total', sa.Float(), nullable=False),
sa.Column('status', sa.String(20), server_default='pending'),
)
op.create_foreign_key('fk_orders_users', 'orders', 'users', ['user_id'], ['id'])
# ... all other schema elements from squashed migrations
def downgrade():
# Reverse everything
op.drop_table('orders')
op.drop_table('users')
Expected output: Squash migration combines all schema changes from 300 migrations into a single file. It becomes the new base (down_revision = None). Fresh databases start with the squash migration, skipping the 300 individual files.
Django Squashing
# Django provides built-in squash support
python manage.py squashmigrations users 0001 0300
# Output:
# Creating squash migration for users...
# Will squash 300 migrations into 1
# Created new migration: users/migrations/0301_squash_0001_0300.py
# users/migrations/0301_squash_0001_0300.py
# Auto-generated Django squash migration
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [
('users', '0001_initial'),
('users', '0002_add_email_verified'),
('users', '0003_add_phone'),
# ... all replaced migrations listed
('users', '0300_add_last_login'),
]
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(primary_key=True)),
('email', models.EmailField(max_length=255, unique=True)),
('name', models.CharField(max_length=200)),
('email_verified', models.BooleanField(default=False)),
('phone', models.CharField(max_length=20, blank=True)),
('last_login', models.DateTimeField(null=True)),
],
),
]
Expected output: Django squash migration lists all replaced migrations in the replaces attribute. The migration combines all operations into one. Django knows the squash replaces the original migrations and skips them.
Flyway Squashing
# Flyway approach: Create a new baseline
# 1. Extract the current schema
pg_dump --schema-only --no-owner mydb > schema.sql
# 2. Clean the schema dump to create a baseline migration
# Remove: search_path, SET statements, comments
# Keep: CREATE TABLE, CREATE INDEX, CREATE VIEW statements
# 3. Create the baseline migration
mv schema.sql sql/V9999__baseline.sql
# 4. Update Flyway baseline configuration
flyway baseline --baselineVersion=9999
-- sql/V9999__baseline.sql
-- Squash of all migrations before this point
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(200),
email_verified BOOLEAN DEFAULT FALSE,
phone VARCHAR(20),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
total NUMERIC(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
-- All tables, indexes, and constraints from squashed migrations
Expected output: Flyway baseline migration contains the complete schema as one SQL file. The baseline version (9999) is after all previous migrations. Flyway marks all previous migrations as applied and starts fresh databases from the baseline.
Prisma Squashing
# Prisma approach: Recreate the migration history
# 1. Ensure current schema.prisma reflects desired state
# 2. Delete old migration files (keep a backup)
rm -rf prisma/migrations/*
# 3. Create a fresh baseline migration
npx prisma migrate dev --name baseline
# Output:
# Your database is now in sync with your schema.
# Created migration: prisma/migrations/20260628000000_baseline
// prisma/schema.prisma (final state after all migrations)
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
phone String?
emailVerified Boolean @default(false)
lastLogin DateTime?
orders Order[]
createdAt DateTime @default(now())
}
model Order {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
total Float
status String @default("pending")
createdAt DateTime @default(now())
}
Expected output: Prisma squashing deletes old migrations and creates a single baseline migration. The schema.prisma file reflects the final schema state. New databases start with the baseline migration.
Verifying the Squash
# Verify squash correctness with a fresh database
#!/bin/bash
set -euo pipefail
echo "=== Squash Verification ==="
# Create a fresh database from old migrations
echo "Setting up database with old migrations..."
createdb test_squash_old
export DATABASE_URL=postgresql://localhost/test_squash_old
alembic upgrade head
pg_dump --schema-only test_squash_old > schema_old.sql
# Create a fresh database from squash
echo "Setting up database with squash..."
createdb test_squash_new
export DATABASE_URL=postgresql://localhost/test_squash_new
# Apply only the squash migration
alembic upgrade squash_revision_001
pg_dump --schema-only test_squash_new > schema_new.sql
# Compare schemas
echo "Comparing schemas..."
diff schema_old.sql schema_new.sql
if [ $? -eq 0 ]; then
echo "PASS: Squash produces identical schema"
else
echo "FAIL: Schema mismatch between old and squash"
exit 1
fi
# Clean up
dropdb test_squash_old
dropdb test_squash_new
echo "=== Verification Complete ==="
Expected output: Verification creates databases from both old migrations and the squash, dumps schemas, and compares them. Identical schemas confirm the squash is correct. This test should run in CI after squashing.
Updating Dependencies
# Before squash: migrations in unsquashed branch depend on old migration
# migrations/0301_add_new_feature.py
"""Add new feature after squash."""
revision = '0301'
down_revision = '0300' # Depends on old migration 0300
# After squash: update dependency to squash revision
# migrations/0301_add_new_feature.py (updated)
"""Add new feature after squash."""
revision = '0301'
down_revision = 'squash_revision_001' # Now depends on squash
Expected output: After squashing, all unsquashed migrations that depended on old migrations must be updated to depend on the squash revision. This ensures the migration chain remains intact.
Common Mistakes
1. Squashing Too Early
Squashing migrations that are still being rolled back in different environments causes problems. Only squash migrations that have been applied everywhere and are unlikely to need individual rollback.
2. Not Verifying the Squash
A squash that produces a different schema than the original migrations causes data loss or application errors. Always verify the squash produces an identical schema. Test against a production backup copy.
3. Forgetting to Update Downstream Dependencies
Unsquashed migrations that depend on old migrations break after squashing. Update all downstream migration dependencies. Run the full migration chain from scratch to verify.
4. Squashing Without Team Coordination
Squashing changes the migration history. Team members with feature branches that include old migrations will have conflicts. Coordinate the squash timing. Communicate the new baseline to all developers.
5. Deleting Squashed Migrations
After squashing, keep old migration files during a transition period. Some environments may still need them. Remove them only after verifying all environments have applied the squash.
6. Not Updating CI/CD Pipelines
After squashing, CI pipelines should use the new baseline. Update CI configuration to start from the squash. Verify pipeline time improvement after squashing.
Practice Questions
1. What is migration squashing?
Combining many old migration files into a single baseline migration that represents the cumulative schema. Fresh databases start from the baseline, skipping the individual old migrations.
2. When should you squash migrations?
When migration count exceeds 100, fresh database setup takes over 30 seconds, CI pipeline time is dominated by migrations, or the oldest migrations are over 6 months old and unlikely to need individual rollback.
3. How do you verify a squash is correct?
Create fresh databases from both old migrations and the squash. Dump both schemas. Compare them. If they are identical, the squash is correct. Use pg_dump or equivalent for your database.
4. What happens to unsquashed migrations after squashing?
Unsquashed migrations must update their dependencies to point to the squash revision. The squash becomes the new base. All subsequent migrations chain from the squash.
Challenge
Squash a projects migration directory with 300+ migrations: analyze migration count and fresh setup time, create a squash migration combining all migrations into one, verify the squash produces identical schema, update all downstream migration dependencies, update CI/CD to use the baseline, and create a rollback plan if the squash causes issues.
FAQ
Mini Project: Squash Automation
Build a squash automation tool that: counts migrations and measures setup time, recommends when to squash, creates a squash migration for Alembic or Django, verifies the squash produces identical schema, updates downstream migration dependencies, runs the full migration chain from scratch, and generates a report of time saved.
What's Next
Now that you understand squashing migrations, learn about Testing Migrations for ensuring migration correctness before production deployment.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro