#36161: Deletion in reverse data migration fails with a chain of at least 3
foreign
keys
-----------------------------+--------------------------------------
Reporter: Enrico Zini | Type: Bug
Status: new | Component: Migrations
Version: 4.2 | 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
-----------------------------+--------------------------------------
We had a data migration that created an object, and deleted it on its
reverse side. Reversing the migration failed with an error like:
{{{
ValueError: Cannot query "Modelname object (1)": Must be "Modelname"
instance.
}}}
Similar code worked on a migration earlier in the migration history, and
bisecting which of the steps between them introduced the issue we
pinpointed it to something that looked completely unrelated.
This was a nasty issue to debug.
I put together a reproducer at https://github.com/spanezz/django-reverse-
migration-issue and Colin Watson came up with a workaround. We are not in
a position to be able to go as far as to propose a fix.
This looks very much like the problems encountered in these two
stackoverflow issues, still without a solution:
* https://stackoverflow.com/questions/69836002/historical-model-does-not-
allow-deletion-in-reverting-django-data-migration
* https://stackoverflow.com/questions/75249322/django-cant-delete-rows-
when-migrating-backwards
It could also be related to https://code.djangoproject.com/ticket/27737
When building the migration state, various methods in `ProjectState`
sometimes choose to delay rendering of relationships for non-relational
fields (apparently to improve migration performance). This causes
`ProjectState._find_reload_model` to use `get_related_models_tuples`
rather than `get_related_models_recursive` to determine which other models
need to be reloaded, which returns only models that are one relation step
away from the model being changed. `_find_reload_model` then follows one
more step (under the comment "For all direct related models recursively
get all related models"; note that "recursively" is not true if `delay` is
set), but stops there. This means that for models two relation steps
away, the migration state ends up with a type object as returned by
`apps.get_model`, while the foreign key information of a related model
(three relation steps from the model changed in the migration) contains an
equivalent, but different type object, which was instantiated in a
previous migration step.
While the two type objects point to the same model, they are different
objects and their `id()` values differ. The problem surfaces a lot lower
in the execution stack in
`django.db.models.query_utils.check_rel_lookup_compatibility`, invoked
indirectly by model deletion code, which tries to enforce that a model is
the same as the target of a foreign key:
{{{
def check_rel_lookup_compatibility(model, target_opts, field):
"""
Check that self.model is compatible with target_opts. Compatibility
is OK if:
1) model and opts match (where proxy inheritance is removed)
2) model is parent of opts' model or the other way around
"""
def check(opts):
# Uncomment this to get a breakpoint when the mismatch happens:
# if model._meta.label == opts.label and id(
# model._meta.concrete_model
# ) != id(opts.concrete_model):
# breakpoint()
return (
# This would be a work-around, but not a fix:
# (model._meta.app_label, model._meta.object_name)
# == (opts.app_label, opts.object_name)
model._meta.concrete_model == opts.concrete_model
or opts.concrete_model in model._meta.get_parent_list()
or model in opts.get_parent_list()
)
}}}
The workaround that we are currently using (see https://github.com/spanezz
/django-reverse-migration-issue/blob/main/db/workaround.py and
https://github.com/spanezz/django-reverse-migration-
issue/blob/main/db/migrations/0002_alter_models.py#L34 ) involves
introducing a migration operation that forces a reload of the affected
model.
This situation hints at some caching issue in the migration infrastructure
which we've been unable to follow up, but which surfaces in real world
scenarios and is extremely difficult to identify and handle when it does.
--
Ticket URL: <https://code.djangoproject.com/ticket/36161>
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 visit
https://groups.google.com/d/msgid/django-updates/01070194b7516c2d-5e910a6a-76e9-4ec0-b87d-e018477427b6-000000%40eu-central-1.amazonses.com.