#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 [email protected].
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