#35652: Unapplying a data migration that removes data fails with relations
-----------------------------------+--------------------------------------
     Reporter:  Timothy Schilling  |                     Type:  Bug
       Status:  new                |                Component:  Migrations
      Version:  5.0                |                 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
-----------------------------------+--------------------------------------
 I believe there's a bug in the plan logic for the `MigrationExecutor`
 class. When a related model has been removed in an earlier migration, a
 later data migration that removes data from a model will attempt to run a
 query to collect/delete data from the related model. Unfortunately, that
 related model's table has been removed from the database.

 Given the following app and model structure:

 {{{

 # App A

 class A(models.Model):
     name = models.CharField()


 # App B

 class B(models.Model):
     a = models.ForeignKey(A, on_delete=models.CASCADE)
 }}}



 1. Create migrations
 2. Remove model `B`, create migration
 3. Apply all migrations forwards
 4. `python manage.py makemigrations a --empty --name create_data`

 {{{

     def create_data(apps, schema_editor):
         A = apps.get_model('a', 'A')
         A.objects.create(name='test')

     def remove_data(apps, schema_editor):
         A = apps.get_model('a', 'A')
         A.objects.filter(name='test').delete()

     # In migration class
     operations = [migrations.RunPython(create_data, remove_data)]
 }}}


 5. Attempt to reverse back to A 0001 (`python manage.py migrate a 0001`)

 It should break on the reverse of A 0002_create_data, because it will
 attempt to run a query to delete related `B` instances, but that table has
 been removed.

 It appears that the migration app state isn't removing the `B` model.


 I was able to track this down to at least the
 `MigrationExecutor.migration_plan` method returning a full plan that puts
 the all app A migrations before the second app B migration.

 I found that the RemoveModel operation mutates the state properly, but
 that `MigrationExecturor._migrate_all_backwards` is using a different
 state from `states[migration]`. That state is from the `full_plan` that
 gets passed in which comes from `MigrationExectutor.migration_plan`.


 Interestingly enough, if we change this part of the code:
 
https://github.com/django/django/blob/main/django/db/migrations/executor.py#L195-L214

 To


 {{{
 for migration, _ in full_plan:
     if not migrations_to_run:
         # We remove every migration that we applied from this set so
         # that we can bail out once the last migration has been applied
         # and don't always run until the very end of the migration
         # process.
         break
     if migration not in migrations_to_run and migration in
 applied_migrations:
         # Only mutate the state if the migration is actually applied
         # to make sure the resulting state doesn't include changes
         # from unrelated migrations.
         migration.mutate_state(state, preserve=False)
 for migration, _ in full_plan:
     if not migrations_to_run:
         # We remove every migration that we applied from this set so
         # that we can bail out once the last migration has been applied
         # and don't always run until the very end of the migration
         # process.
         break
     if migration in migrations_to_run:
         if "apps" not in state.__dict__:
             state.apps  # Render all -- performance critical
         # The state before this migration
         states[migration] = state
         # The old state keeps as-is, we continue with the new state
         state = migration.mutate_state(state, preserve=True)
         migrations_to_run.remove(migration)
 }}}


 It unapply successfully because all the `applied_migrations` are mutating
 the state before the `migrations_to_run` stores any state.


 Note: I haven't reproduced this on a fresh project.
-- 
Ticket URL: <https://code.djangoproject.com/ticket/35652>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-updates+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/010701910e8b0a95-9792863c-12f3-4c13-a450-5fbfb2ceabcc-000000%40eu-central-1.amazonses.com.

Reply via email to