#37495 new Bug

makemigrations generates wrong dependencies when a new model has an FK to a renamed model in a different app

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

Description

When adding a new model with a ForeignKey to a model that is also being renamed in a different app, makemigrations generates dependencies that put the new model with a FK to renamed model's new name before renaming the referenced model and migrate fails.

  1. App shop has class T0(models.Model): pass. Run makemigrations.
    Migrations for 'shop':
      shop/migrations/0001_initial.py
        + Create model T0
    
  2. Rename T0 to T0r, and add app zeta with:
    class T1(models.Model):
        t0 = models.ForeignKey("shop.T0r", models.CASCADE)
    
  3. Run makemigrations and answer yes to the rename.
    Migrations for 'zeta':
      zeta/migrations/0001_initial.py
        + Create model T1
    Migrations for 'shop':
      shop/migrations/0002_rename_t0_t0r.py
        ~ Rename model T0 to T0r
    
  4. Run migrate.

The dependencies come out backwards. shop/0002_rename_t0_t0r depends on ('zeta', '0001_initial'), and zeta/0001_initial depends only on ('shop', '0001_initial') even though its foreign key points at shop.t0r:

Applying zeta.0001_initial...
ValueError: Related model 'shop.t0r' cannot be resolved

This is a regression in Django 2.0 introduced by #28493's change to generate_renamed_models(). The same steps work on 1.11 and fail on 2.0 and later. This is essentially the reverse structure of #28493: its foreign key used the old name and had to run before the rename. Here it uses the new name and has to run after.

Changing the dependency in zeta.0001_initial to shop.0002_rename_t0_t0r and removing the zeta.0001_initial dependency from shop.0002_rename_t0_t0r makes it work. That's the order generated by Django 1.11, and the only order that works migrating forwards and back.

I haven't had time to investigate further, but I played around a little bit with this patch and it appears to work:

  • django/db/migrations/autodetector.py

    diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py
    index 907988d..df0a9ef 100644
    a b class MigrationAutodetector:  
    596596                            rem_model_state, model_state
    597597                        ):
    598598                            dependencies = []
     599                            # A relation to the old name must be created
     600                            # before the rename. A relation added in this run
     601                            # refers to the new name and is created after it.
     602                            old_relations = self.from_state.relations.get(
     603                                (rem_app_label, rem_model_name), {}
     604                            )
    599605                            fields = list(model_state.fields.values()) + [
    600606                                field.remote_field
    601                                 for relations in self.to_state.relations[
     607                                for related_key, relations in self.to_state.relations[
    602608                                    app_label, model_name
    603                                 ].values()
    604                                 for field in relations.values()
     609                                ].items()
     610                                for field_name, field in relations.items()
     611                                if field_name in old_relations.get(related_key, {})
    605612                            ]
    606613                            for field in fields:
    607614                                if field.is_relation:

I found this with the same fuzzer as #37493, which tests a migration extension that manages database views with Django migrations. Its random changes include renaming a model and adding a model with a foreign key to an existing one, across multiple apps. One change it produced failed to migrate. I then produced a minimal repro using Django alone.

This looks semi-related to #37477, but the proposed patch only applies to changes in a single app at a time.

Change History (0)

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