#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.