diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py index b89b6b511bb..9c5bd5fbcb3 100644 --- a/django/db/migrations/operations/models.py +++ b/django/db/migrations/operations/models.py @@ -303,6 +303,71 @@ class CreateModel(ModelOperation): managers=self.managers, ), ] + elif ( + isinstance(operation, IndexOperation) + and self.name_lower == operation.model_name_lower + ): + if isinstance(operation, AddIndex): + return [ + CreateModel( + self.name, + fields=self.fields, + options={ + **self.options, + "indexes": [ + *self.options.get("indexes", []), + operation.index, + ], + }, + bases=self.bases, + managers=self.managers, + ), + ] + elif isinstance(operation, RemoveIndex): + options_indexes = [ + index + for index in self.options.get("indexes", []) + if index.name != operation.name + ] + return [ + CreateModel( + self.name, + fields=self.fields, + options={ + **self.options, + "indexes": options_indexes, + }, + bases=self.bases, + managers=self.managers, + ), + ] + elif isinstance(operation, RenameIndex) and operation.old_fields: + options_index_together = { + fields + for fields in self.options.get("index_together", []) + if fields != operation.old_fields + } + if options_index_together: + self.options["index_together"] = options_index_together + else: + self.options.pop("index_together", None) + return [ + CreateModel( + self.name, + fields=self.fields, + options={ + **self.options, + "indexes": [ + *self.options.get("indexes", []), + models.Index( + fields=operation.old_fields, name=operation.new_name + ), + ], + }, + bases=self.bases, + managers=self.managers, + ), + ] return super().reduce(operation, app_label) diff --git a/docs/releases/4.2.1.txt b/docs/releases/4.2.1.txt index a1130effe82..bed64f6ad1e 100644 --- a/docs/releases/4.2.1.txt +++ b/docs/releases/4.2.1.txt @@ -53,3 +53,8 @@ Bugfixes * Fixed a regression in Django 4.2 where breadcrumbs didn't appear on admin site app index views (:ticket:`34512`). + +* Made squashing migrations reduce ``AddIndex``, ``RemoveIndex``, + ``RenameIndex``, and ``CreateModel`` operations which allows removing a + deprecated ``Meta.index_together`` option from historical migrations and use + ``Meta.indexes`` instead (:ticket:`34525`). diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index d923be55b86..7d7848708de 100644 --- a/docs/releases/4.2.txt +++ b/docs/releases/4.2.txt @@ -502,7 +502,8 @@ Should become:: Running the :djadmin:`makemigrations` command will generate a migration containing a :class:`~django.db.migrations.operations.RenameIndex` operation -which will rename the existing index. +which will rename the existing index. Next, consider squashing migrations to +remove ``index_together`` from historical migrations. The ``AlterIndexTogether`` migration operation is now officially supported only for pre-Django 4.2 migration files. For backward compatibility reasons, it's diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index 44923a3c2e2..ee199fea68f 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -2266,10 +2266,9 @@ class AutodetectorTests(BaseAutodetectorTests): changes, "eggs", 0, - ["CreateModel", "CreateModel", "AddIndex", "AlterUniqueTogether"], + ["CreateModel", "CreateModel"], ) self.assertNotIn("unique_together", changes["eggs"][0].operations[0].options) - self.assertNotIn("unique_together", changes["eggs"][0].operations[1].options) self.assertMigrationDependencies(changes, "eggs", 0, []) def test_alter_db_table_add(self): @@ -2565,6 +2564,9 @@ class AutodetectorTests(BaseAutodetectorTests): def test_create_model_with_indexes(self): """Test creation of new model with indexes already defined.""" + added_index = models.Index( + fields=["name"], name="create_model_with_indexes_idx" + ) author = ModelState( "otherapp", "Author", @@ -2573,25 +2575,25 @@ class AutodetectorTests(BaseAutodetectorTests): ("name", models.CharField(max_length=200)), ], { - "indexes": [ - models.Index(fields=["name"], name="create_model_with_indexes_idx") - ] + "indexes": [added_index], }, ) changes = self.get_changes([], [author]) - added_index = models.Index( - fields=["name"], name="create_model_with_indexes_idx" - ) # Right number of migrations? self.assertEqual(len(changes["otherapp"]), 1) # Right number of actions? migration = changes["otherapp"][0] - self.assertEqual(len(migration.operations), 2) + self.assertEqual(len(migration.operations), 1) # Right actions order? - self.assertOperationTypes(changes, "otherapp", 0, ["CreateModel", "AddIndex"]) + self.assertOperationTypes(changes, "otherapp", 0, ["CreateModel"]) self.assertOperationAttributes(changes, "otherapp", 0, 0, name="Author") self.assertOperationAttributes( - changes, "otherapp", 0, 1, model_name="author", index=added_index + changes, + "otherapp", + 0, + 0, + name="Author", + options={"indexes": [added_index]}, ) def test_add_indexes(self): @@ -4043,62 +4045,69 @@ class AutodetectorTests(BaseAutodetectorTests): }, ) - def test_add_model_order_with_respect_to_index_constraint(self): - tests = [ - ( - "AddIndex", - { - "indexes": [ - models.Index(fields=["_order"], name="book_order_idx"), - ] - }, - ), - ( - "AddConstraint", - { - "constraints": [ - models.CheckConstraint( - check=models.Q(_order__gt=1), - name="book_order_gt_1", - ), - ] - }, - ), - ] - for operation, extra_option in tests: - with self.subTest(operation=operation): - after = ModelState( - "testapp", - "Author", - [ - ("id", models.AutoField(primary_key=True)), - ("name", models.CharField(max_length=200)), - ("book", models.ForeignKey("otherapp.Book", models.CASCADE)), - ], - options={ - "order_with_respect_to": "book", - **extra_option, - }, - ) - changes = self.get_changes([], [self.book, after]) - self.assertNumberMigrations(changes, "testapp", 1) - self.assertOperationTypes( - changes, - "testapp", - 0, - [ - "CreateModel", - operation, - ], - ) - self.assertOperationAttributes( - changes, - "testapp", - 0, - 0, - name="Author", - options={"order_with_respect_to": "book"}, - ) + def test_add_model_order_with_respect_to_constraint(self): + after = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("book", models.ForeignKey("otherapp.Book", models.CASCADE)), + ], + options={ + "order_with_respect_to": "book", + "constraints": [ + models.CheckConstraint( + check=models.Q(_order__gt=1), name="book_order_gt_1" + ), + ], + }, + ) + changes = self.get_changes([], [self.book, after]) + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes( + changes, + "testapp", + 0, + ["CreateModel", "AddConstraint"], + ) + self.assertOperationAttributes( + changes, + "testapp", + 0, + 0, + name="Author", + options={"order_with_respect_to": "book"}, + ) + + def test_add_model_order_with_respect_to_index(self): + after = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("book", models.ForeignKey("otherapp.Book", models.CASCADE)), + ], + options={ + "order_with_respect_to": "book", + "indexes": [models.Index(fields=["_order"], name="book_order_idx")], + }, + ) + changes = self.get_changes([], [self.book, after]) + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes(changes, "testapp", 0, ["CreateModel"]) + self.assertOperationAttributes( + changes, + "testapp", + 0, + 0, + name="Author", + options={ + "order_with_respect_to": "book", + "indexes": [models.Index(fields=["_order"], name="book_order_idx")], + }, + ) def test_set_alter_order_with_respect_to_index_constraint_unique_together(self): tests = [ diff --git a/tests/migrations/test_optimizer.py b/tests/migrations/test_optimizer.py index 2f6616ad5b4..cb4000cb033 100644 --- a/tests/migrations/test_optimizer.py +++ b/tests/migrations/test_optimizer.py @@ -1172,3 +1172,181 @@ class OptimizerTests(SimpleTestCase): ], [], ) + + def test_create_model_add_index(self): + self.assertOptimizesTo( + [ + migrations.CreateModel( + name="Pony", + fields=[ + ("weight", models.IntegerField()), + ("age", models.IntegerField()), + ], + options={ + "indexes": [models.Index(fields=["age"], name="idx_pony_age")], + }, + ), + migrations.AddIndex( + "Pony", + models.Index(fields=["weight"], name="idx_pony_weight"), + ), + ], + [ + migrations.CreateModel( + name="Pony", + fields=[ + ("weight", models.IntegerField()), + ("age", models.IntegerField()), + ], + options={ + "indexes": [ + models.Index(fields=["age"], name="idx_pony_age"), + models.Index(fields=["weight"], name="idx_pony_weight"), + ], + }, + ), + ], + ) + + def test_create_model_remove_index(self): + self.assertOptimizesTo( + [ + migrations.CreateModel( + name="Pony", + fields=[ + ("weight", models.IntegerField()), + ("age", models.IntegerField()), + ], + options={ + "indexes": [ + models.Index(fields=["age"], name="idx_pony_age"), + models.Index(fields=["weight"], name="idx_pony_weight"), + ], + }, + ), + migrations.RemoveIndex("Pony", "idx_pony_age"), + ], + [ + migrations.CreateModel( + name="Pony", + fields=[ + ("weight", models.IntegerField()), + ("age", models.IntegerField()), + ], + options={ + "indexes": [ + models.Index(fields=["weight"], name="idx_pony_weight"), + ], + }, + ), + ], + ) + + def test_create_model_remove_index_together_rename_index(self): + self.assertOptimizesTo( + [ + migrations.CreateModel( + name="Pony", + fields=[ + ("weight", models.IntegerField()), + ("age", models.IntegerField()), + ], + options={ + "index_together": [("age", "weight")], + }, + ), + migrations.RenameIndex( + "Pony", new_name="idx_pony_age_weight", old_fields=("age", "weight") + ), + ], + [ + migrations.CreateModel( + name="Pony", + fields=[ + ("weight", models.IntegerField()), + ("age", models.IntegerField()), + ], + options={ + "indexes": [ + models.Index( + fields=["age", "weight"], name="idx_pony_age_weight" + ), + ], + }, + ), + ], + ) + + def test_create_model_index_together_rename_index(self): + self.assertOptimizesTo( + [ + migrations.CreateModel( + name="Pony", + fields=[ + ("weight", models.IntegerField()), + ("age", models.IntegerField()), + ("height", models.IntegerField()), + ("rank", models.IntegerField()), + ], + options={ + "index_together": [("age", "weight"), ("height", "rank")], + }, + ), + migrations.RenameIndex( + "Pony", new_name="idx_pony_age_weight", old_fields=("age", "weight") + ), + ], + [ + migrations.CreateModel( + name="Pony", + fields=[ + ("weight", models.IntegerField()), + ("age", models.IntegerField()), + ("height", models.IntegerField()), + ("rank", models.IntegerField()), + ], + options={ + "index_together": {("height", "rank")}, + "indexes": [ + models.Index( + fields=["age", "weight"], name="idx_pony_age_weight" + ), + ], + }, + ), + ], + ) + + def test_create_model_rename_index_no_old_fields(self): + self.assertOptimizesTo( + [ + migrations.CreateModel( + name="Pony", + fields=[ + ("weight", models.IntegerField()), + ("age", models.IntegerField()), + ], + options={ + "indexes": [models.Index(fields=["age"], name="idx_pony_age")], + }, + ), + migrations.RenameIndex( + "Pony", new_name="idx_pony_age_new", old_name="idx_pony_age" + ), + ], + [ + migrations.CreateModel( + name="Pony", + fields=[ + ("weight", models.IntegerField()), + ("age", models.IntegerField()), + ], + options={ + "indexes": [models.Index(fields=["age"], name="idx_pony_age")], + }, + ), + migrations.RenameIndex( + "Pony", new_name="idx_pony_age_new", old_name="idx_pony_age" + ), + ], + )