Opened 3 years ago

Last modified 3 years ago

#24572 new Bug

Migration plan may be broken when applying migrations on a clean database

Reported by: Markus Holtermann Owned by: nobody
Component: Migrations Version: master
Severity: Normal Keywords:
Cc: Florian Apolloner, Shai Berger Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

  1. python manage.py startapp a
  2. python manage.py startapp b

It is important that app a is in alphabetically ordering before app b

  1. Add both apps to INSTALLED_APPS

  1. Add a model to app a:
    from django.db import models
    
    
    class MyModel(models.Model):
        pass
    
  1. Add a model to app b:
    from django.db import models
    
    
    class MyModel(models.Model):
        pass
    
  1. python manage.py makemigrations

  1. Add a ForeignKey from b.MyModel to a.MyModel
    from django.db import models
    
    
    class MyModel(models.Model):
        a = models.ForeignKey('a.MyModel', null=True)
    
  1. python manage.py makemigrations

  1. Remove the ForeignKey on b.MyModel to a.MyModel
    from django.db import models
    
    
    class MyModel(models.Model):
        pass
    
  1. python manage.py makemigrations

  1. Remove a.MyModel
  2. python manage.py makemigrations

  1. python manage.py showmigrations --plan -v 3
    [ ]  a.0001_initial
    [ ]  a.0002_delete_mymodel ... (a.0001_initial)
    [ ]  b.0001_initial
    [ ]  b.0002_mymodel_a ... (a.0001_initial, b.0001_initial)
    [ ]  b.0003_remove_mymodel_a ... (b.0002_mymodel_a)
    
  2. $ python manage.py migrate
    Operations to perform:
      Apply all migrations: a, b
    Running migrations:
      Rendering model states... DONE
      Applying a.0001_initial... OK
      Applying a.0002_delete_mymodel... OK
      Applying b.0001_initial... OK
      Applying b.0002_mymodel_a...
    
    Traceback (most recent call last):
      File "manage.py", line 10, in <module>
        execute_from_command_line(sys.argv)
      File "/home/markus/Coding/django/django/core/management/__init__.py", line 330, in execute_from_command_line
        utility.execute()
      File "/home/markus/Coding/django/django/core/management/__init__.py", line 322, in execute
        self.fetch_command(subcommand).run_from_argv(self.argv)
      File "/home/markus/Coding/django/django/core/management/base.py", line 347, in run_from_argv
        self.execute(*args, **cmd_options)
      File "/home/markus/Coding/django/django/core/management/base.py", line 398, in execute
        output = self.handle(*args, **options)
      File "/home/markus/Coding/django/django/core/management/commands/migrate.py", line 195, in handle
        executor.migrate(targets, plan, fake=fake, fake_initial=fake_initial)
      File "/home/markus/Coding/django/django/db/migrations/executor.py", line 110, in migrate
        self.apply_migration(states[migration], migration, fake=fake, fake_initial=fake_initial)
      File "/home/markus/Coding/django/django/db/migrations/executor.py", line 147, in apply_migration
        state = migration.apply(state, schema_editor)
      File "/home/markus/Coding/django/django/db/migrations/migration.py", line 118, in apply
        operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
      File "/home/markus/Coding/django/django/db/migrations/operations/fields.py", line 62, in database_forwards
        field,
      File "/home/markus/Coding/django/django/db/backends/sqlite3/schema.py", line 220, in add_field
        self._remake_table(model, create_fields=[field])
      File "/home/markus/Coding/django/django/db/backends/sqlite3/schema.py", line 179, in _remake_table
        self.create_model(temp_model)
      File "/home/markus/Coding/django/django/db/backends/base/schema.py", line 231, in create_model
        definition, extra_params = self.column_sql(model, field)
      File "/home/markus/Coding/django/django/db/backends/base/schema.py", line 130, in column_sql
        db_params = field.db_parameters(connection=self.connection)
      File "/home/markus/Coding/django/django/db/models/fields/related.py", line 2052, in db_parameters
        return {"type": self.db_type(connection), "check": []}
      File "/home/markus/Coding/django/django/db/models/fields/related.py", line 2043, in db_type
        rel_field = self.target_field
      File "/home/markus/Coding/django/django/db/models/fields/related.py", line 1946, in target_field
        return self.foreign_related_fields[0]
      File "/home/markus/Coding/django/django/db/models/fields/related.py", line 1704, in foreign_related_fields
        return tuple(rhs_field for lhs_field, rhs_field in self.related_fields)
      File "/home/markus/Coding/django/django/db/models/fields/related.py", line 1691, in related_fields
        self._related_fields = self.resolve_related_fields()
      File "/home/markus/Coding/django/django/db/models/fields/related.py", line 1676, in resolve_related_fields
        raise ValueError('Related model %r cannot be resolved' % self.remote_field.model)
    ValueError: Related model 'a.MyModel' cannot be resolved
    

The problem is that the migration a.0002_delete_mymodel doesn't have any information that it must be applied after b.0003_remove_mymodel_a which in turn removes any references to a.MyModel again.

Change History (5)

comment:1 Changed 3 years ago by Markus Holtermann

I created a demo project to demonstrate the problem: https://github.com/MarkusH/django-ticket-triage/compare/master...ticket24572

comment:2 Changed 3 years ago by Marten Kenbeek

Triage Stage: UnreviewedAccepted

There's more to this than just the fact that a.2 requires b.3 but b.3 is not a dependency of a.2. Even if a.2 exists but is not run, the above error will be thrown when migrating to b.2. Git bisect shows that this particular error is a regression from 1aa3e09c2043c88a760e8b73fb95dc8f1ffef50e.

Fixing the regressions from that commit at least opens up an easy workaround that doesn't involve deleting migration files. A simple manage.py migrate b followed by manage.py migrate a will work.

comment:3 Changed 3 years ago by Markus Holtermann

Actually it is bbbed99f6260a8b3e65cb990e49721b1ce4a441b. Which also makes way more sense, as this does use a.2 to generate the initial state right before each migration that needs to be applied. See https://github.com/django/django/blob/3e7d9d05ac5efff4e4732c3453c7a8ef502d0ed0/django/db/migrations/executor.py#L65 for details.

comment:4 Changed 3 years ago by Florian Apolloner

Cc: Florian Apolloner added

comment:5 Changed 3 years ago by Shai Berger

Cc: Shai Berger added

The right workaround is to add the dependency manually.

Adding the dependency automatically would require keeping close track of the dependency at the model and field level.

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