- Notifications
You must be signed in to change notification settings - Fork 135
Description
When a field is renamed via RenameField AND subsequently altered (type change or nullability change) via AlterField in the same migration, indexes defined in Meta.indexes that include the field are dropped but not restored. Depending on the specific scenario, a FieldDoesNotExist exception may also be raised.
This was identified during the work on #486 / #498 and is documented with @expectedFailure tests in that PR.
This bug was deliberately not addressed in #498 because the fix there targets the common case i.e. altering a field without renaming it. The rename + alter combination involves a different problem: a mismatch between the model state (where RenameField has already updated the field name) and the index metadata (where index.fields still references the old name). Fixing this requires changes to how _delete_indexes() resolves field names and how the restoration guard handles column renames, both of which are orthogonal to the Meta.indexes restoration logic added in #498. Bundling both fixes together would have broadened the scope and risk of that PR.
Root Cause
There are two separate failures at play:
1. _delete_indexes() raises FieldDoesNotExist
When AlterField calls _delete_indexes(), it iterates over model._meta.indexes and calls model._meta.get_field(field_name) for each field in the index:
# mssql/schema.py, _delete_indexes() for index in model._meta.indexes: columns = [model._meta.get_field(field).column for field in index.fields]However, RenameField has already updated the model state — the field is now known by its new name (e.g., a_renamed), but index.fields still references the old name (e.g., a). The get_field('a') call raises FieldDoesNotExist.
2. Restoration phase is skipped for renamed columns
Even if the drop phase were to succeed, the restoration block in _alter_field is guarded by:
if (old_type != new_type or (old_field.null != new_field.null)) and ( old_field.column == new_field.column # column rename is handled separately above ):When a field has been renamed, old_field.column != new_field.column, so the entire restoration block is skipped. The comment says "column rename is handled separately above", referring to the sp_rename code path — but that path only renames the column itself; it does not drop and recreate indexes that reference the column.
Repoduction steps:
Create a model with a Meta.indexes index, then run a migration that renames a field and alters it:
# Migration 1: Create model with index operations = [ migrations.CreateModel( name='MyModel', fields=[ ('id', models.AutoField(primary_key=True)), ('a', models.CharField(max_length=20)), ('b', models.CharField(max_length=20)), ], ), migrations.AddIndex( model_name='mymodel', index=models.Index(fields=['a', 'b'], name='idx_ab'), ), ] # Migration 2: Rename field and change its type operations = [ migrations.RenameField( model_name='mymodel', old_name='a', new_name='a_renamed', ), migrations.AlterField( model_name='mymodel', name='a_renamed', field=models.CharField(max_length=40), # type change ), ]After running both migrations, the index idx_ab will be missing from the database.
The same issue occurs when changing nullability instead of type:
migrations.AlterField( model_name='mymodel', name='a_renamed', field=models.CharField(max_length=20, null=True), # nullability change ),Existing Tests
The following @expectedFailure tests in testapp/tests/test_indexes.py (added in #498) exercise this bug:
test_index_from_meta_indexes_retained_after_rename_and_type_changetest_index_from_meta_indexes_retained_after_rename_and_nullability_change
Workaround
Split the rename and alter into separate migrations so that the rename is fully committed before the alter runs. This avoids the stale field name in model state and ensures old_field.column == new_field.column during the alter.