Squashing Django migrations by targeting linear paths

Migration squashing is the process of combining a long chain of Django migrations into fewer, consolidated files.

A few ways to squash migrations:

  1. the Django way:

    • use django-admin squashmigrations
    • it's the official tool, but you might hit circular dependencies that require manual fixes
  2. the "delete all" way (generating migrations from scratch):

    • delete all migrations, run TRUNCATE django_migrations, then regenerate everything
    • requires halting PR merges and extreme caution when running commands on production databases
  3. the "linear paths" way:

    • this method targets linear paths and avoids deployment nightmares

This post is mostly a summary of the "linear path" way described in Squashing Django Migrations the Easy Way by Jack Linke.

Visualize the migration graph

Django's migration system builds a Directed Acyclic Graph (DAG), where:

  • each migration depends on one or more earlier migrations
  • there are no cycles (or loops)
  • some migrations are linear (squashable)
  • some migrations branch or merge (not squashable)

To see this graph, use the migrationgraph management command:

pip install django-model-info
python manage.py migrationgraph

Identify safe vs unsafe paths

You want to squash only linear sequences that:

  • depend on exactly one previous migration
  • are depended on by only one subsequent migration

Safe squashable paths are linear:

Safe linear sequences in the dependency graph

Avoid unsafe migrations that would break the dependency graph:

  • migrations with multiple dependencies, e.g., orders/0001
  • migrations that depend on multiple migrations, e.g., orders/0012

Squash the safe migrations

Run the squashmigrations command for safe paths:

python manage.py squashmigrations orders 0002 0006
python manage.py squashmigrations orders 0007 0011
python manage.py squashmigrations invitations 0002 0003

Each new migration file:

  • replaces the specified old migrations
  • keeps track of what it replaces via the replaces = [...] attribute
  • can be deployed alongside the originals, allowing for a smooth rollout
  • is optimized by Django to avoid redundant operations

Deploy in two stages

  1. deploy the squashed migrations, but keep the old ones

    • run the squashed migrations
    • verify everything runs without error
    • check that all replaced migrations are marked as applied

      SELECT app, name, applied FROM django_migrations 
      WHERE app IN ('orders', 'invitations')
      ORDER BY app, name;
      
  2. transition fully to squashed migrations

    • delete the migrations that were replaced
    • update dependencies in any migrations that relied on them
      • swap filenames to the squashed version in the dependencies attribute
    • remove the replaces = [...] line from the squashed migration
      • this converts it into a standard migration

A note on elidable=True

You can mark RunSQL or RunPython operations as elidable by passing elidable=True:

migrations.RunPython(migrate_data_forward, migrations.RunPython.noop, elidable=True)

It's useful for:

  • one-off data fixes
  • temporary updates that don't need to persist in the final migration history

Django treats elidable operations as safe to omit when squashing.

If you're squashing old migrations that include RunPython or RunSQL, re-check whether they can safely be marked as elidable.

Avant Bootable macOS backups do not matter anymore Après Paper size standards

A Kemar Joint