From b731e8841558ee4caaba766c83f34ea9c7004f8b Mon Sep 17 00:00:00 2001 From: Sergey Fursov Date: Wed, 29 Dec 2021 00:18:17 +0300 Subject: [PATCH] Fixed #31335 -- Fixed removing composed composed Meta constraints/indexes on foreign keys on MySQL. --- django/db/backends/mysql/schema.py | 55 ++++++++++++++++++++++++++---- tests/schema/tests.py | 25 ++++++++++++++ 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/django/db/backends/mysql/schema.py b/django/db/backends/mysql/schema.py index 044a752c7a..821f4ddbce 100644 --- a/django/db/backends/mysql/schema.py +++ b/django/db/backends/mysql/schema.py @@ -1,5 +1,6 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor -from django.db.models import NOT_PROVIDED +from django.db.models import NOT_PROVIDED, F, UniqueConstraint +from django.db.models.constants import LOOKUP_SEP class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): @@ -117,6 +118,23 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): [effective_default], ) + def remove_constraint(self, model, constraint): + if isinstance(constraint, UniqueConstraint): + self._create_missing_fk_index( + model, + fields=constraint.fields, + expressions=constraint.expressions, + ) + super().remove_constraint(model, constraint) + + def remove_index(self, model, index): + self._create_missing_fk_index( + model, + fields=[field_name for field_name, _ in index.fields_orders], + expressions=index.expressions, + ) + super().remove_index(model, index) + def _field_should_be_indexed(self, model, field): if not super()._field_should_be_indexed(model, field): return False @@ -140,6 +158,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): model, *, fields, + expressions=None, ): """ MySQL can remove an implicit FK index on a field when that field is @@ -150,14 +169,36 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): Manually create an implicit FK index to make it possible to remove the composed index. """ - first_field = model._meta.get_field(fields[0]) + first_field_name = None + if fields: + first_field_name = fields[0] + elif ( + expressions + and self.connection.features.supports_expression_indexes + and isinstance(expressions[0], F) + and LOOKUP_SEP not in expressions[0].name + ): + first_field_name = expressions[0].name + + if not first_field_name: + return + + first_field = model._meta.get_field(first_field_name) if first_field.get_internal_type() == "ForeignKey": - constraint_names = self._constraint_names( - model, - [first_field.column], - index=True, + column = self.connection.introspection.identifier_converter( + first_field.column ) - if not constraint_names: + with self.connection.cursor() as cursor: + constraint_names = [ + name + for name, infodict in self.connection.introspection.get_constraints( + cursor, model._meta.db_table + ).items() + if infodict["index"] and infodict["columns"][0] == column + ] + # There are no other indexes that starts with the FK field, only + # the index that is expected to be deleted. + if len(constraint_names) == 1: self.execute( self._create_index_sql(model, fields=[first_field], suffix="") ) diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 941b28900e..aaf6dff2ff 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -2735,6 +2735,24 @@ class SchemaTests(TransactionTestCase): editor.remove_index(Book, index) self.assertNotIn(index.name, self.get_constraints(table)) + def test_composed_index_with_fk(self): + index = Index(fields=["author", "title"], name="book_author_title_idx") + self._test_composed_index_with_fk(index) + + def test_composed_desc_index_with_fk(self): + index = Index(fields=["-author", "title"], name="book_author_title_idx") + self._test_composed_index_with_fk(index) + + @skipUnlessDBFeature("supports_expression_indexes") + def test_composed_func_index_with_fk(self): + index = Index(F("author"), F("title"), name="book_author_title_idx") + self._test_composed_index_with_fk(index) + + @skipUnlessDBFeature("supports_expression_indexes") + def test_composed_desc_func_index_with_fk(self): + index = Index(F("author").desc(), F("title"), name="book_author_title_idx") + self._test_composed_index_with_fk(index) + @skipUnlessDBFeature("supports_expression_indexes") def test_composed_func_transform_index_with_fk(self): index = Index(F("title__lower"), name="book_title_lower_idx") @@ -2756,6 +2774,13 @@ class SchemaTests(TransactionTestCase): editor.remove_constraint(Book, constraint) self.assertNotIn(constraint.name, self.get_constraints(table)) + def test_composed_constraint_with_fk(self): + constraint = UniqueConstraint( + fields=["author", "title"], + name="book_author_title_uniq", + ) + self._test_composed_constraint_with_fk(constraint) + @skipUnlessDBFeature( "supports_column_check_constraints", "can_introspect_check_constraints" )