diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index 4a59a6fe6786..491b80107b40 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -1690,7 +1690,7 @@ def _field_should_be_altered(self, old_field, new_field, ignore=None): _, old_path, old_args, old_kwargs = old_field.deconstruct() _, new_path, new_args, new_kwargs = new_field.deconstruct() # Don't alter when: - # - changing only a field name + # - changing only a field name (unless it's a many-to-many) # - changing an attribute that doesn't affect the schema # - changing an attribute in the provided set of ignored attributes # - adding only a db_column and the column name is not changed @@ -1708,7 +1708,7 @@ def _field_should_be_altered(self, old_field, new_field, ignore=None): ): old_kwargs.pop("to", None) new_kwargs.pop("to", None) - # db_default can take many form but result in the same SQL. + # db_default can take many forms but result in the same SQL. if ( old_kwargs.get("db_default") and new_kwargs.get("db_default") @@ -1716,11 +1716,19 @@ def _field_should_be_altered(self, old_field, new_field, ignore=None): ): old_kwargs.pop("db_default") new_kwargs.pop("db_default") - return ( + if ( old_field.concrete and new_field.concrete and (self.quote_name(old_field.column) != self.quote_name(new_field.column)) - ) or (old_path, old_args, old_kwargs) != (new_path, new_args, new_kwargs) + ): + return True + if ( + old_field.many_to_many + and new_field.many_to_many + and old_field.name != new_field.name + ): + return True + return (old_path, old_args, old_kwargs) != (new_path, new_args, new_kwargs) def _field_should_be_indexed(self, model, field): return field.db_index and not field.unique diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py index 93251ff322e3..428631997806 100644 --- a/django/db/models/constraints.py +++ b/django/db/models/constraints.py @@ -408,8 +408,8 @@ def check(self, model, connection): ): errors.append( checks.Warning( - f"{connection.display_name} does not support unique constraints " - "with nulls distinct.", + f"{connection.display_name} does not support " + "UniqueConstraint.nulls_distinct.", hint=( "A constraint won't be created. Silence this warning if you " "don't care about it." diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 7dfa7c61d24b..1e56464a6f18 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -447,8 +447,8 @@ Models expression and won't be validated during the model ``full_clean()``. * **models.W046**: ```` does not support comments on tables (``db_table_comment``). -* **models.W047**: ```` does not support unique constraints with - nulls distinct. +* **models.W047**: ```` does not support + ``UniqueConstraint.nulls_distinct``. * **models.E048**: ``constraints/indexes/unique_together`` refers to a ``CompositePrimaryKey`` ````, but ``CompositePrimaryKey``\s are not supported for that option. diff --git a/docs/releases/6.0.1.txt b/docs/releases/6.0.1.txt index 72c35b3e6952..a84d49b07a9a 100644 --- a/docs/releases/6.0.1.txt +++ b/docs/releases/6.0.1.txt @@ -12,3 +12,7 @@ Bugfixes * Fixed a regression in Django 6.0 where :ttag:`querystring` mishandled multi-value :class:`~django.http.QueryDict` keys, both by only preserving the last value and by incorrectly handling ``None`` values (:ticket:`36783`). + +* Fixed a regression in Django 6.0 that prevented changing the name of a + :class:`~django.db.models.ManyToManyField` from taking effect when applying + migrations (:ticket:`36800`). diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py index fe3c8126152f..679afa3dc67d 100644 --- a/tests/invalid_models_tests/test_models.py +++ b/tests/invalid_models_tests/test_models.py @@ -2891,8 +2891,8 @@ class Meta: ] warn = Warning( - f"{connection.display_name} does not support unique constraints with nulls " - "distinct.", + f"{connection.display_name} does not support " + "UniqueConstraint.nulls_distinct.", hint=( "A constraint won't be created. Silence this warning if you don't care " "about it." diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 12c14563e048..ab6cd60c0782 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -2889,6 +2889,40 @@ def test_m2m_repoint_custom(self): def test_m2m_repoint_inherited(self): self._test_m2m_repoint(InheritedManyToManyField) + def test_m2m_rename(self): + class LocalBook(Model): + authors = ManyToManyField("schema.Author") + + class Meta: + app_label = "schema" + apps = new_apps + + self.local_models = [LocalBook] + with connection.schema_editor() as editor: + editor.create_model(Author) + editor.create_model(LocalBook) + old_field = LocalBook._meta.get_field("authors") + new_field = ManyToManyField("schema.Author") + new_field.contribute_to_class(LocalBook, "writers") + with connection.schema_editor() as editor: + editor.alter_field(LocalBook, old_field, new_field, strict=True) + # Ensure old M2M is gone. + with self.assertRaises(DatabaseError): + self.column_classes( + LocalBook._meta.get_field("authors").remote_field.through + ) + if connection.features.supports_foreign_keys: + self.assertForeignKeyExists( + new_field.remote_field.through, + "author_id", + "schema_author", + ) + new_through_table = new_field.remote_field.through._meta.db_table + self.assertIn("writers", new_through_table) + self.assertNotIn("authors", new_through_table) + # Remove the old field from meta for tearDown(). + LocalBook._meta.local_many_to_many.remove(old_field) + @isolate_apps("schema") def test_m2m_rename_field_in_target_model(self): class LocalTagM2MTest(Model):