Migrations
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Django 6.1 (6.1.1, released 2026-09-02; 6.1.2 is scheduled for 2026-10-06) on Python 3.12 to 3.14, with the 5.2 LTS differences noted where they matter, and was checked on 2026-10-03. Every example ran on Django 6.1.1 and Python 3.13 with SQLite, in a throw-away project with a small catalog app (the Bookshelf catalogue that grows across this section), so the SQL shown is SQLite’s. Where PostgreSQL behaves differently the text says so, and the PostgreSQL-only operations (AddIndexConcurrently and friends) are described but were not run because no PostgreSQL server was available.
Models describe what the database should look like now. Migrations are how Django gets an existing database from one shape to the next without losing its data: a version-controlled, ordered history of schema changes (and, when you need it, data changes) that every developer, test run and production server replays in the same way. This page covers the commands, the anatomy of a migration file, the graph that orders migrations across apps, merging, reversibility, data migrations, hand-written SQL, separating the database from the model state, squashing, non-atomic and zero-downtime deployments, custom operations, serialization, multiple databases and how to test it all. The field definitions are in Models & Fields, constraints and indexes in Constraints, Indexes & Composite Keys, and the PostgreSQL operations in PostgreSQL & Database Backends.
What migrations are and where they run
A migration is a normal Python file, in the migrations package of an app, that declares a Migration class with a list of dependencies and a list of operations. Django does three different jobs with that history:
-
Compute the state. Running the operations in memory, in order, reconstructs what every model looked like at a given point of history (the project state). This state is also what
makemigrationscompares with your currentmodels.pyto find out what changed, which is why the operations are declarative and why you must tell Django about every schema change (seeSeparateDatabaseAndState). -
Generate SQL. Each operation asks the database’s schema editor to turn "create a model" or "add a field" into SQL for the backend in use.
-
Record what has been applied. Applied migrations are stored in the
django_migrationstable, one row per(app, name).
Migrations are not part of the request/response cycle. They are a build and deploy step: you create them on your machine with makemigrations, commit them with the model change, and run migrate on every other environment before (or while) the new code starts serving requests. The development server only checks and prints a notice about unapplied migrations (runserver calls check_migrations()).
autodetector compares models
with the replayed migration state"] B --> C["Review the migration file
sqlmigrate, edit if needed"] C --> D["migrate on the dev database
(and run the tests)"] D --> E["Commit models.py and the
migration together"] E --> F["CI: makemigrations --check
and the test suite"] F --> G["Deploy: migrate
then switch traffic"]
Source: Migrations.
The commands
Four commands cover the daily workflow, and two more help you keep the history small.
| Command | What it does | Options worth knowing |
|---|---|---|
|
Creates new migrations from the differences between your models and the migration history. |
|
|
Applies and unapplies migrations until the database matches the target. |
|
|
Prints the SQL a migration would run, without running it. |
|
|
Lists migrations and whether they are applied. |
|
|
Collapses a range of migrations into one that |
|
|
Rewrites one migration with its operations optimised (for example |
|
Source: django-admin: makemigrations and the entries for migrate, sqlmigrate, showmigrations, squashmigrations and optimizemigration on the same page.
A first migration, end to end
The Bookshelf catalogue starts with two models.
# catalog/models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
python manage.py makemigrations catalog
python manage.py sqlmigrate catalog 0001
python manage.py showmigrations catalog
python manage.py migrate catalog
python manage.py makemigrations --check
Migrations for 'catalog':
catalog/migrations/0001_initial.py
+ Create model Author
+ Create model Book
BEGIN;
--
-- Create model Author
--
CREATE TABLE "catalog_author" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(100) NOT NULL);
--
-- Create model Book
--
CREATE TABLE "catalog_book" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(200) NOT NULL, "author_id" bigint NOT NULL REFERENCES "catalog_author" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "catalog_book_author_id_b0849980" ON "catalog_book" ("author_id");
COMMIT;
catalog
[ ] 0001_initial
Operations to perform:
Apply all migrations: catalog
Running migrations:
Applying catalog.0001_initial... OK
No changes detected
The generated file is plain Python and is meant to be read and committed (the id column is a BigAutoField because DEFAULT_AUTO_FIELD defaults to BigAutoField since Django 6.0):
# catalog/migrations/0001_initial.py
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Author",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name="Book",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("title", models.CharField(max_length=200)),
("author", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="books", to="catalog.author")),
],
),
]
Things to notice:
-
Read the summary printed by
makemigrations(+create,~alter,-remove,pPython,sSQL,?mixed). The symbols were added in Django 5.1; they come from each operation’scategory. -
sqlmigrateneeds a database connection, because Django resolves constraint names against it. Run it against a copy of the database you will migrate, and run it for every migration a DBA wants to review.--backwardsprints the SQL of the reverse direction. -
makemigrations --checkexits with status 1 when model changes lack a migration (and, since Django 4.2, it no longer writes the missing files). Put it in CI so that a forgotten migration fails the build. -
Commit the model change and its migration together; otherwise a colleague who pulls half of the change gets a database that does not match the code.
-
--namegives a migration a meaningful name instead of an autogenerated one such as0002_book_slug; the name must be a valid Python identifier.--no-headeromits the "Generated by Django" line and timestamp.
Reading the plan before applying it
migrate --plan and showmigrations --plan show what is about to happen without touching the database, which is what you want in a deployment pipeline. migrate --check exits non-zero when unapplied migrations exist (added in Django 3.1), which makes it a readiness check.
python manage.py migrate catalog --plan
python manage.py showmigrations --plan catalog
python manage.py migrate --check; echo "exit status: $?"
Planned operations:
catalog.0002_book_slug
Add field slug to book
catalog.0003_populate_book_slugs
Raw Python operation
catalog.0004_book_slug_unique
Alter field slug on book
[X] catalog.0001_initial
[ ] catalog.0002_book_slug
[ ] catalog.0003_populate_book_slugs
[ ] catalog.0004_book_slug_unique
exit status: 1
showmigrations --list (the default) groups migrations per app and, at -v 2, shows when each was applied. Apps without migrations are listed with (no migrations).
Source: showmigrations and migrate --plan.
Anatomy of a migration file
What Django looks for is a subclass of django.db.migrations.Migration named Migration. Its useful attributes are these.
| Attribute | Meaning |
|---|---|
|
Migrations that must be applied first, as |
|
The ordered list of |
|
Migrations that must run after this one, for cases where you cannot edit the other migration’s |
|
|
|
|
|
Present on squashed migrations: the list of migrations this one stands in for. |
|
A function (not an attribute) used inside |
Operations accept a few shared options too. elidable=True on RunPython and RunSQL lets the optimiser drop the operation when squashing (see Squashing migrations), and hints is passed to the database routers (see Multiple databases).
The operations at a glance
All the core operations live in django.db.migrations.operations and are re-exported from django.db.migrations.
| Operation | Effect | Notes |
|---|---|---|
|
Creates the model and its table. |
|
|
Drops the model and its table. |
|
|
Renames the model (and its table). |
Not autodetected when many fields change at once: it looks like a delete plus a create and loses the data. |
|
Change |
|
|
Changes |
Prefer |
|
Changes |
Only for migration files from before Django 4.2; use |
|
Adds or removes the |
|
|
Record changes to |
No database change; they keep the historical models used by |
|
Adds a column (or the table of a |
|
|
Drops the column. |
Irreversible when the field is not nullable and has no default. Since Django 6.0 PostgreSQL no longer uses |
|
Changes type, |
Not every change is possible on every database (for example text to integer on most). |
|
Renames the field and, unless |
|
|
Index management. |
|
|
Constraint management. |
|
| Operation | Purpose |
|---|---|
|
Arbitrary SQL, with an optional reverse and optional state operations. |
|
Python in a historical context: data migrations. |
|
Different operations for the database and for the model state. |
The django.contrib.postgres.operations module adds CreateExtension (and specific subclasses such as BloomExtension), AddIndexConcurrently, RemoveIndexConcurrently, AddConstraintNotValid and ValidateConstraint; they are covered in Zero-downtime migrations and in PostgreSQL & Database Backends.
Source: Migration operations and Database migration operations.
Dependencies and the migration graph
Migrations are per app, but the tables they create are not independent. A ForeignKey from catalog to authors needs the authors table to exist first, so the migration that adds it declares a dependency on a migration of authors. Django does not use file names or numbers for ordering: it builds a directed graph from dependencies and run_before and applies the migrations in a topological order. The numeric prefix is only a convenience for humans.
(ForeignKey to AUTH_USER_MODEL)"] c3 --> c4
Consequences worth knowing:
-
Restricting
makemigrationsormigrateto one app label is a best-effort promise: any other app needed to get the dependencies right is included. -
Unapplying a migration also unapplies every migration that depends on it, whatever the app. Use
migrate --planto see the list before you reverse anything. -
Apps without migrations must not have relations to apps with migrations. It may seem to work, but it is not supported.
-
If a
RunPythonuses a model from another app, add the latest migration of that app todependencies, otherwiseapps.get_model()raisesLookupError: No installed app with label ….
Swappable dependencies
When a model points at settings.AUTH_USER_MODEL, the autodetector writes migrations.swappable_dependency(settings.AUTH_USER_MODEL), which depends on the first migration of the app that holds the swapped-in model. For this to work the swappable model must be created in that app’s initial migration. The generated file for a nullable owner on Book looks like this (generated in the project with the built-in User; only the name of the first dependency depends on your own history):
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("catalog", "0013_title_words_not_null"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="book",
name="owner",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]
Set AUTH_USER_MODEL before the first migrate of a new project (see Custom User Model & Backends), because changing it later means rewriting history.
Using models from another app, and third-party apps that may disappear
When moving data between apps, the dependency on the old app is conditional, and get_model must tolerate its absence, so that the migration runs on projects where the old app was never installed:
from django.apps import apps as global_apps
from django.db import migrations
def forwards(apps, schema_editor):
try:
OldModel = apps.get_model("old_app", "OldModel")
except LookupError:
return # the old app is not installed: nothing to move
NewModel = apps.get_model("new_app", "NewModel")
NewModel.objects.bulk_create(
NewModel(new_attribute=old.old_attribute) for old in OldModel.objects.all()
)
class Migration(migrations.Migration):
operations = [migrations.RunPython(forwards, migrations.RunPython.noop)]
dependencies = [
("myapp", "0123_the_previous_migration"),
("new_app", "0001_initial"),
]
if global_apps.is_installed("old_app"):
dependencies.append(("old_app", "0001_initial"))
This is the documented pattern; it is reproduced as a template and was not executed because it needs two apps that do not exist in the Bookshelf project.
Merging conflicting migrations
Two developers can each create 0007_… from the same 0006. The graph then has two leaf nodes in one app, and Django refuses to continue:
CommandError: Conflicting migrations detected; multiple leaf nodes in the migration graph: (0007_book_isbn, 0007_book_page_count in catalog).
To fix them run 'python manage.py makemigrations --merge'
makemigrations --merge writes an empty migration that depends on both leaves. If the two branches touched different things (as here: one added page_count, the other isbn) the merge is safe and automatic. If they conflict, you edit the dependencies by hand to linearise them.
python manage.py makemigrations --merge --noinput
python manage.py showmigrations --plan catalog
Merging catalog
Branch 0007_book_isbn
+ Add field isbn to book
Branch 0007_book_page_count
+ Add field page_count to book
Created new merge migration .../catalog/migrations/0008_merge_0007_book_isbn_0007_book_page_count.py
[X] catalog.0006_book_title_index
[ ] catalog.0007_book_page_count
[ ] catalog.0007_book_isbn
[ ] catalog.0008_merge_0007_book_isbn_0007_book_page_count
# catalog/migrations/0008_merge_0007_book_isbn_0007_book_page_count.py
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("catalog", "0007_book_isbn"),
("catalog", "0007_book_page_count"),
]
operations = []
Alternative to a merge file: linearise by hand by changing the second branch’s dependency to the first one’s name (dependencies = [("catalog", "0007_book_page_count")] in 0007_book_isbn.py), which is what you must do when the branches really are ordered. If either branch contains a data migration, think about whether the order matters before accepting the automatic merge. Squashing the history later removes the clutter.
Related is the consistency check: if you edit dependencies and a migration is recorded as applied while one of its dependencies is not, Django refuses to run or create migrations until you fix the dependencies (InconsistentMigrationHistory).
Schema changes that need a hand-written migration
Adding a unique, non-nullable field to a table with rows
A plain AddField for a unique non-nullable field fails on a table with existing rows: the default is computed once and every row would get the same value. Django’s documented answer is three migrations: add the field as nullable, populate it, then tighten it. The example adds a unique slug to Book.
# catalog/models.py (step 1: the field is nullable and not unique yet)
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
slug = models.SlugField(max_length=220, null=True)
python manage.py makemigrations catalog --name book_slug
python manage.py sqlmigrate catalog 0002
python manage.py makemigrations catalog --empty --name populate_book_slugs
BEGIN;
--
-- Add field slug to book
--
ALTER TABLE "catalog_book" ADD COLUMN "slug" varchar(220) NULL;
CREATE INDEX "catalog_book_slug_4eebf644" ON "catalog_book" ("slug");
COMMIT;
Step 2 is a data migration (see the next section for the details of RunPython):
# catalog/migrations/0003_populate_book_slugs.py
from django.db import migrations
from django.utils.text import slugify
def populate_slugs(apps, schema_editor):
# Historical model: the version of Book as of migration 0002.
Book = apps.get_model("catalog", "Book")
db_alias = schema_editor.connection.alias
for book in Book.objects.using(db_alias).filter(slug__isnull=True):
# The id suffix keeps the slugs unique ("The Dispossessed" appears twice).
book.slug = f"{slugify(book.title)}-{book.pk}"
book.save(update_fields=["slug"])
def clear_slugs(apps, schema_editor):
Book = apps.get_model("catalog", "Book")
Book.objects.using(schema_editor.connection.alias).update(slug=None)
class Migration(migrations.Migration):
dependencies = [
("catalog", "0002_book_slug"),
]
operations = [
migrations.RunPython(populate_slugs, reverse_code=clear_slugs),
]
Step 3 changes the model to unique=True without null=True. Running makemigrations now asks a question, because the database needs something to put in existing NULL rows:
It is impossible to change a nullable field 'slug' on book to non-nullable without providing a default. This is because the database needs something to populate existing rows.
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Ignore for now. Existing rows that contain NULL values will have to be handled manually, for example with a RunPython or RunSQL operation.
3) Quit and manually define a default value in models.py.
Select an option:
Because step 2 already filled every row, write the last migration by hand (makemigrations --empty) rather than answering "1", which would put the same value in every row:
# catalog/migrations/0004_book_slug_unique.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("catalog", "0003_populate_book_slugs"),
]
operations = [
migrations.AlterField(
model_name="book",
name="slug",
field=models.SlugField(max_length=220, unique=True),
),
]
python manage.py makemigrations --check --dry-run
python manage.py migrate catalog
python manage.py sqlmigrate catalog 0004
No changes detected
Operations to perform:
Apply all migrations: catalog
Running migrations:
Applying catalog.0002_book_slug... OK
Applying catalog.0003_populate_book_slugs... OK
Applying catalog.0004_book_slug_unique... OK
BEGIN;
--
-- Alter field slug on book
--
CREATE TABLE "new__catalog_book" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "slug" varchar(220) NOT NULL UNIQUE, "title" varchar(200) NOT NULL, "author_id" bigint NOT NULL REFERENCES "catalog_author" ("id") DEFERRABLE INITIALLY DEFERRED);
INSERT INTO "new__catalog_book" ("id", "title", "author_id", "slug") SELECT "id", "title", "author_id", coalesce("slug", NULL) FROM "catalog_book";
DROP TABLE "catalog_book";
ALTER TABLE "new__catalog_book" RENAME TO "catalog_book";
CREATE INDEX "catalog_book_author_id_b0849980" ON "catalog_book" ("author_id");
COMMIT;
The last SQL block shows a SQLite limitation: it cannot alter a column in place, so Django creates a new table, copies the rows, drops the old table and renames the new one (the same emulation the migrations guide describes). PostgreSQL would issue ALTER TABLE … ALTER COLUMN … SET NOT NULL and add the unique constraint. This is also why migrating on SQLite in production is discouraged.
There is a race to be aware of: rows created after the AddField and before the data migration finishes are overwritten, so run these migrations while writes are paused, or use the batching and ordering techniques of Zero-downtime migrations.
Source: Migrations that add unique fields and Migrations: SQLite.
Renaming a field without losing data
If you rename a field and nothing else changes, the autodetector asks whether it was a rename and, when you say yes, writes RenameField instead of dropping and re-adding the column. The prompt is interactive. Run with --noinput, the question is answered "no": if the new field is not nullable and has no default, makemigrations refuses (Field 'display_name' on model 'author' not migrated: it is impossible to add a non-nullable field without specifying a default), but if the new field is nullable it silently writes RemoveField plus AddField (- Remove field full_name from author, + Add field display_name to author) and the data is lost. Without a terminal (CI) and without --noinput, the prompt fails with EOFError.
# models.py: Author.name becomes Author.full_name
echo y | python manage.py makemigrations catalog --name rename_author_name
python manage.py sqlmigrate catalog 0005
Was author.name renamed to author.full_name (a CharField)? [y/N] Migrations for 'catalog':
catalog/migrations/0005_rename_author_name.py
~ Rename field name on author to full_name
BEGIN;
--
-- Rename field name on author to full_name
--
ALTER TABLE "catalog_author" RENAME COLUMN "name" TO "full_name";
COMMIT;
migrations.RenameField(model_name="author", old_name="name", new_name="full_name")
The same applies to RenameModel, which the autodetector offers only when little else about the model changed; if you rename a model and several of its fields at once, split the work into two migrations, or add the RenameModel by hand. Since Django 6.1, renaming a model through a migration also renames its Permission.name and Permission.codename values.
Source: RenameField, RenameModel and Django 6.1 release notes.
Reversibility
migrate can go backwards: pass the target migration, or zero to unapply everything the app has applied. Each operation knows how to reverse itself (a CreateModel reverses to a drop, an AddField to a drop of the column), with some limits that matter.
python manage.py migrate catalog 0002
python manage.py showmigrations catalog
python manage.py migrate catalog
Operations to perform:
Target specific migration: 0002_book_slug, from catalog
Running migrations:
Rendering model states... DONE
Unapplying catalog.0004_book_slug_unique... OK
Unapplying catalog.0003_populate_book_slugs... OK
catalog
[X] 0001_initial
[X] 0002_book_slug
[ ] 0003_populate_book_slugs
[ ] 0004_book_slug_unique
Operations to perform:
Apply all migrations: catalog
Running migrations:
Applying catalog.0003_populate_book_slugs... OK
Applying catalog.0004_book_slug_unique... OK
-
A migration is irreversible if any of its operations is.
RunPythonwithoutreverse_codeandRunSQLwithoutreverse_sqlare irreversible;RemoveFieldof a non-nullable field with no default is too. -
Pass
RunPython.nooporRunSQL.noopas the reverse when "undo" really is "nothing to do". It documents the intent and keeps the migration reversible (in the slug example, the schema reverse already removes the column, so an explicitclear_slugswas optional). -
Reversing drops data that the forward direction created. A reversible migration is not a backup.
-
All dependent migrations are unapplied first, regardless of app.
Trying to go back past an irreversible operation raises IrreversibleError. This migration creates a table with RunSQL and no reverse:
# catalog/migrations/0005_irreversible_demo.py (demonstration only; removed afterwards)
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("catalog", "0004_book_slug_unique")]
operations = [
migrations.RunSQL("CREATE TABLE demo_books (id integer PRIMARY KEY)"),
]
django.db.migrations.exceptions.IrreversibleError: Operation <RunSQL 'CREATE TABLE demo_books (id integer PRIMARY KEY)'> in catalog.0005_irreversible_demo is not reversible
The fix is to add reverse_sql="DROP TABLE demo_books" (or RunSQL.noop).
Source: Reversing migrations and RunSQL.
Data migrations with RunPython
Migrations can change data as well as schema. Django cannot generate data migrations, but creating one is short: makemigrations --empty app writes a migration with the right dependencies, and you add a RunPython operation that takes two callables, code and optionally reverse_code. Each receives two arguments: an apps registry with the historical models for that point in the history, and the schema_editor.
Historical models
When migrations run, Django works from the versions of the models stored in the migration files, not from your current models.py. That is why a data migration must call apps.get_model() and never import the model:
-
If you import the real model, the migration works today and breaks later, when a new installation replays old migrations against a model that has since gained fields, lost fields or changed constraints. The failure is silent until then.
-
Historical models have the same fields, relationships,
Metaoptions (versioned too) and managers that setuse_in_migrations = True. They do not have your custom methods, customsave(), custom constructors or properties: plan for it. -
Base classes are stored as pointers and field options such as
upload_toandlimit_choices_toas references to functions, so those classes and functions must be kept as long as a migration refers to them. Squashing removes old references.
The test in Testing migrations proves the point: in the historical Author model, str is Django’s default (Author object (1)) rather than your method.
To make a manager available in migrations, declare it with use_in_migrations = True; Django then serializes it into an AlterModelManagers operation:
class BookManager(models.Manager):
use_in_migrations = True
def get_queryset(self):
return super().get_queryset().order_by("title")
class Book(models.Model):
objects = BookManager()
# ...
Migrations for 'catalog':
catalog/migrations/0016_alter_book_managers.py
~ Change managers on book
migrations.AlterModelManagers(
name="book",
managers=[("objects", catalog.models.BookManager())],
)
The consequence is that the manager class must stay importable for as long as that migration exists. If you build it with from_queryset(), inherit from the generated class so that it can be imported. See Managers & Custom QuerySets.
Source: Historical models, Model managers and Data migrations.
Writing the function well
-
Keep the function above the
Migrationclass, with a name that says what it does. -
Use
schema_editor.connection.aliasas the database alias (Model.objects.using(db_alias)). Model methods otherwise go to the default database, which is wrong when migrating another one. -
Prefer
bulk_create,bulk_updateandQuerySet.update()over row-by-rowsave()for large tables. Row-by-rowsave()in the slug migration above is fine for a few thousand rows; the batched variant in Zero-downtime migrations scales further. -
Write data migrations as separate migrations that sit next to the schema migrations, so they can be reverted and squashed independently.
-
Mixing a schema change and a
RunPythonin the same migration on PostgreSQL can raiseOperationalError: cannot ALTER TABLE "…" because it has pending trigger events, because both run in the same transaction. Split them into two migrations. -
Migrations run synchronously, and the
RunPythoncallables receive a synchronousschema_editor: use the synchronous ORM in them.
migrations.RunPython(forwards, reverse_code=backwards, atomic=None, hints=None, elidable=False)
atomic defaults to None: on databases without DDL transactions (MySQL, Oracle) the operation runs in a transaction, which is safe but may crash if you use the schema_editor there; pass atomic=False in that case. On SQLite and PostgreSQL, the operation adds no transaction beyond the migration’s own, unless the migration is non-atomic and you pass atomic=True.
Source: RunPython.
RunSQL, and keeping the state in sync
RunSQL runs SQL the ORM cannot express (triggers, extensions, database-specific features). Pass reverse_sql to make it reversible, and state_operations to tell the autodetector what the SQL did to the model state; otherwise the next makemigrations would want to do it again. The next migration creates the index of a Meta.indexes entry by hand:
class Book(models.Model):
# ...
class Meta:
indexes = [models.Index(fields=["title"], name="book_title_idx")]
# catalog/migrations/0006_book_title_index.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("catalog", "0005_rename_author_name"),
]
operations = [
migrations.RunSQL(
sql='CREATE INDEX "book_title_idx" ON "catalog_book" ("title")',
reverse_sql='DROP INDEX "book_title_idx"',
state_operations=[
migrations.AddIndex(
model_name="book",
index=models.Index(fields=["title"], name="book_title_idx"),
),
],
),
]
python manage.py makemigrations --check --dry-run
python manage.py sqlmigrate catalog 0006 --backwards
No changes detected
BEGIN;
--
-- Raw SQL operation
--
DROP INDEX "book_title_idx";
COMMIT;
sql may be a string, a list of strings or a list of (statement, params) tuples (use %s placeholders, and double literal percent signs when passing parameters). On every backend but PostgreSQL Django splits a multi-statement string into separate statements. Do not put BEGIN or COMMIT in the SQL of an atomic migration on PostgreSQL or SQLite; it breaks Django’s transaction handling. Hand-written SQL is not portable, so when you have more than one database in the matrix, branch on schema_editor.connection.vendor or keep the SQL in a dedicated migration.
Source: RunSQL and Model index reference.
SeparateDatabaseAndState
SeparateDatabaseAndState is the escape hatch for changes where what the database must do and what the model state must record are different. It takes two lists: database_operations, used when applying SQL, and state_operations, used when computing the state. It is a generalised version of RunSQL’s `state_operations argument. The two classic cases are renaming a table by hand to keep its data when Django would drop and recreate it, and recording a change that already happened in the database.
The documented example turns the automatic ManyToManyField of Book and Tag into one with a through model that carries an extra field. The default migration would delete the catalog_book_tags table and create a new one, losing the links. Instead, rename the table in the database and describe the new model only in the state.
# models.py (after the change)
class Book(models.Model):
# ...
tags = models.ManyToManyField("Tag", through="BookTag", blank=True)
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
class BookTag(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
is_primary = models.BooleanField(default=False)
class Meta:
constraints = [
models.UniqueConstraint(fields=["book", "tag"], name="unique_book_tag"),
]
The old table name comes from sqlmigrate or dbshell; the new one is BookTag._meta.db_table. The foreign keys keep the names Django used (book_id, tag_id), and any extra field is added after SeparateDatabaseAndState:
# catalog/migrations/0010_book_tags_through.py
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("catalog", "0009_tag_and_book_tags"),
]
operations = [
migrations.SeparateDatabaseAndState(
database_operations=[
# Old name: from sqlmigrate/dbshell. New name: BookTag._meta.db_table.
migrations.RunSQL(
sql="ALTER TABLE catalog_book_tags RENAME TO catalog_booktag",
reverse_sql="ALTER TABLE catalog_booktag RENAME TO catalog_book_tags",
),
],
state_operations=[
migrations.CreateModel(
name="BookTag",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("book", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="catalog.book")),
("tag", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="catalog.tag")),
],
options={
"constraints": [
models.UniqueConstraint(fields=["book", "tag"], name="unique_book_tag")
],
},
),
migrations.AlterField(
model_name="book",
name="tags",
field=models.ManyToManyField(blank=True, through="catalog.BookTag", to="catalog.tag"),
),
],
),
migrations.AddField(
model_name="booktag",
name="is_primary",
field=models.BooleanField(default=False),
),
]
python manage.py makemigrations --check --dry-run -v3
python manage.py migrate catalog
python manage.py shell -c "
from catalog.models import BookTag
print([(bt.book.title, bt.tag.name, bt.is_primary) for bt in BookTag.objects.all()])"
python manage.py migrate catalog 0009
No changes detected
Running migrations:
Applying catalog.0010_book_tags_through... OK
[('Kindred', 'time travel', False), ('The Dispossessed', 'utopia', False)]
Running migrations:
Rendering model states... DONE
Unapplying catalog.0010_book_tags_through... OK
The existing rows survived (the links were created with the old automatic table), the autodetector reports no pending changes, and the migration is reversible because the RunSQL has a reverse. The sqlmigrate output shows the rename followed by the table rebuild that adds is_primary on SQLite. Two details to check on your database: the old unique index of the automatic table keeps its old name after the rename, and the unique_book_tag constraint is only in the state. On SQLite the AddField that follows rebuilds the table and creates the constraint (visible in the sqlmigrate output), but on a database that alters the table in place no step creates it, so add the constraint to the database explicitly if you need it enforced under the new name.
Other uses follow the same shape: moving a model between apps (database_operations rename the table, state_operations delete the model in one app and create it in the other), changing Meta.managed, and the PostgreSQL case in the Django 4.1 release notes where only the state changes (database_operations=[] with RemoveConstraint and AddConstraint in state_operations). If the state and the database drift apart, the migration framework can break and even lose data, so check database_operations with sqlmigrate and dbshell, and state_operations with makemigrations --dry-run. A final caution from the guide: if you change an unmanaged model (managed = False) to managed, remove managed = False and generate that migration before making other schema changes to the model, because schema changes in the same migration may not be applied.
Squashing migrations
Hundreds of migrations are not a performance problem (the loader is optimised for them), but they slow down fresh databases and test runs and clutter the repository. squashmigrations reads the operations of a range of migrations, runs the optimiser over them (CreateModel followed by DeleteModel cancels out, AddField folds into CreateModel) and writes a new migration whose replaces attribute lists the originals. Both can coexist: databases part-way through the old range keep using the old files until they reach the end, and new databases use the squashed migration.
python manage.py squashmigrations catalog 0001 0004 --noinput
Will squash the following migrations:
- 0001_initial
- 0002_book_slug
- 0003_populate_book_slugs
- 0004_book_slug_unique
Optimizing...
Optimized from 5 operations to 4 operations.
Created new squashed migration .../catalog/migrations/0001_initial_squashed_0004_book_slug_unique.py
You should commit this migration but leave the old ones in place;
the new migration will be used for new installs. Once you are sure
all instances of the codebase have applied the migrations you squashed,
you can delete them.
Manual porting required
Your migrations contained functions that must be manually copied over,
as we could not safely copy their implementation.
See the comment at the top of the squashed migration for details.
The last lines matter. RunPython operations cannot be optimised through (unless they are marked elidable), and Django cannot copy functions into the new file, so it writes a reference that is not valid Python:
# Functions from the following migrations need manual copying.
# Move them and any dependencies into this file, then update the
# RunPython operations to refer to the local versions:
# catalog.migrations.0003_populate_book_slugs
class Migration(migrations.Migration):
replaces = [('catalog', '0001_initial'), ('catalog', '0002_book_slug'), ('catalog', '0003_populate_book_slugs'), ('catalog', '0004_book_slug_unique')]
# ...
migrations.RunPython(
code=catalog.migrations.0003_populate_book_slugs.populate_slugs,
reverse_code=catalog.migrations.0003_populate_book_slugs.clear_slugs,
),
Until you port the functions, every manage.py command fails with SyntaxError: invalid decimal literal. Copy populate_slugs and clear_slugs into the squashed file and refer to them locally (code=populate_slugs). Two ways to avoid the work: squash only the range after your last data migration by passing the optional start_migration_name (squashmigrations catalog 0004 0010), or mark throw-away data migrations elidable=True so that squashing drops them.
After the manual port, the state is consistent:
python manage.py showmigrations catalog
python manage.py makemigrations --check
catalog
[-] 0001_initial_squashed_0004_book_slug_unique (4 squashed migrations) Run 'manage.py migrate' to finish recording.
[X] 0005_rename_author_name
...
No changes detected
Running migrate records the squashed migration as applied on a database that had the whole range applied. On a fresh database only the squashed migration runs, in one step:
Running migrations:
Applying catalog.0001_initial_squashed_0004_book_slug_unique... OK
Applying catalog.0005_rename_author_name... OK
...
Source: Squashing migrations and squashmigrations
Finishing the squash
The recommended process is a two-release dance. Squash and keep the old files, commit and release, wait until every environment (including every third-party user of your app) has applied the new release, then transition the squashed migration into a normal one:
-
Delete the files of the migrations it replaces.
-
Update every migration that depended on the deleted ones to depend on the squashed migration instead.
-
Remove the
replacesattribute from the squashed migration. -
Run
migrate --prunefor the app, to delete the references to the removed migrations from thedjango_migrationstable. It is useful when the names of deleted migrations might be reused, and it requires an app label.
python manage.py migrate catalog --prune
python manage.py migrate --check; echo "exit status: $?"
Pruning migrations:
Pruning catalog.0001_initial OK
Pruning catalog.0002_book_slug OK
Pruning catalog.0003_populate_book_slugs OK
Pruning catalog.0004_book_slug_unique OK
exit status: 0
Source: Squashing migrations and migrate --prune
Running migrate --prune without an app label fails with Migrations can be pruned only when an app is specified. Since Django 6.0 a squashed migration can itself be squashed, before being transitioned to a normal migration; this helps when not every environment has applied the first squash yet. Squashing may also produce migrations that do not run: retry with --no-optimize (and report a bug), or, for a CircularDependencyError, break one foreign key of the loop out into a separate migration by hand.
Two helpers that shrink migrations without squashing
-
optimizemigration app_label name(Django 4.1) optimises the operations of one migration, overwriting the file. When it contains functions that must be copied manually it writes a new file suffixed_optimizedinstead.--checkexits non-zero when the migration can be optimised, which is handy as a lint. -
makemigrations --update(Django 4.2) folds new model changes into the latest migration and optimises the result, instead of creating a new file. Use it while a migration is still local and unapplied; the updated migration gets a generated name (--namekeeps one).
# catalog/migrations/0017_two_ops.py: two operations touching the same new field
operations = [
migrations.AddField("book", "subtitle", models.CharField(max_length=50, default="")),
migrations.AlterField("book", "subtitle", models.CharField(max_length=120, default="")),
]
python manage.py optimizemigration catalog 0017
Optimizing from 2 operations to 1 operations.
Optimized migration .../catalog/migrations/0017_two_ops.py
operations = [
migrations.AddField(
model_name="book",
name="subtitle",
field=models.CharField(default="", max_length=120),
),
]
# change Book.notes max_length from 50 to 200 before the first migration was applied
python manage.py makemigrations catalog --update
Migrations for 'catalog':
catalog/migrations/0016_book_notes_updated.py
+ Add field notes to book
Deleted catalog/migrations/0016_book_notes.py
Non-atomic migrations
On databases with DDL transactions (SQLite and PostgreSQL) a migration runs inside a single transaction by default, so a failure leaves nothing half-applied. On databases without them (MySQL, Oracle) every operation runs on its own, and a failed migration has to be repaired by hand, because MySQL’s atomic DDL is per statement, not per migration. For long data migrations on big tables a single transaction is a liability: it holds locks and a snapshot for the whole run. Set atomic = False to run the operations without a surrounding transaction. You can still open transactions where you want them, with transaction.atomic() or RunPython(…, atomic=True).
The next migration backfills a new title_words column in batches, one short transaction per batch:
# catalog/migrations/0012_backfill_title_words.py
from django.db import migrations, transaction
BATCH_SIZE = 2 # use a few thousand on a real table
def backfill(apps, schema_editor):
Book = apps.get_model("catalog", "Book")
db_alias = schema_editor.connection.alias
last_pk = 0
while True:
batch = list(
Book.objects.using(db_alias)
.filter(pk__gt=last_pk, title_words__isnull=True)
.order_by("pk")[:BATCH_SIZE]
)
if not batch:
break
# One short transaction per batch instead of one for the whole table.
with transaction.atomic(using=db_alias):
for book in batch:
book.title_words = len(book.title.split())
Book.objects.using(db_alias).bulk_update(batch, ["title_words"])
last_pk = batch[-1].pk
class Migration(migrations.Migration):
atomic = False
dependencies = [
("catalog", "0011_book_title_words"),
]
operations = [
migrations.RunPython(backfill, reverse_code=migrations.RunPython.noop),
]
The loop pages by primary key (pk__gt=last_pk) instead of re-querying isnull rows from the start and ends when a batch is empty. It is restartable: if it fails half-way, the rows already updated stay updated and the next run only touches the rest. The atomic attribute has no effect on databases without DDL transactions (MySQL, Oracle). Also, in a non-atomic migration, only use BEGIN/COMMIT in RunSQL on SQLite and PostgreSQL when you really mean it.
Source: Non-atomic migrations and Migrations: transactions.
Zero-downtime migrations
During a rolling deployment, old and new code run at the same time against the same database, and a schema change can lock a table for as long as it takes to rewrite it. Django has no switch for "zero downtime": it is a discipline of small, backward-compatible steps, often called expand and contract.
-
Expand. Add the new column (or table) as nullable and without a default that forces a rewrite. Old code ignores it.
-
Dual write. Deploy code that writes the old and the new columns and reads the new one (with a fallback).
-
Backfill. Fill the new column for old rows, in batches, in a non-atomic migration (above).
-
Contract. In the following release, tighten the column (
NOT NULL, constraint) and then drop the old column. Never drop a column that running code still reads.
The example makes title_words mandatory once it is backfilled. The model drops null=True and a hand-written migration tightens the column:
# catalog/migrations/0013_title_words_not_null.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("catalog", "0012_backfill_title_words"),
]
operations = [
migrations.AlterField(
model_name="book",
name="title_words",
field=models.PositiveSmallIntegerField(),
),
]
python manage.py makemigrations --check
python manage.py migrate catalog
No changes detected
Running migrations:
Applying catalog.0013_title_words_not_null... OK
Rules of thumb that follow from the documentation:
-
On older databases, adding a field with a default can rewrite the whole table, even for nullable fields. The
AddFieldreference advises adding the nullable field without the default first and then adding the default in a secondAlterFieldmigration.preserve_default=Falseonly affects what Django remembers, not the database default. -
Index creation takes a lock that blocks writes. On PostgreSQL use
AddIndexConcurrentlyandRemoveIndexConcurrentlyfromdjango.contrib.postgres.operations;CONCURRENTLYcannot run in a transaction, so such a migration must setatomic = False. -
To add a check constraint to a big PostgreSQL table without a long scan, use
AddConstraintNotValidfollowed byValidateConstraintin two separate migrations (doing both in one atomic migration is the same as a plainAddConstraint, and in one non-atomic migration may leave the database inconsistent if validation fails). -
Do not rename columns or tables that running code uses; add the new name and migrate over the two releases above.
RenameFieldandRenameModelare safe for the database but not for old code. -
On PostgreSQL, consider a short
lock_timeoutfor the session that runs the migration, so that anALTER TABLEwaiting for a lock fails fast instead of queueing every request behind it (a PostgreSQL setting, not a Django one; see PostgreSQL & Database Backends).
# PostgreSQL only: not run here (no PostgreSQL server was available)
from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False # CREATE INDEX CONCURRENTLY cannot run in a transaction
dependencies = [("catalog", "0013_title_words_not_null")]
operations = [
AddIndexConcurrently(
"book",
models.Index(fields=["isbn"], name="book_isbn_idx"),
),
]
The general database-side practices (lock levels, online schema change tools, backfilling strategies and rollback plans) are covered, independently of Django, in Evolving the Database Model: Best Practices and Core Concepts; the tools that other stacks use to do the same job are compared in Choosing a Migration Tool.
Database support
Migrations run on every backend Django ships, but the databases are not equally capable.
| Database | What to know |
|---|---|
PostgreSQL |
The most capable: transactional DDL (a failed migration rolls back completely), |
MySQL / MariaDB |
No transactions around schema changes: if a migration fails you must unpick the changes by hand before retrying. MySQL 8 made many DDL operations cheaper but cannot promise the absence of locks. There is also a relatively small limit on the combined size of the columns an index covers. |
SQLite |
Very little built-in |
Source: Migrations: backend support.
Custom operations and the schema editor
When you repeat the same hand-written change in several migrations, write your own operation. An Operation subclass implements state_forwards (mutate the project state), database_forwards and, if reversible, database_backwards, and uses the schema_editor to execute SQL. The next operation creates and drops a database view and changes no model state.
# catalog/operations.py
from django.db.migrations.operations.base import Operation, OperationCategory
class CreateView(Operation):
"""Create a database view; reverse drops it. Does not change the model state."""
reversible = True
reduces_to_sql = True # show the SQL in sqlmigrate
category = OperationCategory.SQL
def __init__(self, name, select_sql):
self.name = name
self.select_sql = select_sql
def state_forwards(self, app_label, state):
pass # views are not models, so the project state does not change
def database_forwards(self, app_label, schema_editor, from_state, to_state):
schema_editor.execute(
"CREATE VIEW %s AS %s"
% (schema_editor.quote_name(self.name), self.select_sql)
)
def database_backwards(self, app_label, schema_editor, from_state, to_state):
schema_editor.execute("DROP VIEW %s" % schema_editor.quote_name(self.name))
def describe(self):
return "Creates view %s" % self.name
@property
def migration_name_fragment(self):
return "create_view_%s" % self.name
def deconstruct(self):
# Needed only if makemigrations/squashmigrations must write the operation out.
return (self.__class__.__qualname__, [], {"name": self.name, "select_sql": self.select_sql})
# catalog/migrations/0014_book_titles_view.py
from django.db import migrations
from catalog.operations import CreateView
class Migration(migrations.Migration):
dependencies = [
("catalog", "0013_title_words_not_null"),
]
operations = [
CreateView(
"catalog_book_titles",
"SELECT id, title, title_words FROM catalog_book",
),
]
python manage.py sqlmigrate catalog 0014
python manage.py migrate catalog
python manage.py migrate catalog 0013
BEGIN;
--
-- Creates view catalog_book_titles
--
CREATE VIEW "catalog_book_titles" AS SELECT id, title, title_words FROM catalog_book;
COMMIT;
Running migrations:
Applying catalog.0014_book_titles_view... OK
Rendering model states... DONE
Unapplying catalog.0014_book_titles_view... OK
Notes from the reference:
-
reduces_to_sqlisFalseby default, which makessqlmigrateskip the operation;reversibledefaults toFalse, which makes Django refuse to go back past it;categorypicks the symbol shown bymakemigrations(OperationCategorysince Django 5.1:ADDITION+,REMOVAL-,ALTERATION~,PYTHONp,SQLs,MIXED?). -
Field instances in
ModelState.fields(and managers inModelState.managers) are shared across migrations for speed: never mutate their attributes; replace the instance instate_forwards. -
If you use model classes from
from_stateindatabase_forwards, callfrom_state.clear_delayed_apps_cache()first so related models are available. -
A views-and-tables hazard: on SQLite, an operation that rebuilds the table underneath a view fails, because the drop and rename leave the view pointing at a missing table. With the view above in place, adding an
ownerfield toBook(which makes SQLite rebuildcatalog_book) fails withOperationalError: error in view catalog_book_titles: no such table: main.catalog_book. Drop dependent views before such migrations and recreate them after. Django 6.0 made the PostgreSQL side of this explicit:RemoveFieldno longer usesCASCADE, so dependent objects Django does not manage must be removed manually before running it.
The schema editor, available as connection.schema_editor() (always as a context manager, which manages transactions and deferred SQL such as foreign-key constraints), exposes the same building blocks that operations use: create_model, delete_model, add_index, remove_index, rename_index, add_constraint, remove_constraint, alter_unique_together, alter_db_table, alter_db_table_comment, alter_db_tablespace, add_field, remove_field, alter_field and the low-level execute. You rarely need it outside migrations; it is useful in tests and tools that create temporary tables for unmanaged models:
from django.db import connection
with connection.schema_editor() as schema_editor:
schema_editor.create_model(MyUnmanagedModel)
schema_editor.connection.alias gives the name of the database being migrated, which is what multi-database migrations use.
Source: Writing your own operation and SchemaEditor.
Serialization: what can go into a migration
To write a migration, Django must turn the current model definition into Python source. It can serialize most things: numbers, strings, bytes and None, lists, sets, tuples, dicts, ranges, dates and times (aware ones too), zoneinfo.ZoneInfo instances (Django 6.0), Decimal, Enum and Flag instances (Flag since 4.2), UUID, functools.partial, pathlib paths, TextChoices and IntegerChoices members, any Django field, functions and classes defined at module top level, functions decorated with functools.cache() or functools.lru_cache() (since 5.0), and anything with a deconstruct() method. It cannot serialize nested classes, lambdas or arbitrary instances.
A class you own becomes serializable with the @deconstructible decorator, plus an eq so that makemigrations does not see a change on every run:
# catalog/models.py
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.deconstruct import deconstructible
@deconstructible
class DigitsOnly:
def __init__(self, length=13):
self.length = length
def __call__(self, value):
if not (value.isdigit() and len(value) == self.length):
raise ValidationError(f"Expected {self.length} digits.")
def __eq__(self, other):
return isinstance(other, DigitsOnly) and self.length == other.length
class Book(models.Model):
isbn = models.CharField(max_length=13, blank=True, validators=[DigitsOnly(13)])
# ...
Migrations for 'catalog':
catalog/migrations/0016_alter_book_isbn.py
~ Alter field isbn on book
migrations.AlterField(
model_name="book",
name="isbn",
field=models.CharField(blank=True, max_length=13, validators=[catalog.models.DigitsOnly(13)]),
)
With a lambda as validator instead, makemigrations fails when it writes the file:
ValueError: Cannot serialize function: lambda
Use a top-level function or a deconstructible class. The consequences of serialization are lasting: everything a migration references (validators, upload_to functions, custom fields, manager classes, base classes) must remain importable for as long as the migration exists, which is one more reason to squash. Other rules from the guide:
-
You cannot change the number of positional arguments of an already migrated custom field: old migrations call the new
initwith the old signature and raiseTypeError. Add new arguments as keyword arguments with defaults. -
To remove a custom field Django-style, first set
system_check_deprecated_detailson it, then after your deprecation periodsystem_check_removed_details, and keep the stub (withinit(),deconstruct()andget_internal_type()) until no migration references it. -
If you maintain a reusable app, create migrations with the lowest Django version you support: migration files made on Django X.Y are meant to run unchanged on X.Y+1, but there is no forward compatibility.
-
To teach Django to write a type it does not know, register a serializer with
MigrationWriter.register_serializer(type, serializer_class), where the serializer’sserialize()returns the source text and the set of imports it needs.
Multiple databases
With several databases, migrate works on one at a time: migrate --database archive applies the migrations to the archive entry of DATABASES, and each database has its own django_migrations table. A router’s allow_migrate(db, app_label, model_name=None, **hints) decides which migrations run where (see Transactions, Raw SQL & Multiple Databases). For RunPython and RunSQL, hints is passed to the router so that it can make a decision, and schema_editor.connection.alias tells the function which database it is running against. The next router and migration seed data on the default database only:
# catalog/dbrouters.py
class HintRouter:
def allow_migrate(self, db, app_label, model_name=None, **hints):
if "target_db" in hints:
return db == hints["target_db"]
return True
# config/settings.py
DATABASES["archive"] = {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "archive.sqlite3"}
DATABASE_ROUTERS = ["catalog.dbrouters.HintRouter"]
# catalog/migrations/0015_seed_default_db_only.py
from django.db import migrations
def forwards(apps, schema_editor):
Tag = apps.get_model("catalog", "Tag")
# Always use the alias of the database being migrated.
Tag.objects.using(schema_editor.connection.alias).get_or_create(name="seeded")
class Migration(migrations.Migration):
dependencies = [
("catalog", "0014_book_titles_view"),
]
operations = [
migrations.RunPython(
forwards,
reverse_code=migrations.RunPython.noop,
hints={"target_db": "default"},
),
]
python manage.py migrate
python manage.py migrate --database archive
python manage.py shell -c "
from catalog.models import Tag
print('default', list(Tag.objects.using('default').values_list('name', flat=True)))
print('archive', list(Tag.objects.using('archive').values_list('name', flat=True)))"
default ['utopia', 'time travel', 'seeded']
archive []
Both databases ended with the same schema, but only default received the seed row. If your RunPython or RunSQL touches a single model, pass model_name as a hint, which is especially important for reusable apps. Two more effects of multiple databases: makemigrations checks the consistency of the history on every database the routers allow, and tests that call migration commands must declare the databases they use (databases = {"default", "archive"} or databases = "all"), otherwise Django raises DatabaseOperationForbidden.
Source: Data migrations and multiple databases and Multiple databases.
Testing migrations and CI
Three checks keep migrations honest. The first two are cheap and belong in every pipeline:
-
python manage.py makemigrations --check --dry-runfails when someone changed a model without a migration. -
python manage.py migrateagainst an empty database (the test runner does this for you) proves the whole history replays. Reverse it too (migrate app zero) if you promise reversibility. -
A migration test for each data migration, which seeds data at the old state, migrates and asserts the result. Django has no built-in helper; the building block is
MigrationExecutor.
# catalog/tests.py
from django.core.management import call_command
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
from django.test import TransactionTestCase
class MigrationTestCase(TransactionTestCase):
"""Base class: migrate to `migrate_from`, let the test seed data, then to `migrate_to`."""
app = "catalog"
migrate_from = None
migrate_to = None
def setUp(self):
super().setUp()
self.executor = MigrationExecutor(connection)
self.executor.migrate([(self.app, self.migrate_from)])
old_apps = self.executor.loader.project_state([(self.app, self.migrate_from)]).apps
self.setUpBeforeMigration(old_apps)
self.executor = MigrationExecutor(connection) # reload the graph
self.executor.migrate([(self.app, self.migrate_to)])
self.apps = self.executor.loader.project_state([(self.app, self.migrate_to)]).apps
def setUpBeforeMigration(self, apps):
pass
def tearDown(self):
# Leave the test database fully migrated for the next test.
call_command("migrate", verbosity=0)
super().tearDown()
class PopulateSlugsTests(MigrationTestCase):
migrate_from = "0002_book_slug"
migrate_to = "0004_book_slug_unique"
def setUpBeforeMigration(self, apps):
Author = apps.get_model("catalog", "Author")
Book = apps.get_model("catalog", "Book")
author = Author.objects.create(name="Ursula K. Le Guin")
Book.objects.create(title="The Dispossessed", author=author)
Book.objects.create(title="The Dispossessed", author=author)
def test_slugs_are_filled_and_unique(self):
Book = self.apps.get_model("catalog", "Book")
slugs = sorted(Book.objects.values_list("slug", flat=True))
self.assertEqual(len(slugs), len(set(slugs)))
self.assertTrue(all(s.startswith("the-dispossessed-") for s in slugs))
def test_historical_models_have_no_custom_methods(self):
Author = self.apps.get_model("catalog", "Author")
# The real Author.__str__ returns the name; the historical one is Django's default.
self.assertTrue(str(Author(pk=1)).startswith("Author object"))
class ReversibilityTests(MigrationTestCase):
migrate_from = "0004_book_slug_unique"
migrate_to = "0004_book_slug_unique"
def test_every_migration_can_be_reversed_and_reapplied(self):
call_command("migrate", "catalog", "zero", verbosity=0)
call_command("migrate", "catalog", verbosity=0)
class NoMissingMigrationsTests(TransactionTestCase):
def test_models_and_migrations_are_in_sync(self):
# --check exits with status 1 (SystemExit) when model changes lack a migration.
call_command("makemigrations", "--check", "--dry-run", verbosity=0)
python manage.py test catalog
Creating test database for alias 'default'...
....
----------------------------------------------------------------------
Ran 4 tests in 0.140s
OK
Destroying test database for alias 'default'...
(With a second database in DATABASES, as in the multi-database section, the last test class needs databases = {"default", "archive"}.) The TransactionTestCase base is required: migrations change the schema, which cannot happen inside the transaction that wraps a plain TestCase. The tests are slower than ordinary ones, so write them for risky data migrations and delete them once the migration is squashed away. Third-party helpers such as django-test-migrations exist for the same job, but the example above only uses what Django ships. For a project-wide view of test strategy see Testing.
A pipeline step list that works for most projects: makemigrations --check --dry-run, migrate on a throw-away database of the same engine as production, the test suite, migrate --plan (kept as a build artifact for review), and on deploy migrate --check as a readiness gate followed by migrate --noinput. Run migrations once per deploy, from one place (a release job, an init container or a one-off command), not from every web process at start-up; concurrent migrate runs race on the same tables. See Deployment.
Source: Writing and running tests and makemigrations --check.
Existing databases, fakes and unmanaged tables
A few commands and options change the bookkeeping without (or instead of) running SQL. They are sharp tools.
| Option | Use |
|---|---|
|
Marks migrations up to the target as applied (or unapplied) without running SQL. For when you applied the SQL by hand. It can leave |
|
Skips an app’s initial migration if all the tables (for |
|
Creates tables for apps without migrations. Not recommended; sometimes used when migrations are too slow for hundreds of models. |
|
Removes records of nonexistent migrations (see squashing). |
To bring an app that has models and tables but no migrations (for example, created before migrations existed) under migrations: run makemigrations your_app_label to create the initial migration, then migrate --fake-initial. This only works if you have not changed the models since you made their tables and have not edited the database by hand, because Django compares models with migration files, not with the database. The difference is easy to see:
# tables exist, but django_migrations has no catalog rows
python manage.py migrate catalog 0001
python manage.py migrate catalog 0001 --fake-initial
django.db.utils.OperationalError: table "catalog_author" already exists
Target specific migration: 0001_initial, from catalog
Running migrations:
Applying catalog.0001_initial... FAKED
--fake also reverses: migrate catalog 0001 --fake against a fully migrated database does not run any reverse SQL, it only marks the later migrations as unapplied (the output reads Unapplying catalog.0005_rename_author_name… FAKED), leaving the schema ahead of the recorded state. Use it deliberately.
The MIGRATION_MODULES setting overrides the package that holds an app’s migrations, which is how you keep generated migrations for a third-party app in your own project (or disable them in tests with None, a practice that hides real problems and is best avoided). makemigrations and migrate also gained a Command.autodetector attribute in Django 5.2, so a custom command can plug in a subclass of the autodetector.
Pitfalls
-
Editing an applied migration. Never change a migration that has run anywhere else; add a new one. The exception is fixing a data migration that imported a real model instead of using
apps.get_model(), which Django’s guide explicitly allows. -
Importing the real model in
RunPython. Works today, fails when a new installation replays old migrations. Useapps.get_model(). -
Relying on custom methods in migrations. Historical models have no custom
save(), constructors or methods: the logic you need has to live in the migration. -
Answering the autodetector wrongly.
--noinputanswers "no" to the rename question, so a rename to a nullable field becomes drop plus add and the data is lost. Read the plan, runsqlmigrate, review the file. -
One migration for everything. Mixing schema changes and data changes (especially on PostgreSQL) causes pending-trigger errors and long locks. Separate them.
-
Long transactions. A big backfill in an atomic migration holds locks for its duration. Use
atomic = Falseand batches. -
Dropping a column that running code reads. Deploy the code that stops using it first, then drop it.
-
Merge conflicts resolved by deleting a migration. Never delete a migration that has been applied anywhere; merge or re-point dependencies.
-
Squashing without porting functions.
RunPythonreferences become invalid Python; copy the functions over. -
Dependent views and triggers. They are invisible to the ORM and break table rebuilds (SQLite) and drops (PostgreSQL since Django 6.0 no longer cascades).
-
SQLite as production. The emulated
ALTER TABLEis slow and fragile; test migrations on the production engine. -
Running
migratefrom every replica. Run it once per deploy from a release job. -
Forgetting
makemigrations --checkin CI. A forgotten migration then reaches production as a mismatch between code and schema. -
Using
--faketo "fix" errors. It records something that did not happen. Find the cause instead.
Where the books are outdated
The books that accompany this section predate several changes in how migrations work and look:
-
Older material describes South and
syncdb. South was replaced by built-in migrations in Django 1.7 andsyncdbwas removed in Django 1.9, so every example here usesmakemigrationsandmigrate. -
Initial migrations in older books show
AutoFieldids.DEFAULT_AUTO_FIELDwas added in Django 3.2 and defaults toBigAutoFieldsince Django 6.0, so new migrations createBigAutoField(as in the generated file above) unless the app configures otherwise. -
Old migrations that mention
NullBooleanField(deprecated in 3.1, removed in 4.0),index_together(deprecated in 4.2, removed in 5.1) orAlterIndexTogetherneed editing or squashing. Since 4.2 the advice is to useMeta.indexes(whose migrations areAddIndexand, since 4.1,RenameIndex) andUniqueConstraintinstead ofunique_together. -
Material written before Django 3.1 and 4.x does not mention
migrate --check,migrate --prune,makemigrations --scriptableand--update,optimizemigration, or the symbols in themakemigrationssummary.
What changed recently
| Version | Change |
|---|---|
3.1 |
|
4.1 |
|
4.2 |
|
5.0 |
Serialization of |
5.1 |
|
5.2 |
|
6.0 |
Squashed migrations can be squashed again before being transitioned to normal migrations; |
6.1 |
Renaming a model through a migration also renames its permissions' |
References
-
Evolving the Database Model (database-side practices and tools)