From fdb4410ce77a136fa241f97230821bb3238dc436 Mon Sep 17 00:00:00 2001 From: "utkarsh.arya@zomato.com" Date: Sat, 15 Nov 2025 22:59:14 +0000 Subject: [PATCH] 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. --- django/db/migrations/autodetector.py | 18 ++++++++++++++++ tests/migrations/test_autodetector.py | 31 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index 30616c0172..30afd20155 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -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(( diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index daff57fc2c..760e9023d4 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -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')