Opened 2 months ago

Last modified 2 months ago

#35595 assigned Bug

Generated migration for removing related models can't migrate backwards

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

Description

The migration that's created when removing two related models, results in a migration that can't be migrated backwards. However, this is only the case when the model with the foreign key has a Meta.indexes specified that references the foreign key field.

Given the following models:

class Animal(models.Model):
    name = models.CharField(max_length=100)


class Dog(models.Model):
    name = models.CharField(max_length=100)
    animal = models.ForeignKey(Animal, on_delete=models.CASCADE)

    class Meta:
        indexes = [models.Index(fields=("animal", "name"))]

Doing the following results in the error at the end.

  1. python manage.py makemigrations [app] -> 0001_initial.py (in my test it was the second migration, in case you can't reproduce it)
  2. Remove both models
  3. python manage.py makemigrations [app] -> 0002_remove.py
  4. python manage.py sqlmigrate [app] 0002 --backwards

Error:

❯ ./manage.py sqlmigrate endpoint 0003 --backwards
Traceback (most recent call last):
  File "site-packages/django/db/models/options.py", line 681, in get_field
    return self.fields_map[field_name]
           ~~~~~~~~~~~~~~~^^^^^^^^^^^^
KeyError: 'animal'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/schillingt/Projects/aspiredu/./manage.py", line 11, in <module>
    execute_from_command_line(sys.argv)
  File "site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "site-packages/django/core/management/__init__.py", line 436, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "site-packages/django/core/management/base.py", line 413, in run_from_argv
    self.execute(*args, **cmd_options)
  File "site-packages/django/core/management/commands/sqlmigrate.py", line 38, in execute
    return super().execute(*args, **options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/django/core/management/base.py", line 459, in execute
    output = self.handle(*args, **options)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/django/core/management/commands/sqlmigrate.py", line 80, in handle
    sql_statements = loader.collect_sql(plan)
                     ^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/django/db/migrations/loader.py", line 383, in collect_sql
    state = migration.unapply(state, schema_editor, collect_sql=True)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/django/db/migrations/migration.py", line 193, in unapply
    operation.database_backwards(
  File "site-packages/django/db/migrations/operations/models.py", line 394, in database_backwards
    schema_editor.create_model(model)
  File "site-packages/django/db/backends/base/schema.py", line 513, in create_model
    self.deferred_sql.extend(self._model_indexes_sql(model))
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/django/db/backends/base/schema.py", line 1624, in _model_indexes_sql
    output.append(index.create_sql(model, self))
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/django/db/models/indexes.py", line 112, in create_sql
    model._meta.get_field(field_name)
  File "site-packages/django/db/models/options.py", line 683, in get_field
    raise FieldDoesNotExist(
django.core.exceptions.FieldDoesNotExist: Dog has no field named 'animal'

The generated migrations for me are:

0001:

operations = [
    migrations.CreateModel(
        name="Animal",
        fields=[
            (
                "id",
                models.AutoField(
                    auto_created=True,
                    primary_key=True,
                    serialize=False,
                    verbose_name="ID",
                ),
            ),
            ("name", models.CharField(max_length=100)),
        ],
    ),
    migrations.CreateModel(
        name="Dog",
        fields=[
            (
                "id",
                models.AutoField(
                    auto_created=True,
                    primary_key=True,
                    serialize=False,
                    verbose_name="ID",
                ),
            ),
            ("name", models.CharField(max_length=100)),
            (
                "animal",
                models.ForeignKey(
                    on_delete=django.db.models.deletion.CASCADE,
                    to="[app].animal",
                ),
            ),
        ],
        options={
            "indexes": [
                models.Index(
                    fields=["animal", "name"], name="[app]_do_animal__b82a2f_idx"
                )
            ],
        },
    ),
]

0002:

operations = [
    migrations.RemoveField(
        model_name="dog",
        name="animal",
    ),
    migrations.DeleteModel(
        name="Animal",
    ),
    migrations.DeleteModel(
        name="Dog",
    ),
]

My environment is:

  • Django 5.0.6
  • Postgres 15
  • python 3.12.4
  • M1 mac

Removing the Meta.indexes results in a generated migration that can be migrated backwards.

This appears by solveable in at least two ways:

  1. Adding the following operation to the top of the removal migration solves the problem.
migrations.RemoveIndex(
    model_name="dog",
    name="[app]_do_animal__b82a2f_idx",
),
  1. Removing the RemoveField operation from the removal migration and rearranging the models.
operations = [
    migrations.DeleteModel(
        name="Dog",
    ),
    migrations.DeleteModel(
        name="Animal",
    ),
]

Change History (2)

comment:1 by Sarah Boyce, 2 months ago

Triage Stage: UnreviewedAccepted
Type: UncategorizedBug

Thank you for the report! Replicated on main 👍

comment:2 by Akash Kumar Sen, 2 months ago

Owner: set to Akash Kumar Sen
Status: newassigned
Note: See TracTickets for help on using tickets.
Back to Top