diff --git a/django/db/backends/schema.py b/django/db/backends/schema.py index d0c4639d7fd..2a62803a731 100644 --- a/django/db/backends/schema.py +++ b/django/db/backends/schema.py @@ -483,8 +483,11 @@ class BaseDatabaseSchemaEditor(object): new_type = new_db_params['type'] if old_type is None and new_type is None and (old_field.rel.through and new_field.rel.through and old_field.rel.through._meta.auto_created and new_field.rel.through._meta.auto_created): return self._alter_many_to_many(model, old_field, new_field, strict) + elif old_type is None and new_type is None and (old_field.rel.through and new_field.rel.through and not old_field.rel.through._meta.auto_created and not new_field.rel.through._meta.auto_created): + # Both sides have through models; this is a no-op. + return elif old_type is None or new_type is None: - raise ValueError("Cannot alter field %s into %s - they are not compatible types (probably means only one is an M2M with implicit through model)" % ( + raise ValueError("Cannot alter field %s into %s - they are not compatible types (you cannot alter to or from M2M fields, or add or remove through= on M2M fields)" % ( old_field, new_field, )) diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index 005d4b13c86..5b6155be47f 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -148,8 +148,11 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): new_type = new_db_params['type'] if old_type is None and new_type is None and (old_field.rel.through and new_field.rel.through and old_field.rel.through._meta.auto_created and new_field.rel.through._meta.auto_created): return self._alter_many_to_many(model, old_field, new_field, strict) + elif old_type is None and new_type is None and (old_field.rel.through and new_field.rel.through and not old_field.rel.through._meta.auto_created and not new_field.rel.through._meta.auto_created): + # Both sides have through models; this is a no-op. + return elif old_type is None or new_type is None: - raise ValueError("Cannot alter field %s into %s - they are not compatible types (probably means only one is an M2M with implicit through model)" % ( + raise ValueError("Cannot alter field %s into %s - they are not compatible types (you cannot alter to or from M2M fields, or add or remove through= on M2M fields)" % ( old_field, new_field, )) diff --git a/tests/schema/models.py b/tests/schema/models.py index f85a3c6362e..f5f59f7416d 100644 --- a/tests/schema/models.py +++ b/tests/schema/models.py @@ -24,6 +24,22 @@ class AuthorWithM2M(models.Model): apps = new_apps +class AuthorWithM2MThrough(models.Model): + name = models.CharField(max_length=255) + tags = models.ManyToManyField("schema.TagM2MTest", related_name="authors", through="AuthorTag") + + class Meta: + apps = new_apps + + +class AuthorTag(models.Model): + author = models.ForeignKey("schema.AuthorWithM2MThrough") + tag = models.ForeignKey("schema.TagM2MTest") + + class Meta: + apps = new_apps + + class Book(models.Model): author = models.ForeignKey(Author) title = models.CharField(max_length=100, db_index=True) diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 55b2ee5ce39..371eaadecbd 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -9,7 +9,7 @@ from django.db.models.fields.related import ManyToManyField, ForeignKey from django.db.transaction import atomic from .models import (Author, AuthorWithM2M, Book, BookWithLongName, BookWithSlug, BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename, - UniqueTest, Thing, TagThrough, BookWithM2MThrough) + UniqueTest, Thing, TagThrough, BookWithM2MThrough, AuthorTag, AuthorWithM2MThrough) class SchemaTests(TransactionTestCase): @@ -402,6 +402,30 @@ class SchemaTests(TransactionTestCase): # Cleanup model states AuthorWithM2M._meta.local_many_to_many.remove(new_field) + def test_m2m_through_alter(self): + """ + Tests altering M2Ms with explicit through models (should no-op) + """ + # Create the tables + with connection.schema_editor() as editor: + editor.create_model(AuthorTag) + editor.create_model(AuthorWithM2MThrough) + editor.create_model(TagM2MTest) + # Ensure the m2m table is there + self.assertEqual(len(self.column_classes(AuthorTag)), 3) + # "Alter" the field's blankness. This should not actually do anything. + with connection.schema_editor() as editor: + old_field = AuthorWithM2MThrough._meta.get_field_by_name("tags")[0] + new_field = ManyToManyField("schema.TagM2MTest", related_name="authors", through="AuthorTag") + new_field.contribute_to_class(AuthorWithM2MThrough, "tags") + editor.alter_field( + Author, + old_field, + new_field, + ) + # Ensure the m2m table is still there + self.assertEqual(len(self.column_classes(AuthorTag)), 3) + def test_m2m_repoint(self): """ Tests repointing M2M fields