﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
36878	Migration's ModelState has varying type for unique_together and index_together options causing autodetector crash	Markus Holtermann	Markus Holtermann	"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]
                ]
}}}
"	Bug	closed	Migrations	dev	Normal	fixed		Lily	Ready for checkin	1	0	0	0	0	0
