#36878: Migration's ModelState has varying type for unique_together and
index_together options causing autodetector crash
-------------------------------------+-------------------------------------
Reporter: Markus | Owner: Markus Holtermann
Holtermann |
Type: | Status: assigned
Uncategorized |
Component: | Version: dev
Migrations |
Severity: Normal | Keywords:
Triage Stage: | Has patch: 0
Unreviewed |
Needs documentation: 0 | Needs tests: 0
Patch needs improvement: 0 | Easy pickings: 0
UI/UX: 0 |
-------------------------------------+-------------------------------------
When leveraging the `MigrationAutodetector` to get the changes between two
project states, the following exception is raised (on 5.2, but the same
applies to the current master at commit
b1ffa9a9d78b0c2c5ad6ed5a1d84e380d5cfd010):
{{{#!python
Traceback (most recent call last):
File "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "src/manage.py", line 67, in <module>
management.execute_from_command_line(sys.argv)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
File ".venv/lib/python3.13/site-
packages/django/core/management/__init__.py", line 442, in
execute_from_command_line
utility.execute()
~~~~~~~~~~~~~~~^^
File ".venv/lib/python3.13/site-
packages/django/core/management/__init__.py", line 436, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File ".venv/lib/python3.13/site-
packages/django/core/management/base.py", line 420, in run_from_argv
self.execute(*args, **cmd_options)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
File ".venv/lib/python3.13/site-
packages/django/core/management/base.py", line 464, in execute
output = self.handle(*args, **options)
File "src/core/management/commands/some_command.py", line 94, in handle
self._get_migrations_and_operations(section)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
File "src/core/management/commands/some_command.py", line 238, in
_get_migrations_and_operations
new_migrations = autodetector.changes(self.graph,
trim_to_apps={"affiliates"})
File ".venv/lib/python3.13/site-
packages/django/db/migrations/autodetector.py", line 67, in changes
changes = self._detect_changes(convert_apps, graph)
File ".venv/lib/python3.13/site-
packages/django/db/migrations/autodetector.py", line 213, in
_detect_changes
self.generate_removed_altered_unique_together()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File ".venv/lib/python3.13/site-
packages/django/db/migrations/autodetector.py", line 1718, in
generate_removed_altered_unique_together
self._generate_removed_altered_foo_together(operations.AlterUniqueTogether)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".venv/lib/python3.13/site-
packages/django/db/migrations/autodetector.py", line 1699, in
_generate_removed_altered_foo_together
) in self._get_altered_foo_together_operations(operation.option_name):
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
File ".venv/lib/python3.13/site-
packages/django/db/migrations/autodetector.py", line 1668, in
_get_altered_foo_together_operations
new_value = set(new_value) if new_value else set()
~~~^^^^^^^^^^^
TypeError: unhashable type: 'list'
}}}
Reason:
The migration `ModelState` tracks the changes for all options
(`order_with_respect_to`, `unique_together`, `indexes`, ...) in its
`options` attribute, which is a simple dict. For most keys inside the
dict, the value is just a `list` of something. However, for
`unique_together` and `index_together`, the value should be a set of
tuples
([
https://github.com/django/django/blob/b1ffa9a9d78b0c2c5ad6ed5a1d84e380d5cfd010/django/db/migrations/state.py#L841
see the ModelState.from_model() method]).
Unfortunately, there are situations inside the `ProjectState`'s mutation
functions
([
https://github.com/django/django/blob/b1ffa9a9d78b0c2c5ad6ed5a1d84e380d5cfd010/django/db/migrations/state.py#L340-L345
e.g. rename_field()]) where the data type for
`model_state.options["unique_together"]` is changed to `list[list[str]]`:
{{{#!python
for option in ("index_together", "unique_together"):
if option in options:
options[option] = [
[new_name if n == old_name else n for n in together]
for together in options[option]
]
}}}
--
Ticket URL: <
https://code.djangoproject.com/ticket/36878>
Django <
https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.