How Django Migrations Generate and Apply Schema Changes

Django migrations serve as a version control system for your database schema, translating Python model declarations into database-specific SQL statements. This article explains the underlying mechanism of Django migrations, breaking down how the framework inspects model definitions, detects schema differences, writes migration files as pure Python code, and executes those instructions against a relational database while maintaining a reliable execution history.

1. Detecting Schema Changes with makemigrations

When you run python manage.py makemigrations, Django does not inspect your actual database. Instead, it inspects your codebase. It performs this comparison through the following steps:

  1. State Construction: Django reconstructs the "historical state" of your models by reading all existing migration files in chronological order in memory.
  2. Current State Evaluation: Django loads the live definitions currently present in your models.py files.
  3. Autodetection: The MigrationAutodetector class compares the historical state with the current state. It identifies differences, such as added models, removed fields, altered constraints, or modified indexes.
  4. Operation Generation: The autodetector converts these differences into a sequence of serialized Python objects known as Operations (such as migrations.CreateModel, migrations.AddField, or migrations.AlterField).

2. The Structure of a Migration File

The output of makemigrations is a Python file placed in the app's migrations/ directory. Each file defines a subclass of django.db.migrations.Migration containing two primary attributes:

3. Resolving Dependencies and Tracking State with migrate

Running python manage.py migrate triggers the process of applying these Python-based instructions to the database:

  1. Reading Applied Migrations: Django queries a special database table named django_migrations. This table stores the app name, the migration name, and the timestamp of when each migration was executed.
  2. Building the Execution Plan: Django compares the migrations found in the filesystem against the records in django_migrations. Any migration present on disk but absent from the database table is scheduled for execution according to the dependency graph.

4. Translating Python Operations to SQL

To execute an operation, Django uses a database-specific wrapper called the SchemaEditor (e.g., DatabaseSchemaEditor for PostgreSQL, MySQL, or SQLite):

5. Finalizing the Database State

Once the SchemaEditor successfully executes the generated SQL statements against the target database, Django inserts a new row into the django_migrations table containing the app label and migration name. This marks the migration as applied, ensuring that subsequent runs of migrate ignore already-processed files.