diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index 00e04fd799..5f41053283 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -1,5 +1,6 @@ import functools import re +from collections import defaultdict from itertools import chain from django.conf import settings @@ -1213,6 +1214,8 @@ class MigrationAutodetector: def create_altered_indexes(self): option_name = operations.AddIndex.option_name + self.renamed_index_together_values = defaultdict(list) + for app_label, model_name in sorted(self.kept_model_keys): old_model_name = self.renamed_models.get( (app_label, model_name), model_name @@ -1242,6 +1245,43 @@ class MigrationAutodetector: renamed_indexes.append((old_index_name, new_index_name, None)) remove_from_added.append(new_index) remove_from_removed.append(old_index) + # Find index_together changed to indexes. + for ( + old_value, + new_value, + index_together_app_label, + index_together_model_name, + dependencies, + ) in self._get_altered_foo_together_operations( + operations.AlterIndexTogether.option_name + ): + if ( + app_label != index_together_app_label + or model_name != index_together_model_name + ): + continue + removed_values = old_value.difference(new_value) + for removed_index_together in removed_values: + renamed_index_together_indexes = [] + for new_index in added_indexes: + _, args, kwargs = new_index.deconstruct() + # Ensure only 'fields' are defined in the Index. + if ( + not args + and new_index.fields == list(removed_index_together) + and set(kwargs) == {"name", "fields"} + ): + renamed_index_together_indexes.append(new_index) + + if len(renamed_index_together_indexes) == 1: + renamed_index = renamed_index_together_indexes[0] + remove_from_added.append(renamed_index) + renamed_indexes.append( + (None, renamed_index.name, removed_index_together) + ) + self.renamed_index_together_values[ + index_together_app_label, index_together_model_name + ].append(removed_index_together) # Remove renamed indexes from the lists of added and removed # indexes. added_indexes = [ @@ -1439,6 +1479,13 @@ class MigrationAutodetector: model_name, dependencies, ) in self._get_altered_foo_together_operations(operation.option_name): + if operation == operations.AlterIndexTogether: + old_value = { + value + for value in old_value + if value + not in self.renamed_index_together_values[app_label, model_name] + } removal_value = new_value.intersection(old_value) if removal_value or old_value: self.add_operation( diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt index 7c9f1beca6..bd8af7d51c 100644 --- a/docs/releases/4.1.txt +++ b/docs/releases/4.1.txt @@ -355,6 +355,12 @@ Migrations ``RemoveIndex`` and ``AddIndex``, when renaming indexes defined in the :attr:`Meta.indexes `. +* The migrations autodetector now generates + :class:`~django.db.migrations.operations.RenameIndex` operations instead of + ``AlterIndexTogether`` and ``AddIndex``, when moving indexes defined in the + :attr:`Meta.index_together ` to the + :attr:`Meta.indexes `. + Models ~~~~~~ diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index a28477e590..547e0b32c5 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -2598,6 +2598,79 @@ class AutodetectorTests(TestCase): old_name="book_title_author_idx", ) + def test_rename_index_together_to_index(self): + changes = self.get_changes( + [self.author_empty, self.book_foo_together], + [self.author_empty, self.book_indexes], + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes( + changes, "otherapp", 0, ["RenameIndex", "AlterUniqueTogether"] + ) + self.assertOperationAttributes( + changes, + "otherapp", + 0, + 0, + model_name="book", + new_name="book_title_author_idx", + old_fields=("author", "title"), + ) + self.assertOperationAttributes( + changes, + "otherapp", + 0, + 1, + name="book", + unique_together=set(), + ) + + def test_rename_index_together_to_index_extra_options(self): + # Indexes with extra options don't match indexes in index_together. + book_partial_index = ModelState( + "otherapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("author", models.ForeignKey("testapp.Author", models.CASCADE)), + ("title", models.CharField(max_length=200)), + ], + { + "indexes": [ + models.Index( + fields=["author", "title"], + condition=models.Q(title__startswith="The"), + name="book_title_author_idx", + ) + ], + }, + ) + changes = self.get_changes( + [self.author_empty, self.book_foo_together], + [self.author_empty, book_partial_index], + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes( + changes, + "otherapp", + 0, + ["AlterUniqueTogether", "AlterIndexTogether", "AddIndex"], + ) + + def test_rename_index_together_to_index_order_fields(self): + # Indexes with reordered fields don't match indexes in index_together. + changes = self.get_changes( + [self.author_empty, self.book_foo_together], + [self.author_empty, self.book_unordered_indexes], + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes( + changes, + "otherapp", + 0, + ["AlterUniqueTogether", "AlterIndexTogether", "AddIndex"], + ) + def test_order_fields_indexes(self): """Test change detection of reordering of fields in indexes.""" changes = self.get_changes(