What does Django's SeparateDatabaseAndState migration operation do, and when would you reach for it?
answer
- two things a migration changes
- the autodetector's view
- two lists of operations
- remove from models, drop later
basics
~20 sSeparateDatabaseAndState takes two lists: state_operations change Django's model state, database_operations change the real schema. It lets you change one without the other, such as removing a field from the models a release before dropping its column.
solid answer
~40 sEvery normal operation does two things: it updates Django's in-memory **project state**, which the autodetector and historical models are built from, and it changes the **database**. `SeparateDatabaseAndState(database_operations=[...], state_operations=[...])` decouples them. The classic zero-downtime use is retiring a column: one release makes the field nullable, the next removes it with `state_operations=[RemoveField(...)]` and no database operations, so `makemigrations` is satisfied while the column stays for old code that still selects it, and a later release drops the column with `RunSQL`. Other uses are adopting a table or index created by hand, and switching a `ManyToManyField` to an explicit `through` model without rebuilding the table. The risk is drift: if state and schema disagree, later migrations can fail or lose data, so I check with `sqlmigrate` and `makemigrations --dry-run`.
code
python · 14 linesfrom django.db import migrations
class Migration(migrations.Migration):
dependencies = [("analytics", "0032_alter_event_legacy_payload_null")]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.RemoveField(model_name="event", name="legacy_payload"),
],
database_operations=[],
),
]go deeper
Know that a migration changes both Django's model state and the database, and that SeparateDatabaseAndState lets you change them separately.
Explain the two lists, how a state-only RemoveField keeps the column, and why the later drop must use RunSQL.
Use it to sequence zero-downtime removals and adoptions, and verify both sides with sqlmigrate and makemigrations --dry-run.
Limit its use to reviewed cases with a written plan, since every use is a place where state and schema could drift.
## Two sides of every migration A Django migration operation such as `AddField` has two responsibilities: - **State**: update the in-memory **project state**, the description of all models that Django rebuilds by replaying migration files. `makemigrations` compares this state with your `models.py`, and `RunPython` gets its historical models from it. - **Database**: change the actual schema with SQL through the schema editor. Usually the two move together. `SeparateDatabaseAndState` lets you specify them separately. ## How the operation works `SeparateDatabaseAndState(database_operations=None, state_operations=None)`: 1. When Django applies state, it runs only the `state_operations`. 2. When Django changes the database, it runs only the `database_operations`, each against the state it expects. 3. Unapplying reverses the database operations in reverse order. Either list can be empty, which gives you a **state-only** or a **database-only** change. `RunSQL` has a narrower version of the same idea through its own `state_operations` argument. ## Where it earns its keep | Situation | state_operations | database_operations | |---|---|---| | Remove a field from models while the old release still selects the column | `RemoveField` | none | | Adopt a table or index someone created by hand | `CreateModel` / `AddIndex` | none | | Switch a `ManyToManyField` to an explicit `through` model on the existing table | `CreateModel` for the through model, then `AlterField` | a `RunSQL` renaming the join table | | Build something in SQL that Django cannot express, but describe it to the autodetector | the equivalent Django operation | `RunSQL` | ## Example: retiring a column across releases The `analytics.Event.legacy_payload` field is no longer used. 1. **Release N-1**: `AlterField` makes it `null=True`, so inserts that omit it stay valid. 2. **Release N**: remove it from the model and write a migration with `SeparateDatabaseAndState(state_operations=[RemoveField("event", "legacy_payload")], database_operations=[])`. New code no longer references it; old processes still select it, and the column is still there. 3. **Release N+1**: drop the column with `RunSQL("ALTER TABLE analytics_event DROP COLUMN legacy_payload;", reverse_sql=...)`. A `RemoveField` cannot do this step, because the field is already gone from state and the operation would not find it. ## Risks and checks - Django's documentation calls it highly specialised and warns that if the database and Django's state drift apart, the migration framework can break, "even leading to data loss". - Check the database side with `sqlmigrate` (and `dbshell` for the live schema). - Check the state side with `makemigrations --dry-run`: after a correct state-only change, it should report no changes. - Write the reverse path deliberately; a state-only removal reverses by re-adding the field to state, which is only right if the column still exists. In an interview, the concise version is: state is what Django believes, database is what exists, and this operation lets you move them one at a time when zero-downtime ordering needs it.
- How do you confirm a state-only SeparateDatabaseAndState left Django's state matching your models?Run `makemigrations --dry-run` (or `--check`). If the state operations describe the models correctly, the autodetector reports no changes. Then use `sqlmigrate` on the migration to confirm it emits no SQL for the state-only part.
- How does RunSQL's state_operations argument relate to SeparateDatabaseAndState?It is a narrower form of the same idea: `RunSQL` runs raw SQL on the database and applies the listed operations to state only. `SeparateDatabaseAndState` generalises it, letting both sides be arbitrary lists of operations, including an empty database side.
It is like updating a building's floor plan and doing the construction separately: you can strike a room from the plan today so no new tenant is sent there, and send the demolition crew next month once the last old tenant has moved out.
saying these in an interview costs you the question
- state_operations also run against the database
- A state-only RemoveField drops the column
- A later RemoveField can drop a column already removed from state
- Drift between state and schema is harmless
- SeparateDatabaseAndState is generated by makemigrations for renames