diff --git a/tests/schema/fields.py b/tests/schema/fields.py index 4f70c96b0b..d4302a6677 100644 --- a/tests/schema/fields.py +++ b/tests/schema/fields.py @@ -52,3 +52,7 @@ class CustomManyToManyField(RelatedField): _get_m2m_attr = ManyToManyField.__dict__['_get_m2m_attr'] _get_m2m_reverse_attr = ManyToManyField.__dict__['_get_m2m_reverse_attr'] _get_m2m_db_table = ManyToManyField.__dict__['_get_m2m_db_table'] + + +class InheritedManyToManyField(ManyToManyField): + pass diff --git a/tests/schema/models.py b/tests/schema/models.py index 6eba7ccae1..cf49d95abb 100644 --- a/tests/schema/models.py +++ b/tests/schema/models.py @@ -25,24 +25,9 @@ class AuthorWithDefaultHeight(models.Model): apps = new_apps -class AuthorWithM2M(models.Model): +class AuthorWithEvenLongerName(models.Model): name = models.CharField(max_length=255) - - class Meta: - 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") + height = models.PositiveIntegerField(null=True, blank=True) class Meta: apps = new_apps @@ -67,6 +52,13 @@ class BookWeak(models.Model): apps = new_apps +class BookWithLongName(models.Model): + author_foreign_key_with_really_long_field_name = models.ForeignKey(AuthorWithEvenLongerName) + + class Meta: + apps = new_apps + + class BookWithO2O(models.Model): author = models.OneToOneField(Author) title = models.CharField(max_length=100, db_index=True) @@ -77,31 +69,6 @@ class BookWithO2O(models.Model): db_table = "schema_book" -class BookWithM2M(models.Model): - author = models.ForeignKey(Author) - title = models.CharField(max_length=100, db_index=True) - pub_date = models.DateTimeField() - tags = models.ManyToManyField("TagM2MTest", related_name="books") - - class Meta: - apps = new_apps - - -class TagThrough(models.Model): - book = models.ForeignKey("schema.BookWithM2MThrough") - tag = models.ForeignKey("schema.TagM2MTest") - - class Meta: - apps = new_apps - - -class BookWithM2MThrough(models.Model): - tags = models.ManyToManyField("TagM2MTest", related_name="books", through=TagThrough) - - class Meta: - apps = new_apps - - class BookWithSlug(models.Model): author = models.ForeignKey(Author) title = models.CharField(max_length=100, db_index=True) @@ -113,6 +80,10 @@ class BookWithSlug(models.Model): db_table = "schema_book" +class Note(models.Model): + info = models.TextField() + + class Tag(models.Model): title = models.CharField(max_length=255) slug = models.SlugField(unique=True) @@ -121,14 +92,6 @@ class Tag(models.Model): apps = new_apps -class TagM2MTest(models.Model): - title = models.CharField(max_length=255) - slug = models.SlugField(unique=True) - - class Meta: - apps = new_apps - - class TagIndexed(models.Model): title = models.CharField(max_length=255) slug = models.SlugField(unique=True) @@ -138,6 +101,14 @@ class TagIndexed(models.Model): index_together = [["slug", "title"]] +class TagM2MTest(models.Model): + title = models.CharField(max_length=255) + slug = models.SlugField(unique=True) + + class Meta: + apps = new_apps + + class TagUniqueRename(models.Model): title = models.CharField(max_length=255) slug2 = models.SlugField(unique=True) @@ -147,30 +118,6 @@ class TagUniqueRename(models.Model): db_table = "schema_tag" -class UniqueTest(models.Model): - year = models.IntegerField() - slug = models.SlugField(unique=False) - - class Meta: - apps = new_apps - unique_together = ["year", "slug"] - - -class AuthorWithEvenLongerName(models.Model): - name = models.CharField(max_length=255) - height = models.PositiveIntegerField(null=True, blank=True) - - class Meta: - apps = new_apps - - -class BookWithLongName(models.Model): - author_foreign_key_with_really_long_field_name = models.ForeignKey(AuthorWithEvenLongerName) - - class Meta: - apps = new_apps - - # Based on tests/reserved_names/models.py @python_2_unicode_compatible class Thing(models.Model): @@ -183,5 +130,10 @@ class Thing(models.Model): return self.when -class Note(models.Model): - info = models.TextField() +class UniqueTest(models.Model): + year = models.IntegerField() + slug = models.SlugField(unique=False) + + class Meta: + apps = new_apps + unique_together = ["year", "slug"] diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 12fac76579..863392084b 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -1,17 +1,21 @@ import datetime +import itertools import unittest -from django.test import TransactionTestCase from django.db import connection, DatabaseError, IntegrityError, OperationalError -from django.db.models.fields import (BinaryField, BooleanField, CharField, IntegerField, - PositiveIntegerField, SlugField, TextField) +from django.db.models import Model +from django.db.models.fields import (BinaryField, BooleanField, CharField, DateTimeField, + IntegerField, PositiveIntegerField, SlugField, TextField) from django.db.models.fields.related import ForeignKey, ManyToManyField, OneToOneField from django.db.transaction import atomic -from .fields import CustomManyToManyField -from .models import (Author, AuthorWithDefaultHeight, AuthorWithM2M, Book, BookWithLongName, - BookWithSlug, BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename, - UniqueTest, Thing, TagThrough, BookWithM2MThrough, AuthorTag, AuthorWithM2MThrough, - AuthorWithEvenLongerName, BookWeak, Note, BookWithO2O) +from django.test import TransactionTestCase + +from .fields import CustomManyToManyField, InheritedManyToManyField +from .models import ( + Author, AuthorWithDefaultHeight, AuthorWithEvenLongerName, Book, BookWeak, + BookWithLongName, BookWithO2O, BookWithSlug, Note, Tag, TagIndexed, + TagM2MTest, TagUniqueRename, Thing, UniqueTest, new_apps +) class SchemaTests(TransactionTestCase): @@ -26,24 +30,34 @@ class SchemaTests(TransactionTestCase): available_apps = [] models = [ - Author, AuthorWithM2M, Book, BookWithLongName, BookWithSlug, - BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename, UniqueTest, - Thing, TagThrough, BookWithM2MThrough, AuthorWithEvenLongerName, - BookWeak, BookWithO2O, + Author, AuthorWithDefaultHeight, AuthorWithEvenLongerName, Book, + BookWeak, BookWithLongName, BookWithO2O, BookWithSlug, Note, Tag, + TagIndexed, TagM2MTest, TagUniqueRename, Thing, UniqueTest, ] # Utility functions + def setUp(self): + # local_models should contain test dependent model classes that will be + # automatically removed from the app cache on test tear down. + self.local_models = [] + def tearDown(self): # Delete any tables made for our models self.delete_tables() + new_apps.clear_cache() + for model in new_apps.get_models(): + model._meta._expire_cache() + if 'schema' in new_apps.all_models: + for model in self.local_models: + del new_apps.all_models['schema'][model._meta.model_name] def delete_tables(self): "Deletes all model tables for our models for a clean test environment" with connection.cursor() as cursor: connection.disable_constraint_checking() table_names = connection.introspection.table_names(cursor) - for model in self.models: + for model in itertools.chain(SchemaTests.models, self.local_models): # Remove any M2M tables first for field in model._meta.local_many_to_many: with atomic(): @@ -134,15 +148,11 @@ class SchemaTests(TransactionTestCase): pub_date=datetime.datetime.now(), ) # Repoint the FK constraint + old_field = Book._meta.get_field("author") new_field = ForeignKey(Tag) new_field.set_attributes_from_name("author") with connection.schema_editor() as editor: - editor.alter_field( - Book, - Book._meta.get_field("author"), - new_field, - strict=True, - ) + editor.alter_field(Book, old_field, new_field, strict=True) # Make sure the new FK constraint is present constraints = self.get_constraints(Book._meta.db_table) for name, details in constraints.items(): @@ -173,25 +183,17 @@ class SchemaTests(TransactionTestCase): new_field = ForeignKey(Tag, db_constraint=False) new_field.set_attributes_from_name("tag") with connection.schema_editor() as editor: - editor.add_field( - Author, - new_field, - ) + editor.add_field(Author, new_field) # Make sure no FK constraint is present constraints = self.get_constraints(Author._meta.db_table) for name, details in constraints.items(): if details['columns'] == ["tag_id"] and details['foreign_key']: self.fail("FK constraint for tag_id found") # Alter to one with a constraint - new_field_2 = ForeignKey(Tag) - new_field_2.set_attributes_from_name("tag") + new_field2 = ForeignKey(Tag) + new_field2.set_attributes_from_name("tag") with connection.schema_editor() as editor: - editor.alter_field( - Author, - new_field, - new_field_2, - strict=True, - ) + editor.alter_field(Author, new_field, new_field2, strict=True) # Make sure the new FK constraint is present constraints = self.get_constraints(Author._meta.db_table) for name, details in constraints.items(): @@ -201,45 +203,56 @@ class SchemaTests(TransactionTestCase): else: self.fail("No FK constraint for tag_id found") # Alter to one without a constraint again - new_field_2 = ForeignKey(Tag) - new_field_2.set_attributes_from_name("tag") + new_field2 = ForeignKey(Tag) + new_field2.set_attributes_from_name("tag") with connection.schema_editor() as editor: - editor.alter_field( - Author, - new_field_2, - new_field, - strict=True, - ) + editor.alter_field(Author, new_field2, new_field, strict=True) # Make sure no FK constraint is present constraints = self.get_constraints(Author._meta.db_table) for name, details in constraints.items(): if details['columns'] == ["tag_id"] and details['foreign_key']: self.fail("FK constraint for tag_id found") - @unittest.skipUnless(connection.features.supports_foreign_keys, "No FK support") - def test_m2m_db_constraint(self): + def _test_m2m_db_constraint(self, M2MFieldClass): + class LocalAuthorWithM2M(Model): + name = CharField(max_length=255) + + class Meta: + apps = new_apps + + self.local_models = [LocalAuthorWithM2M] + # Create the table with connection.schema_editor() as editor: editor.create_model(Tag) - editor.create_model(Author) + editor.create_model(LocalAuthorWithM2M) # Check that initial tables are there - list(Author.objects.all()) + list(LocalAuthorWithM2M.objects.all()) list(Tag.objects.all()) # Make a db_constraint=False FK - new_field = ManyToManyField("schema.Tag", related_name="authors", db_constraint=False) - new_field.contribute_to_class(Author, "tags") + new_field = M2MFieldClass(Tag, related_name="authors", db_constraint=False) + new_field.contribute_to_class(LocalAuthorWithM2M, "tags") # Add the field with connection.schema_editor() as editor: - editor.add_field( - Author, - new_field, - ) + editor.add_field(LocalAuthorWithM2M, new_field) # Make sure no FK constraint is present constraints = self.get_constraints(new_field.rel.through._meta.db_table) for name, details in constraints.items(): if details['columns'] == ["tag_id"] and details['foreign_key']: self.fail("FK constraint for tag_id found") + @unittest.skipUnless(connection.features.supports_foreign_keys, "No FK support") + def test_m2m_db_constraint(self): + self._test_m2m_db_constraint(ManyToManyField) + + @unittest.skipUnless(connection.features.supports_foreign_keys, "No FK support") + def test_m2m_db_constraint_custom(self): + self._test_m2m_db_constraint(CustomManyToManyField) + + @unittest.skipUnless(connection.features.supports_foreign_keys, "No FK support") + def test_m2m_db_constraint_inherited(self): + self._test_m2m_db_constraint(InheritedManyToManyField) + def test_add_field(self): """ Tests adding fields to models @@ -254,10 +267,7 @@ class SchemaTests(TransactionTestCase): new_field = IntegerField(null=True) new_field.set_attributes_from_name("age") with connection.schema_editor() as editor: - editor.add_field( - Author, - new_field, - ) + editor.add_field(Author, new_field) # Ensure the field is right afterwards columns = self.column_classes(Author) self.assertEqual(columns['age'][0], "IntegerField") @@ -280,10 +290,7 @@ class SchemaTests(TransactionTestCase): new_field = CharField(max_length=30, default="Godwin") new_field.set_attributes_from_name("surname") with connection.schema_editor() as editor: - editor.add_field( - Author, - new_field, - ) + editor.add_field(Author, new_field) # Ensure the field is right afterwards columns = self.column_classes(Author) self.assertEqual(columns['surname'][0], "CharField") @@ -308,10 +315,7 @@ class SchemaTests(TransactionTestCase): new_field = BooleanField(default=False) new_field.set_attributes_from_name("awesome") with connection.schema_editor() as editor: - editor.add_field( - Author, - new_field, - ) + editor.add_field(Author, new_field) # Ensure the field is right afterwards columns = self.column_classes(Author) # BooleanField are stored as TINYINT(1) on MySQL. @@ -345,10 +349,7 @@ class SchemaTests(TransactionTestCase): new_field = TestTransformField(default={1: 2}) new_field.set_attributes_from_name("thing") with connection.schema_editor() as editor: - editor.add_field( - Author, - new_field, - ) + editor.add_field(Author, new_field) # Ensure the field is there columns = self.column_classes(Author) field_type, field_info = columns['thing'] @@ -367,10 +368,7 @@ class SchemaTests(TransactionTestCase): new_field = BinaryField(blank=True) new_field.set_attributes_from_name("bits") with connection.schema_editor() as editor: - editor.add_field( - Author, - new_field, - ) + editor.add_field(Author, new_field) # Ensure the field is right afterwards columns = self.column_classes(Author) # MySQL annoyingly uses the same backend, so it'll come back as one of @@ -389,15 +387,11 @@ class SchemaTests(TransactionTestCase): self.assertEqual(columns['name'][0], "CharField") self.assertEqual(bool(columns['name'][1][6]), bool(connection.features.interprets_empty_strings_as_nulls)) # Alter the name field to a TextField + old_field = Author._meta.get_field("name") new_field = TextField(null=True) new_field.set_attributes_from_name("name") with connection.schema_editor() as editor: - editor.alter_field( - Author, - Author._meta.get_field("name"), - new_field, - strict=True, - ) + editor.alter_field(Author, old_field, new_field, strict=True) # Ensure the field is right afterwards columns = self.column_classes(Author) self.assertEqual(columns['name'][0], "TextField") @@ -406,12 +400,7 @@ class SchemaTests(TransactionTestCase): new_field2 = TextField(null=False) new_field2.set_attributes_from_name("name") with connection.schema_editor() as editor: - editor.alter_field( - Author, - new_field, - new_field2, - strict=True, - ) + editor.alter_field(Author, new_field, new_field2, strict=True) # Ensure the field is right afterwards columns = self.column_classes(Author) self.assertEqual(columns['name'][0], "TextField") @@ -420,15 +409,14 @@ class SchemaTests(TransactionTestCase): def test_alter_text_field(self): # Regression for "BLOB/TEXT column 'info' can't have a default value") # on MySQL. + # Create the table + with connection.schema_editor() as editor: + editor.create_model(Note) + old_field = Note._meta.get_field("info") new_field = TextField(blank=True) new_field.set_attributes_from_name("info") with connection.schema_editor() as editor: - editor.alter_field( - Note, - Note._meta.get_field("info"), - new_field, - strict=True, - ) + editor.alter_field(Note, old_field, new_field, strict=True) def test_alter_null_to_not_null(self): """ @@ -447,14 +435,11 @@ class SchemaTests(TransactionTestCase): self.assertEqual(Author.objects.get(name='Not null author').height, 12) self.assertIsNone(Author.objects.get(name='Null author').height) # Alter the height field to NOT NULL with default + old_field = Author._meta.get_field("height") new_field = PositiveIntegerField(default=42) new_field.set_attributes_from_name("height") with connection.schema_editor() as editor: - editor.alter_field( - Author, - Author._meta.get_field("height"), - new_field - ) + editor.alter_field(Author, old_field, new_field) # Ensure the field is right afterwards columns = self.column_classes(Author) self.assertFalse(columns['height'][1][6]) @@ -475,14 +460,11 @@ class SchemaTests(TransactionTestCase): columns = self.column_classes(AuthorWithDefaultHeight) self.assertTrue(columns['height'][1][6]) # Alter the height field to NOT NULL keeping the previous default + old_field = AuthorWithDefaultHeight._meta.get_field("height") new_field = PositiveIntegerField(default=42) new_field.set_attributes_from_name("height") with connection.schema_editor() as editor: - editor.alter_field( - AuthorWithDefaultHeight, - AuthorWithDefaultHeight._meta.get_field("height"), - new_field, - ) + editor.alter_field(AuthorWithDefaultHeight, old_field, new_field) # Ensure the field is right afterwards columns = self.column_classes(AuthorWithDefaultHeight) self.assertFalse(columns['height'][1][6]) @@ -508,15 +490,11 @@ class SchemaTests(TransactionTestCase): else: self.fail("No FK constraint for author_id found") # Alter the FK + old_field = Book._meta.get_field("author") new_field = ForeignKey(Author, editable=False) new_field.set_attributes_from_name("author") with connection.schema_editor() as editor: - editor.alter_field( - Book, - Book._meta.get_field("author"), - new_field, - strict=True, - ) + editor.alter_field(Book, old_field, new_field, strict=True) # Ensure the field is right afterwards columns = self.column_classes(Book) self.assertEqual(columns['author_id'][0], "IntegerField") @@ -556,15 +534,11 @@ class SchemaTests(TransactionTestCase): author_is_fk = True self.assertTrue(author_is_fk, "No FK constraint for author_id found") # Alter the OneToOneField to ForeignKey + old_field = BookWithO2O._meta.get_field("author") new_field = ForeignKey(Author) new_field.set_attributes_from_name("author") with connection.schema_editor() as editor: - editor.alter_field( - BookWithO2O, - BookWithO2O._meta.get_field("author"), - new_field, - strict=True, - ) + editor.alter_field(BookWithO2O, old_field, new_field, strict=True) # Ensure the field is right afterwards columns = self.column_classes(Book) self.assertEqual(columns['author_id'][0], "IntegerField") @@ -606,15 +580,11 @@ class SchemaTests(TransactionTestCase): author_is_fk = True self.assertTrue(author_is_fk, "No FK constraint for author_id found") # Alter the ForeignKey to OneToOneField + old_field = Book._meta.get_field("author") new_field = OneToOneField(Author) new_field.set_attributes_from_name("author") with connection.schema_editor() as editor: - editor.alter_field( - Book, - Book._meta.get_field("author"), - new_field, - strict=True, - ) + editor.alter_field(Book, old_field, new_field, strict=True) # Ensure the field is right afterwards columns = self.column_classes(BookWithO2O) self.assertEqual(columns['author_id'][0], "IntegerField") @@ -639,17 +609,12 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.create_model(Author) + old_field = Author._meta.get_field("id") new_field = IntegerField(primary_key=True) new_field.set_attributes_from_name("id") new_field.model = Author with connection.schema_editor() as editor: - editor.alter_field( - Author, - Author._meta.get_field("id"), - new_field, - strict=True, - ) - + editor.alter_field(Author, old_field, new_field, strict=True) # This will fail if DROP DEFAULT is inadvertently executed on this # field which drops the id sequence, at least on PostgreSQL. Author.objects.create(name='Foo') @@ -666,127 +631,202 @@ class SchemaTests(TransactionTestCase): self.assertEqual(columns['name'][0], "CharField") self.assertNotIn("display_name", columns) # Alter the name field's name + old_field = Author._meta.get_field("name") new_field = CharField(max_length=254) new_field.set_attributes_from_name("display_name") with connection.schema_editor() as editor: - editor.alter_field( - Author, - Author._meta.get_field("name"), - new_field, - strict=True, - ) + editor.alter_field(Author, old_field, new_field, strict=True) # Ensure the field is right afterwards columns = self.column_classes(Author) self.assertEqual(columns['display_name'][0], "CharField") self.assertNotIn("name", columns) - def test_m2m_create(self): + def _test_m2m_create(self, M2MFieldClass): """ Tests M2M fields on models during creation """ + class LocalBookWithM2M(Model): + author = ForeignKey(Author) + title = CharField(max_length=100, db_index=True) + pub_date = DateTimeField() + tags = M2MFieldClass("TagM2MTest", related_name="books") + + class Meta: + apps = new_apps + + self.local_models = [LocalBookWithM2M] + # Create the tables with connection.schema_editor() as editor: editor.create_model(Author) editor.create_model(TagM2MTest) - editor.create_model(BookWithM2M) + editor.create_model(LocalBookWithM2M) # Ensure there is now an m2m table there - columns = self.column_classes(BookWithM2M._meta.get_field("tags").rel.through) + columns = self.column_classes(LocalBookWithM2M._meta.get_field("tags").rel.through) self.assertEqual(columns['tagm2mtest_id'][0], "IntegerField") - def test_m2m_create_through(self): + def test_m2m_create(self): + self._test_m2m_create(ManyToManyField) + + def test_m2m_create_custom(self): + self._test_m2m_create(CustomManyToManyField) + + def test_m2m_create_inherited(self): + self._test_m2m_create(InheritedManyToManyField) + + def _test_m2m_create_through(self, M2MFieldClass): """ Tests M2M fields on models during creation with through models """ + class LocalTagThrough(Model): + book = ForeignKey("schema.LocalBookWithM2MThrough") + tag = ForeignKey("schema.TagM2MTest") + + class Meta: + apps = new_apps + + class LocalBookWithM2MThrough(Model): + tags = M2MFieldClass("TagM2MTest", related_name="books", through=LocalTagThrough) + + class Meta: + apps = new_apps + + self.local_models = [LocalTagThrough, LocalBookWithM2MThrough] + # Create the tables with connection.schema_editor() as editor: - editor.create_model(TagThrough) + editor.create_model(LocalTagThrough) editor.create_model(TagM2MTest) - editor.create_model(BookWithM2MThrough) + editor.create_model(LocalBookWithM2MThrough) # Ensure there is now an m2m table there - columns = self.column_classes(TagThrough) + columns = self.column_classes(LocalTagThrough) self.assertEqual(columns['book_id'][0], "IntegerField") self.assertEqual(columns['tag_id'][0], "IntegerField") - def test_m2m(self): + def test_m2m_create_through(self): + self._test_m2m_create_through(ManyToManyField) + + def test_m2m_create_through_custom(self): + self._test_m2m_create_through(CustomManyToManyField) + + def test_m2m_create_through_inherited(self): + self._test_m2m_create_through(InheritedManyToManyField) + + def _test_m2m(self, M2MFieldClass): """ Tests adding/removing M2M fields on models """ + class LocalAuthorWithM2M(Model): + name = CharField(max_length=255) + + class Meta: + apps = new_apps + + self.local_models = [LocalAuthorWithM2M] + # Create the tables with connection.schema_editor() as editor: - editor.create_model(AuthorWithM2M) + editor.create_model(LocalAuthorWithM2M) editor.create_model(TagM2MTest) # Create an M2M field - new_field = ManyToManyField("schema.TagM2MTest", related_name="authors") - new_field.contribute_to_class(AuthorWithM2M, "tags") - try: - # Ensure there's no m2m table there - self.assertRaises(DatabaseError, self.column_classes, new_field.rel.through) - # Add the field - with connection.schema_editor() as editor: - editor.add_field( - Author, - new_field, - ) - # Ensure there is now an m2m table there - columns = self.column_classes(new_field.rel.through) - self.assertEqual(columns['tagm2mtest_id'][0], "IntegerField") + new_field = M2MFieldClass("schema.TagM2MTest", related_name="authors") + new_field.contribute_to_class(LocalAuthorWithM2M, "tags") + # Ensure there's no m2m table there + self.assertRaises(DatabaseError, self.column_classes, new_field.rel.through) + # Add the field + with connection.schema_editor() as editor: + editor.add_field(LocalAuthorWithM2M, new_field) + # Ensure there is now an m2m table there + columns = self.column_classes(new_field.rel.through) + self.assertEqual(columns['tagm2mtest_id'][0], "IntegerField") - # "Alter" the field. This should not rename the DB table to itself. - with connection.schema_editor() as editor: - editor.alter_field( - Author, - new_field, - new_field, - ) + # "Alter" the field. This should not rename the DB table to itself. + with connection.schema_editor() as editor: + editor.alter_field(LocalAuthorWithM2M, new_field, new_field) - # Remove the M2M table again - with connection.schema_editor() as editor: - editor.remove_field( - Author, - new_field, - ) - # Ensure there's no m2m table there - self.assertRaises(DatabaseError, self.column_classes, new_field.rel.through) - finally: - # Cleanup model states - AuthorWithM2M._meta.local_many_to_many.remove(new_field) + # Remove the M2M table again + with connection.schema_editor() as editor: + editor.remove_field(LocalAuthorWithM2M, new_field) + # Ensure there's no m2m table there + self.assertRaises(DatabaseError, self.column_classes, new_field.rel.through) - def test_m2m_through_alter(self): + def test_m2m(self): + self._test_m2m(ManyToManyField) + + def test_m2m_custom(self): + self._test_m2m(CustomManyToManyField) + + def test_m2m_inherited(self): + self._test_m2m(InheritedManyToManyField) + + def _test_m2m_through_alter(self, M2MFieldClass): """ Tests altering M2Ms with explicit through models (should no-op) """ + class LocalAuthorTag(Model): + author = ForeignKey("schema.LocalAuthorWithM2MThrough") + tag = ForeignKey("schema.TagM2MTest") + + class Meta: + apps = new_apps + + class LocalAuthorWithM2MThrough(Model): + name = CharField(max_length=255) + tags = M2MFieldClass("schema.TagM2MTest", related_name="authors", through=LocalAuthorTag) + + class Meta: + apps = new_apps + + self.local_models = [LocalAuthorTag, LocalAuthorWithM2MThrough] + # Create the tables with connection.schema_editor() as editor: - editor.create_model(AuthorTag) - editor.create_model(AuthorWithM2MThrough) + editor.create_model(LocalAuthorTag) + editor.create_model(LocalAuthorWithM2MThrough) editor.create_model(TagM2MTest) # Ensure the m2m table is there - self.assertEqual(len(self.column_classes(AuthorTag)), 3) + self.assertEqual(len(self.column_classes(LocalAuthorTag)), 3) # "Alter" the field's blankness. This should not actually do anything. + old_field = LocalAuthorWithM2MThrough._meta.get_field("tags") + new_field = M2MFieldClass("schema.TagM2MTest", related_name="authors", through=LocalAuthorTag) + new_field.contribute_to_class(LocalAuthorWithM2MThrough, "tags") with connection.schema_editor() as editor: - old_field = AuthorWithM2MThrough._meta.get_field("tags") - 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, - ) + editor.alter_field(LocalAuthorWithM2MThrough, old_field, new_field) # Ensure the m2m table is still there - self.assertEqual(len(self.column_classes(AuthorTag)), 3) + self.assertEqual(len(self.column_classes(LocalAuthorTag)), 3) - def test_m2m_repoint(self): + def test_m2m_through_alter(self): + self._test_m2m_through_alter(ManyToManyField) + + def test_m2m_through_alter_custom(self): + self._test_m2m_through_alter(CustomManyToManyField) + + def test_m2m_through_alter_inherited(self): + self._test_m2m_through_alter(InheritedManyToManyField) + + def _test_m2m_repoint(self, M2MFieldClass): """ Tests repointing M2M fields """ + class LocalBookWithM2M(Model): + author = ForeignKey(Author) + title = CharField(max_length=100, db_index=True) + pub_date = DateTimeField() + tags = M2MFieldClass("TagM2MTest", related_name="books") + + class Meta: + apps = new_apps + + self.local_models = [LocalBookWithM2M] + # Create the tables with connection.schema_editor() as editor: editor.create_model(Author) - editor.create_model(BookWithM2M) + editor.create_model(LocalBookWithM2M) editor.create_model(TagM2MTest) editor.create_model(UniqueTest) # Ensure the M2M exists and points to TagM2MTest - constraints = self.get_constraints(BookWithM2M._meta.get_field("tags").rel.through._meta.db_table) + constraints = self.get_constraints(LocalBookWithM2M._meta.get_field("tags").rel.through._meta.db_table) if connection.features.supports_foreign_keys: for name, details in constraints.items(): if details['columns'] == ["tagm2mtest_id"] and details['foreign_key']: @@ -795,33 +835,31 @@ class SchemaTests(TransactionTestCase): else: self.fail("No FK constraint for tagm2mtest_id found") # Repoint the M2M - new_field = ManyToManyField(UniqueTest) - new_field.contribute_to_class(BookWithM2M, "uniques") - try: - with connection.schema_editor() as editor: - editor.alter_field( - Author, - BookWithM2M._meta.get_field("tags"), - new_field, - ) - # Ensure old M2M is gone - self.assertRaises(DatabaseError, self.column_classes, BookWithM2M._meta.get_field("tags").rel.through) - # Ensure the new M2M exists and points to UniqueTest - constraints = self.get_constraints(new_field.rel.through._meta.db_table) - if connection.features.supports_foreign_keys: - for name, details in constraints.items(): - if details['columns'] == ["uniquetest_id"] and details['foreign_key']: - self.assertEqual(details['foreign_key'], ('schema_uniquetest', 'id')) - break - else: - self.fail("No FK constraint for uniquetest_id found") - finally: - # Cleanup through table separately - with connection.schema_editor() as editor: - editor.remove_field(BookWithM2M, BookWithM2M._meta.get_field("uniques")) - # Cleanup model states - BookWithM2M._meta.local_many_to_many.remove(new_field) - BookWithM2M._meta._expire_cache() + old_field = LocalBookWithM2M._meta.get_field("tags") + new_field = M2MFieldClass(UniqueTest) + new_field.contribute_to_class(LocalBookWithM2M, "uniques") + with connection.schema_editor() as editor: + editor.alter_field(LocalBookWithM2M, old_field, new_field) + # Ensure old M2M is gone + self.assertRaises(DatabaseError, self.column_classes, LocalBookWithM2M._meta.get_field("tags").rel.through) + # Ensure the new M2M exists and points to UniqueTest + constraints = self.get_constraints(new_field.rel.through._meta.db_table) + if connection.features.supports_foreign_keys: + for name, details in constraints.items(): + if details['columns'] == ["uniquetest_id"] and details['foreign_key']: + self.assertEqual(details['foreign_key'], ('schema_uniquetest', 'id')) + break + else: + self.fail("No FK constraint for uniquetest_id found") + + def test_m2m_repoint(self): + self._test_m2m_repoint(ManyToManyField) + + def test_m2m_repoint_custom(self): + self._test_m2m_repoint(CustomManyToManyField) + + def test_m2m_repoint_inherited(self): + self._test_m2m_repoint(InheritedManyToManyField) @unittest.skipUnless(connection.features.supports_column_check_constraints, "No check constraints") def test_check_constraints(self): @@ -839,27 +877,19 @@ class SchemaTests(TransactionTestCase): else: self.fail("No check constraint for height found") # Alter the column to remove it + old_field = Author._meta.get_field("height") new_field = IntegerField(null=True, blank=True) new_field.set_attributes_from_name("height") with connection.schema_editor() as editor: - editor.alter_field( - Author, - Author._meta.get_field("height"), - new_field, - strict=True, - ) + editor.alter_field(Author, old_field, new_field, strict=True) constraints = self.get_constraints(Author._meta.db_table) for name, details in constraints.items(): if details['columns'] == ["height"] and details['check']: self.fail("Check constraint for height found") # Alter the column to re-add it + new_field2 = Author._meta.get_field("height") with connection.schema_editor() as editor: - editor.alter_field( - Author, - new_field, - Author._meta.get_field("height"), - strict=True, - ) + editor.alter_field(Author, new_field, new_field2, strict=True) constraints = self.get_constraints(Author._meta.db_table) for name, details in constraints.items(): if details['columns'] == ["height"] and details['check']: @@ -879,43 +909,29 @@ class SchemaTests(TransactionTestCase): self.assertRaises(IntegrityError, Tag.objects.create, title="bar", slug="foo") Tag.objects.all().delete() # Alter the slug field to be non-unique + old_field = Tag._meta.get_field("slug") new_field = SlugField(unique=False) new_field.set_attributes_from_name("slug") with connection.schema_editor() as editor: - editor.alter_field( - Tag, - Tag._meta.get_field("slug"), - new_field, - strict=True, - ) + editor.alter_field(Tag, old_field, new_field, strict=True) # Ensure the field is no longer unique Tag.objects.create(title="foo", slug="foo") Tag.objects.create(title="bar", slug="foo") Tag.objects.all().delete() # Alter the slug field to be unique - new_new_field = SlugField(unique=True) - new_new_field.set_attributes_from_name("slug") + new_field2 = SlugField(unique=True) + new_field2.set_attributes_from_name("slug") with connection.schema_editor() as editor: - editor.alter_field( - Tag, - new_field, - new_new_field, - strict=True, - ) + editor.alter_field(Tag, new_field, new_field2, strict=True) # Ensure the field is unique again Tag.objects.create(title="foo", slug="foo") self.assertRaises(IntegrityError, Tag.objects.create, title="bar", slug="foo") Tag.objects.all().delete() # Rename the field - new_field = SlugField(unique=False) - new_field.set_attributes_from_name("slug2") + new_field3 = SlugField(unique=True) + new_field3.set_attributes_from_name("slug2") with connection.schema_editor() as editor: - editor.alter_field( - Tag, - Tag._meta.get_field("slug"), - TagUniqueRename._meta.get_field("slug2"), - strict=True, - ) + editor.alter_field(Tag, new_field2, new_field3, strict=True) # Ensure the field is still unique TagUniqueRename.objects.create(title="foo", slug2="foo") self.assertRaises(IntegrityError, TagUniqueRename.objects.create, title="bar", slug2="foo") @@ -936,24 +952,16 @@ class SchemaTests(TransactionTestCase): UniqueTest.objects.all().delete() # Alter the model to its non-unique-together companion with connection.schema_editor() as editor: - editor.alter_unique_together( - UniqueTest, - UniqueTest._meta.unique_together, - [], - ) + editor.alter_unique_together(UniqueTest, UniqueTest._meta.unique_together, []) # Ensure the fields are no longer unique UniqueTest.objects.create(year=2012, slug="foo") UniqueTest.objects.create(year=2012, slug="foo") UniqueTest.objects.all().delete() # Alter it back - new_new_field = SlugField(unique=True) - new_new_field.set_attributes_from_name("slug") + new_field2 = SlugField(unique=True) + new_field2.set_attributes_from_name("slug") with connection.schema_editor() as editor: - editor.alter_unique_together( - UniqueTest, - [], - UniqueTest._meta.unique_together, - ) + editor.alter_unique_together(UniqueTest, [], UniqueTest._meta.unique_together) # Ensure the fields are unique again UniqueTest.objects.create(year=2012, slug="foo") self.assertRaises(IntegrityError, UniqueTest.objects.create, year=2012, slug="foo") @@ -977,11 +985,7 @@ class SchemaTests(TransactionTestCase): ) # Alter the model to add an index with connection.schema_editor() as editor: - editor.alter_index_together( - Tag, - [], - [("slug", "title")], - ) + editor.alter_index_together(Tag, [], [("slug", "title")]) # Ensure there is now an index self.assertEqual( True, @@ -992,14 +996,10 @@ class SchemaTests(TransactionTestCase): ), ) # Alter it back - new_new_field = SlugField(unique=True) - new_new_field.set_attributes_from_name("slug") + new_field2 = SlugField(unique=True) + new_field2.set_attributes_from_name("slug") with connection.schema_editor() as editor: - editor.alter_index_together( - Tag, - [("slug", "title")], - [], - ) + editor.alter_index_together(Tag, [("slug", "title")], []) # Ensure there's no index self.assertEqual( False, @@ -1039,22 +1039,14 @@ class SchemaTests(TransactionTestCase): self.assertEqual(columns['name'][0], "CharField") # Alter the table with connection.schema_editor() as editor: - editor.alter_db_table( - Author, - "schema_author", - "schema_otherauthor", - ) + editor.alter_db_table(Author, "schema_author", "schema_otherauthor") # Ensure the table is there afterwards Author._meta.db_table = "schema_otherauthor" columns = self.column_classes(Author) self.assertEqual(columns['name'][0], "CharField") # Alter the table again with connection.schema_editor() as editor: - editor.alter_db_table( - Author, - "schema_otherauthor", - "schema_author", - ) + editor.alter_db_table(Author, "schema_otherauthor", "schema_author") # Ensure the table is still there Author._meta.db_table = "schema_author" columns = self.column_classes(Author) @@ -1074,53 +1066,38 @@ class SchemaTests(TransactionTestCase): self.get_indexes(Book._meta.db_table), ) # Alter to remove the index + old_field = Book._meta.get_field("title") new_field = CharField(max_length=100, db_index=False) new_field.set_attributes_from_name("title") with connection.schema_editor() as editor: - editor.alter_field( - Book, - Book._meta.get_field("title"), - new_field, - strict=True, - ) + editor.alter_field(Book, old_field, new_field, strict=True) # Ensure the table is there and has no index self.assertNotIn( "title", self.get_indexes(Book._meta.db_table), ) # Alter to re-add the index + new_field2 = Book._meta.get_field("title") with connection.schema_editor() as editor: - editor.alter_field( - Book, - new_field, - Book._meta.get_field("title"), - strict=True, - ) + editor.alter_field(Book, new_field, new_field2, strict=True) # Ensure the table is there and has the index again self.assertIn( "title", self.get_indexes(Book._meta.db_table), ) # Add a unique column, verify that creates an implicit index + new_field3 = BookWithSlug._meta.get_field("slug") with connection.schema_editor() as editor: - editor.add_field( - Book, - BookWithSlug._meta.get_field("slug"), - ) + editor.add_field(Book, new_field3) self.assertIn( "slug", self.get_indexes(Book._meta.db_table), ) # Remove the unique, check the index goes with it - new_field2 = CharField(max_length=20, unique=False) - new_field2.set_attributes_from_name("slug") + new_field4 = CharField(max_length=20, unique=False) + new_field4.set_attributes_from_name("slug") with connection.schema_editor() as editor: - editor.alter_field( - BookWithSlug, - BookWithSlug._meta.get_field("slug"), - new_field2, - strict=True, - ) + editor.alter_field(BookWithSlug, new_field3, new_field4, strict=True) self.assertNotIn( "slug", self.get_indexes(Book._meta.db_table), @@ -1138,16 +1115,14 @@ class SchemaTests(TransactionTestCase): self.get_indexes(Tag._meta.db_table)['id']['primary_key'], ) # Alter to change the PK + id_field = Tag._meta.get_field("id") + old_field = Tag._meta.get_field("slug") new_field = SlugField(primary_key=True) new_field.set_attributes_from_name("slug") new_field.model = Tag with connection.schema_editor() as editor: - editor.remove_field(Tag, Tag._meta.get_field("id")) - editor.alter_field( - Tag, - Tag._meta.get_field("slug"), - new_field, - ) + editor.remove_field(Tag, id_field) + editor.alter_field(Tag, old_field, new_field) # Ensure the PK changed self.assertNotIn( 'id', @@ -1203,10 +1178,7 @@ class SchemaTests(TransactionTestCase): new_field = ForeignKey(AuthorWithEvenLongerName, related_name="something") new_field.set_attributes_from_name("author_other_really_long_named_i_mean_so_long_fk") with connection.schema_editor() as editor: - editor.add_field( - BookWithLongName, - new_field, - ) + editor.add_field(BookWithLongName, new_field) def test_creation_deletion_reserved_names(self): """ @@ -1305,50 +1277,6 @@ class SchemaTests(TransactionTestCase): item = cursor.fetchall()[0] self.assertEqual(item[0], None if connection.features.interprets_empty_strings_as_nulls else '') - def test_custom_manytomanyfield(self): - """ - #24104 - Schema editors should look for many_to_many - """ - # Create the tables - with connection.schema_editor() as editor: - editor.create_model(AuthorWithM2M) - editor.create_model(TagM2MTest) - # Create an M2M field - new_field = CustomManyToManyField("schema.TagM2MTest", related_name="authors") - new_field.contribute_to_class(AuthorWithM2M, "tags") - # Ensure there's no m2m table there - self.assertRaises(DatabaseError, self.column_classes, new_field.rel.through) - try: - # Add the field - with connection.schema_editor() as editor: - editor.add_field( - AuthorWithM2M, - new_field, - ) - # Ensure there is now an m2m table there - columns = self.column_classes(new_field.rel.through) - self.assertEqual(columns['tagm2mtest_id'][0], "IntegerField") - - # "Alter" the field. This should not rename the DB table to itself. - with connection.schema_editor() as editor: - editor.alter_field( - AuthorWithM2M, - new_field, - new_field, - ) - - # Remove the M2M table again - with connection.schema_editor() as editor: - editor.remove_field( - AuthorWithM2M, - new_field, - ) - # Ensure there's no m2m table there - self.assertRaises(DatabaseError, self.column_classes, new_field.rel.through) - finally: - # Cleanup model states - AuthorWithM2M._meta.local_many_to_many.remove(new_field) - def test_add_field_default_dropped(self): # Create the table with connection.schema_editor() as editor: