#36959: Model bases isn't updated when changing parent classes
-------------------------------------+-------------------------------------
     Reporter:  Timothy Schilling    |                     Type:
                                     |  Uncategorized
       Status:  new                  |                Component:
                                     |  Migrations
      Version:                       |                 Severity:  Normal
     Keywords:                       |             Triage Stage:
                                     |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------
 There appears to be a bug when removing multi-table inheritance. The
 majority of this ticket is covered in [https://www.better-
 simple.com/django/2025/03/19/model-bases-in-migrations/ this blog post],
 but I'm pulling out the relevant bits. If things don't make total sense,
 it's possible I removed too much context.

 When removing multi-table inheritance by changing a child model's parent
 from a concrete model to an abstract model (or `models.Model` directly),
 `makemigrations` does not generate a migration operation to update the
 `bases` attribute in the migration state.

 **Steps to reproduce:**

 1. Start with these models

 {{{
 class MyBaseModel(models.Model):
     value = models.IntegerField()

 class MyChildModel(MyBaseModel):
     pass
 }}}

 Make migrations and apply them.

 2. Change the models to:

 {{{
 class AbstractBase(models.Model):
     class Meta:
         abstract = True

     value = models.IntegerField(null=True)

 class MyBaseModel(models.Model):
     pass

 class MyChildModel(AbstractBase):
     pass
 }}}

 Make migrations and apply them. You'll get the error:
 {{{
 django.core.exceptions.FieldError: Local field 'id' in class
 'MyChildModel' clashes with field of the same name from base class
 'MyBaseModel'.
 }}}

 This is because there is no operation to update `bases` for `MyChildModel`
 from `("myapp.mybasemodel",)` to `(models.Model,)`.


 **Expected Behavior**
 `makemigrations` should detect that a model's bases have changed and
 generate an appropriate migration operation (similar to
 `AlterModelOptions`maybe?) to update `bases`.

 **Workaround**
 Thanks to [https://stackoverflow.com/a/67500550/1637351 Andrii on
 StackOverflow], there's a workaround for people to create their own
 operation to change the bases.

 {{{
 from django.db.migrations.operations.models import ModelOptionOperation

 class SetModelBasesOptionOperation(ModelOptionOperation):
     """
     A migration operation that updates the bases of a model.
     This can be used to separate a model from its parent. Specifically
     when multi-table inheritance is used.
     """
     def __init__(self, name, bases):
         super().__init__(name)
         self.bases = bases

     def deconstruct(self):
         return (self.__class__.__qualname__, [], {"bases": self.bases})

     def state_forwards(self, app_label, state):
         model_state = state.models[app_label, self.name_lower]
         model_state.bases = self.bases
         state.reload_model(app_label, self.name_lower, delay=True)

     def database_forwards(self, app_label, schema_editor, from_state,
 to_state):
         pass

     def database_backwards(self, app_label, schema_editor, from_state,
 to_state):
         pass

     def describe(self):
         return "Update bases of the model %s" % self.name

     @property
     def migration_name_fragment(self):
         return "set_%s_bases" % self.name_lower
 }}}

 Which would require the earlier migration's `operations` that fails to be
 updated to look like:

 {{{
 operations = [
     migrations.RemoveField(
         model_name='mybasemodel',
         name='value',
     ),
     migrations.RemoveField(
         model_name='mychildmodel',
         name='mybasemodel_ptr',
     ),
     SetModelBasesOptionOperation("mychildmodel", (models.Model, )),
     migrations.AddField(
         model_name='mychildmodel',
         name='id',
         field=models.AutoField(auto_created=True, primary_key=True,
 serialize=False, verbose_name='ID'),
     ),
     migrations.AddField(
         model_name='mychildmodel',
         name='value',
         field=models.IntegerField(null=True),
     ),
 ]

 }}}
-- 
Ticket URL: <https://code.djangoproject.com/ticket/36959>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/0107019c9f1d3325-99aa3ec3-2e1e-4325-886c-40d44901ddfe-000000%40eu-central-1.amazonses.com.

Reply via email to