diff --git a/django/db/backends/mysql/schema.py b/django/db/backends/mysql/schema.py index ea5dc6fb418f5..4aef58422d6d6 100644 --- a/django/db/backends/mysql/schema.py +++ b/django/db/backends/mysql/schema.py @@ -172,7 +172,14 @@ def _create_missing_fk_index( first_field_name = None if fields: first_field_name = fields[0] - elif expressions: + elif ( + expressions + and self.connection.features.supports_expression_indexes + # expression is a simple F-object + and isinstance(expressions[0], F) + # expression doesn't contain custom lookup (field__lookup) + and LOOKUP_SEP not in expressions[0].name + ): first_field_name = expressions[0].name if not first_field_name: diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index c71cddc44dbe4..dc37289c3fa66 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -5416,6 +5416,96 @@ def assert_index_created(operations): ] ) + def test_composed_func_indexes_dont_fail_implicit_fk_index_check(self): + """ + Test that implicit FK index check works with any kind of index/constraint, + that shouldn't create any additional index. + See the tests above to find types of indexed that should do that. + """ + + app_label = "test_cfidcfkid" + + create_author = migrations.CreateModel( + "Author", + [("id", models.BigAutoField(primary_key=True))], + options={}, + ) + create_book = migrations.CreateModel( + "Book", + [ + ("id", models.BigAutoField(primary_key=True)), + ("author", models.ForeignKey(to="Author", on_delete=models.CASCADE)), + ("title", models.CharField(max_length=100)), + ], + options={}, + ) + + project_state = self.apply_operations( + app_label, + ProjectState(), + [ + create_author, + create_book, + ], + ) + + def get_author_field_indexes(): + Book = project_state.apps.get_model(app_label, "Book") + columns = ["author_id"] + with connection.schema_editor() as editor: + return editor._constraint_names(Book, index=True, column_names=columns) + + def assert_index_not_created(operations): + initial_indexes = get_author_field_indexes() + self.apply_operations(app_label, project_state, operations) + actual_indexes = get_author_field_indexes() + self.assertEqual(set(actual_indexes), set(initial_indexes)) + + desc_expression_index = models.Index( + models.F("author").desc(), + models.F("title"), + name="author_title_index", + ) + assert_index_not_created( + [ + migrations.AddIndex("Book", desc_expression_index), + migrations.RemoveIndex("Book", desc_expression_index.name), + ] + ) + + lookup_index = models.Index(models.F("title__lower"), name="title_index") + with register_lookup(models.CharField, Lower): + assert_index_not_created( + [ + migrations.AddIndex("Book", lookup_index), + migrations.RemoveIndex("Book", lookup_index.name), + ] + ) + + if connection.features.supports_table_check_constraints: + check_constraint = models.CheckConstraint( + check=models.Q(title__contains="a"), + name="title_check", + ) + assert_index_not_created( + [ + migrations.AddConstraint("Book", check_constraint), + migrations.RemoveConstraint("Book", check_constraint.name), + ] + ) + + if connection.features.supports_index_column_ordering: + negative_index = models.Index( + fields=["-author", "title"], + name="author_title_index", + ) + assert_index_not_created( + [ + migrations.AddIndex("Book", negative_index), + migrations.RemoveIndex("Book", negative_index.name), + ] + ) + def test_composed_indexes_dont_create_dup_implicit_fk_index_when_deleting(self): """ When we delete a composed index with a FK included, we shouldn't create the