Opened 90 minutes ago

Closed 76 minutes ago

Last modified 68 minutes ago

#36959 closed Bug (duplicate)

Model bases isn't updated when changing parent classes

Reported by: Timothy Schilling Owned by:
Component: Migrations Version:
Severity: Normal Keywords: mti
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

There appears to be a bug when removing multi-table inheritance. The majority of this ticket is covered in 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.

  1. 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 AlterModelOptionsmaybe?) to update bases.

Workaround
Thanks to 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),
    ),
]

Change History (2)

comment:1 by Jacob Walls, 76 minutes ago

Keywords: mti added
Resolution: duplicate
Status: newclosed
Type: UncategorizedBug

Thanks for the clear write-up. I think it can be handled as part of #23521.

comment:2 by Timothy Schilling, 68 minutes ago

Womp womp. Thank you!

Note: See TracTickets for help on using tickets.
Back to Top