Fix migration order for fields moved from parent to child

Ensure RemoveField on parent model runs before child CreateModel
to avoid FieldError when moving a field in model inheritance.

Fixes #21890.
This commit is contained in:
utkarsh.arya@zomato.com 2025-11-15 22:59:14 +00:00
parent 18759b2209
commit fdb4410ce7
2 changed files with 49 additions and 0 deletions

View file

@ -563,6 +563,24 @@ class MigrationAutodetector:
if isinstance(base, str) and "." in base:
base_app_label, base_name = base.split(".", 1)
dependencies.append((base_app_label, base_name, None, True))
# Check if any fields in this model exist in the parent model
# in the old state and need to be removed first (to avoid
# FieldError when a field is moved from parent to child).
if (base_app_label, base_name.lower()) in self.kept_model_keys:
old_base_model_state = self.from_state.models.get(
(base_app_label, base_name.lower())
)
if old_base_model_state:
for field_name in model_state.fields:
if field_name in old_base_model_state.fields:
# This field exists in the parent in the old state
# Depend on its removal from the parent
dependencies.append((
base_app_label,
base_name.lower(),
field_name,
False
))
# Depend on the other end of the primary key if it's a relation
if primary_key_rel:
dependencies.append((

View file

@ -2454,3 +2454,34 @@ class AutodetectorTests(TestCase):
self.assertNumberMigrations(changes, 'app', 1)
self.assertOperationTypes(changes, 'app', 0, ['DeleteModel'])
self.assertOperationAttributes(changes, 'app', 0, 0, name='Dog')
def test_create_model_with_field_removed_from_base_model(self):
"""
Tests autodetection of field being moved from parent model to child
model. The field removal from the parent should be ordered before the
child model creation to avoid FieldError.
"""
# Before: Readable model with title field
readable_with_field = ModelState('app', 'Readable', [
("id", models.AutoField(primary_key=True)),
("title", models.CharField(max_length=200)),
])
# After: Readable without title, Book subclass with title
readable_without_field = ModelState('app', 'Readable', [
("id", models.AutoField(primary_key=True)),
])
book = ModelState('app', 'Book', [
("readable_ptr", models.OneToOneField(
"app.Readable",
models.CASCADE,
parent_link=True,
primary_key=True,
auto_created=True,
)),
("title", models.CharField(max_length=200)),
], bases=('app.Readable',))
changes = self.get_changes([readable_with_field], [readable_without_field, book])
self.assertNumberMigrations(changes, 'app', 1)
self.assertOperationTypes(changes, 'app', 0, ['RemoveField', 'CreateModel'])
self.assertOperationAttributes(changes, 'app', 0, 0, model_name='readable', name='title')
self.assertOperationAttributes(changes, 'app', 0, 1, name='Book')