The Problem
When you remove a field from a Django model, there’s a dangerous gap during deployment: the migration runs first (dropping the column), but old code pods are still running that expect the field to exist. This causes ProgrammingError: column X does not exist.
The Solution: Two-Phase SeparateDatabaseAndState
Phase 1: Remove from Django, Keep in Database
Migration:
class Migration(migrations.Migration):
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.RemoveField(
model_name='mymodel',
name='field_to_remove',
),
],
database_operations=[
# Empty - don't touch database yet
],
),
]
Model:
class MyModel(models.Model):
# field_to_remove = models.CharField(...) - REMOVED
other_field = models.CharField(...)
Deploy this. Django stops querying the field, but it still exists in the database (harmless).
Phase 2: Drop the Database Column
Migration:
class Migration(migrations.Migration):
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
# Empty - state already correct
],
database_operations=[
migrations.RunSQL(
sql="ALTER TABLE myapp_mymodel DROP COLUMN field_to_remove;",
reverse_sql="ALTER TABLE myapp_mymodel ADD COLUMN field_to_remove VARCHAR(100);"
),
],
),
]
Deploy this to clean up the unused column.
Why This Works
- Phase 1: Django’s ORM stops selecting the field, so no code errors when the column eventually disappears
- Phase 2: Safe to drop the column because no code references it anymore
- No gap: There’s never a mismatch between what Django expects and what exists in the database