diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index 35ab9bf219..1280666924 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -384,8 +384,13 @@ class BaseDatabaseSchemaEditor: self.execute(self._create_index_sql(model, fields, suffix="_idx")) def _delete_composed_index(self, model, fields, constraint_kwargs, sql): + meta_constraint_names = {constraint.name for constraint in model._meta.constraints} + meta_index_names = {constraint.name for constraint in model._meta.indexes} columns = [model._meta.get_field(field).column for field in fields] - constraint_names = self._constraint_names(model, columns, **constraint_kwargs) + constraint_names = self._constraint_names( + model, columns, exclude=meta_constraint_names | meta_index_names, + **constraint_kwargs + ) if len(constraint_names) != 1: raise ValueError("Found wrong number (%s) of constraints for %s(%s)" % ( len(constraint_names), @@ -607,13 +612,15 @@ class BaseDatabaseSchemaEditor: meta_index_names = {index.name for index in model._meta.indexes} # Retrieve only BTREE indexes since this is what's created with # db_index=True. - index_names = self._constraint_names(model, [old_field.column], index=True, type_=Index.suffix) + index_names = self._constraint_names( + model, [old_field.column], index=True, type_=Index.suffix, + exclude=meta_index_names, + ) for index_name in index_names: - if index_name not in meta_index_names: - # The only way to check if an index was created with - # db_index=True or with Index(['field'], name='foo') - # is to look at its name (refs #28053). - self.execute(self._delete_index_sql(model, index_name)) + # The only way to check if an index was created with + # db_index=True or with Index(['field'], name='foo') + # is to look at its name (refs #28053). + self.execute(self._delete_index_sql(model, index_name)) # Change check constraints? if old_db_params['check'] != new_db_params['check'] and old_db_params['check']: meta_constraint_names = {constraint.name for constraint in model._meta.constraints} diff --git a/tests/schema/models.py b/tests/schema/models.py index 2c277c681b..5b756e941c 100644 --- a/tests/schema/models.py +++ b/tests/schema/models.py @@ -62,6 +62,24 @@ class AuthorWithUniqueName(models.Model): apps = new_apps +class AuthorWithIndexedNameAndBirthday(models.Model): + name = models.CharField(max_length=255) + birthday = models.DateField() + + class Meta: + apps = new_apps + index_together = [['name', 'birthday']] + + +class AuthorWithUniqueNameAndBirthday(models.Model): + name = models.CharField(max_length=255) + birthday = models.DateField() + + class Meta: + apps = new_apps + unique_together = [['name', 'birthday']] + + class Book(models.Model): author = models.ForeignKey(Author, models.CASCADE) title = models.CharField(max_length=100, db_index=True) diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 050e4c9385..45befa4e38 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -32,10 +32,11 @@ from .fields import ( from .models import ( Author, AuthorCharFieldWithIndex, AuthorTextFieldWithIndex, AuthorWithDefaultHeight, AuthorWithEvenLongerName, AuthorWithIndexedName, - AuthorWithUniqueName, Book, BookForeignObj, BookWeak, BookWithLongName, - BookWithO2O, BookWithoutAuthor, BookWithSlug, IntegerPK, Node, Note, - NoteRename, Tag, TagIndexed, TagM2MTest, TagUniqueRename, Thing, - UniqueTest, new_apps, + AuthorWithIndexedNameAndBirthday, AuthorWithUniqueName, + AuthorWithUniqueNameAndBirthday, Book, BookForeignObj, BookWeak, + BookWithLongName, BookWithO2O, BookWithoutAuthor, BookWithSlug, IntegerPK, + Node, Note, NoteRename, Tag, TagIndexed, TagM2MTest, TagUniqueRename, + Thing, UniqueTest, new_apps, ) @@ -1839,6 +1840,50 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_unique_together(Book, [['author', 'title']], []) + @skipUnlessDBFeature('allows_multiple_constraints_on_same_fields') + def test_remove_unique_together_does_not_remove_meta_constraints(self): + with connection.schema_editor() as editor: + editor.create_model(AuthorWithUniqueNameAndBirthday) + # Add the custom unique constraint + constraint = UniqueConstraint(fields=['name', 'birthday'], name='author_name_birthday_uniq') + custom_constraint_name = constraint.name + AuthorWithUniqueNameAndBirthday._meta.constraints = [constraint] + with connection.schema_editor() as editor: + editor.add_constraint(AuthorWithUniqueNameAndBirthday, constraint) + # Ensure the constraints exist + constraints = self.get_constraints(AuthorWithUniqueNameAndBirthday._meta.db_table) + self.assertIn(custom_constraint_name, constraints) + other_constraints = [ + name for name, details in constraints.items() + if details['columns'] == ['name', 'birthday'] and details['unique'] and name != custom_constraint_name + ] + self.assertEqual(len(other_constraints), 1) + # Remove unique together + unique_together = AuthorWithUniqueNameAndBirthday._meta.unique_together + with connection.schema_editor() as editor: + editor.alter_unique_together(AuthorWithUniqueNameAndBirthday, unique_together, []) + constraints = self.get_constraints(AuthorWithUniqueNameAndBirthday._meta.db_table) + self.assertIn(custom_constraint_name, constraints) + other_constraints = [ + name for name, details in constraints.items() + if details['columns'] == ['name', 'birthday'] and details['unique'] and name != custom_constraint_name + ] + self.assertEqual(len(other_constraints), 0) + # Re-add unique together + with connection.schema_editor() as editor: + editor.alter_unique_together(AuthorWithUniqueNameAndBirthday, [], unique_together) + constraints = self.get_constraints(AuthorWithUniqueNameAndBirthday._meta.db_table) + self.assertIn(custom_constraint_name, constraints) + other_constraints = [ + name for name, details in constraints.items() + if details['columns'] == ['name', 'birthday'] and details['unique'] and name != custom_constraint_name + ] + self.assertEqual(len(other_constraints), 1) + # Drop the unique constraint + with connection.schema_editor() as editor: + AuthorWithUniqueNameAndBirthday._meta.constraints = [] + editor.remove_constraint(AuthorWithUniqueNameAndBirthday, constraint) + def test_index_together(self): """ Tests removing and adding index_together constraints on a model. @@ -1917,6 +1962,50 @@ class SchemaTests(TransactionTestCase): ), ) + @skipUnlessDBFeature('allows_multiple_constraints_on_same_fields') + def test_remove_index_together_does_not_remove_meta_indexes(self): + with connection.schema_editor() as editor: + editor.create_model(AuthorWithIndexedNameAndBirthday) + # Add the custom index + index = Index(fields=['name', 'birthday'], name='author_name_birthday_idx') + custom_index_name = index.name + AuthorWithIndexedNameAndBirthday._meta.indexes = [index] + with connection.schema_editor() as editor: + editor.add_index(AuthorWithIndexedNameAndBirthday, index) + # Ensure the indexes exist + constraints = self.get_constraints(AuthorWithIndexedNameAndBirthday._meta.db_table) + self.assertIn(custom_index_name, constraints) + other_constraints = [ + name for name, details in constraints.items() + if details['columns'] == ['name', 'birthday'] and details['index'] and name != custom_index_name + ] + self.assertEqual(len(other_constraints), 1) + # Remove index together + index_together = AuthorWithIndexedNameAndBirthday._meta.index_together + with connection.schema_editor() as editor: + editor.alter_index_together(AuthorWithIndexedNameAndBirthday, index_together, []) + constraints = self.get_constraints(AuthorWithIndexedNameAndBirthday._meta.db_table) + self.assertIn(custom_index_name, constraints) + other_constraints = [ + name for name, details in constraints.items() + if details['columns'] == ['name', 'birthday'] and details['index'] and name != custom_index_name + ] + self.assertEqual(len(other_constraints), 0) + # Re-add index together + with connection.schema_editor() as editor: + editor.alter_index_together(AuthorWithIndexedNameAndBirthday, [], index_together) + constraints = self.get_constraints(AuthorWithIndexedNameAndBirthday._meta.db_table) + self.assertIn(custom_index_name, constraints) + other_constraints = [ + name for name, details in constraints.items() + if details['columns'] == ['name', 'birthday'] and details['index'] and name != custom_index_name + ] + self.assertEqual(len(other_constraints), 1) + # Drop the index + with connection.schema_editor() as editor: + AuthorWithIndexedNameAndBirthday._meta.indexes = [] + editor.remove_index(AuthorWithIndexedNameAndBirthday, index) + @isolate_apps('schema') def test_db_table(self): """