Backwards compatible migrations

Hi all,

I’m curious to know how the community are tackling backwards compatible migrations to enable stuff like blue-green deployments.

From my understanding the workflow for the common operations of adding, deleting and renaming columns is something like this:

Adding a columns

Adding a column can be made in one release if the column is nullable or have a default value with the caveat that it has to be a database default and not a Django ORM-default. This can be solved by overriding the original migration and using RunSQL:

RunSQL(
    ‘ALTER TABLE foo ADD COLUMN quantity int DEFAULT 1 NOT NULL’,
    ‘ALTER TABLE foo DROP COLUMN quantity’,
    state_operations=[
        migrations.AddField(
            model_name=‘foo’,
            name=‘quantity’,
            field=models.IntegerField(default=1, null=False),
        ),
    ],
)

I’m not sure if SeparateDatabaseAndState could / should be used instead of RunSQL.

Adding NOT NULL columns where a database default isn’t applicable and a data migration is needed can be made in one release if made in three steps in the same migration. First add the column as nullable, run the data migration and then set the column as NOT NULL. This also requires the migration to be run in one transaction with atomic = True (not supported by all backends). Otherwise it should be done in two releases where the last release sets the column as NOT NULL.

Deleting columns

Deleting a column is made in two releases:

  1. Make the field nullable and set it as editable=False and remove all usage of it while keeping the field in the model.
  2. Delete the field.

Renaming columns

Renaming a column is made in three releases:

  1. Add the field with the new name as nullable or with a database default as described in “Adding columns”. Update any usage of the old field to also write value to the new field.
  2. Run a data migration to copy values to the new field if not set. Make the old field nullable and set it as editable=False and remove all usage of it while keeping the field in the model. The new field can now be set as NOT NULL.
  3. Delete the old field.

Dropping tables

Dropping a table is made in two releases:

  1. Remove all usage of the table to be removed.
  2. Delete the table.

Note that I’m assuming that the database is PostgreSQL >= 11 where database defaults can be added for new columns without rewriting the table.

Am I missing something or do you folks have any recommendations for making these processes as smooth as possible? One part that feels a bit sketchy is keeping a field that is pending deletion on the model. It would be quite easy to miss a place in the code where that field still is used.

1 Like

Hi @hannseman !

Migrations, and databases in general, are “my thing.” I find I’m often called in on projects to run zero downtime migrations. I’ve actually started compiling a guide and am looking into teaching a course about running such migrations.

This is a great post and summarizes some of the basic workflows.

  • Adding columns - your example should work well. RunSQL is perfectly fine to use over SeparateDatabaseAndState in this case. As for NOT NULL, you nearly always want three migrations, at scale. Using an data migration inside an atomic migration is not normally a good idea as you will lock up the whole table. Additionally setting the column to NOT NULL will break a running old version of the site until you deploy new code that populates the field.
  • Deleting columns - your approach will work, apart from during the period between migrating and deploying the new code. Keeping the field in the model makes Django query for it every time it touches the table: SELECT ..., old_field FROM table. This means that the site will be broken until the new code is out. A zero downtime approach is to drop the field from the model, generate the migration, but split it into two - one that does the DropField in a SeparateDatabaseAndState.state_operations , and a second that does it in a SeparateDatabaseAndState.database_operations. Deploy the first, check things are all okay, then deploy the second.
  • Renaming columns - your approach can be done in the database through SQL views. With enough code it gets nigh impossible to chase through all the use of one field over the other in Django - model instances, update queries, bulk_creates, model forms… If you create a view pointing at the table with just the columns renamed, PostgreSQL will allow inserts/updates to propagate through to the table. Then you can point the model at the view, and then drop/swap the view for the table in a migration.
  • Dropping tables - spot on.

Note that I’m assuming that the database is PostgreSQL >= 11 where database defaults can be added for new columns without rewriting the table.

It’s often not a problem to rewrite the table - the problems come when you add locks. I’ve sat through migrations rewriting tables for >20 hours and it’s fine.

Am I missing something or do you folks have any recommendations for making these processes as smooth as possible? One part that feels a bit sketchy is keeping a field that is pending deletion on the model. It would be quite easy to miss a place in the code where that field still is used.

2 Likes

Thanks @adamchainz for your comments and sorry for the very late reply.

I’ve actually started compiling a guide and am looking into teaching a course about running such migrations.

I’m looking forward to this, I’ll keep an eye out!