diff options
author | Florian Apolloner <florian@apolloner.eu> | 2013-02-26 09:53:47 +0100 |
---|---|---|
committer | Florian Apolloner <florian@apolloner.eu> | 2013-02-26 14:36:57 +0100 |
commit | 89f40e36246100df6a11316c31a76712ebc6c501 (patch) | |
tree | 6e65639683ddaf2027908d1ecb1739e0e2ff853b /tests/delete_regress | |
parent | b3d2ccb5bfbaf6e7fe1f98843baaa48c35a70950 (diff) | |
download | django-89f40e36246100df6a11316c31a76712ebc6c501.tar.gz |
Merged regressiontests and modeltests into the test root.
Diffstat (limited to 'tests/delete_regress')
-rw-r--r-- | tests/delete_regress/__init__.py | 1 | ||||
-rw-r--r-- | tests/delete_regress/models.py | 111 | ||||
-rw-r--r-- | tests/delete_regress/tests.py | 360 |
3 files changed, 472 insertions, 0 deletions
diff --git a/tests/delete_regress/__init__.py b/tests/delete_regress/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/tests/delete_regress/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/delete_regress/models.py b/tests/delete_regress/models.py new file mode 100644 index 0000000000..3632a7dbc1 --- /dev/null +++ b/tests/delete_regress/models.py @@ -0,0 +1,111 @@ +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType +from django.db import models + +class Award(models.Model): + name = models.CharField(max_length=25) + object_id = models.PositiveIntegerField() + content_type = models.ForeignKey(ContentType) + content_object = generic.GenericForeignKey() + +class AwardNote(models.Model): + award = models.ForeignKey(Award) + note = models.CharField(max_length=100) + +class Person(models.Model): + name = models.CharField(max_length=25) + awards = generic.GenericRelation(Award) + +class Book(models.Model): + pagecount = models.IntegerField() + +class Toy(models.Model): + name = models.CharField(max_length=50) + +class Child(models.Model): + name = models.CharField(max_length=50) + toys = models.ManyToManyField(Toy, through='PlayedWith') + +class PlayedWith(models.Model): + child = models.ForeignKey(Child) + toy = models.ForeignKey(Toy) + date = models.DateField(db_column='date_col') + +class PlayedWithNote(models.Model): + played = models.ForeignKey(PlayedWith) + note = models.TextField() + +class Contact(models.Model): + label = models.CharField(max_length=100) + +class Email(Contact): + email_address = models.EmailField(max_length=100) + +class Researcher(models.Model): + contacts = models.ManyToManyField(Contact, related_name="research_contacts") + +class Food(models.Model): + name = models.CharField(max_length=20, unique=True) + +class Eaten(models.Model): + food = models.ForeignKey(Food, to_field="name") + meal = models.CharField(max_length=20) + + +# Models for #15776 + +class Policy(models.Model): + policy_number = models.CharField(max_length=10) + +class Version(models.Model): + policy = models.ForeignKey(Policy) + +class Location(models.Model): + version = models.ForeignKey(Version, blank=True, null=True) + +class Item(models.Model): + version = models.ForeignKey(Version) + location = models.ForeignKey(Location, blank=True, null=True) + +# Models for #16128 + +class File(models.Model): + pass + +class Image(File): + class Meta: + proxy = True + +class Photo(Image): + class Meta: + proxy = True + +class FooImage(models.Model): + my_image = models.ForeignKey(Image) + +class FooFile(models.Model): + my_file = models.ForeignKey(File) + +class FooPhoto(models.Model): + my_photo = models.ForeignKey(Photo) + +class FooFileProxy(FooFile): + class Meta: + proxy = True + +class OrgUnit(models.Model): + name = models.CharField(max_length=64, unique=True) + +class Login(models.Model): + description = models.CharField(max_length=32) + orgunit = models.ForeignKey(OrgUnit) + +class House(models.Model): + address = models.CharField(max_length=32) + +class OrderedPerson(models.Model): + name = models.CharField(max_length=32) + lives_in = models.ForeignKey(House) + + class Meta: + ordering = ['name'] diff --git a/tests/delete_regress/tests.py b/tests/delete_regress/tests.py new file mode 100644 index 0000000000..e007ebdd76 --- /dev/null +++ b/tests/delete_regress/tests.py @@ -0,0 +1,360 @@ +from __future__ import absolute_import + +import datetime + +from django.conf import settings +from django.db import transaction, DEFAULT_DB_ALIAS, models +from django.db.utils import ConnectionHandler +from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature + +from .models import (Book, Award, AwardNote, Person, Child, Toy, PlayedWith, + PlayedWithNote, Email, Researcher, Food, Eaten, Policy, Version, Location, + Item, Image, File, Photo, FooFile, FooImage, FooPhoto, FooFileProxy, Login, + OrgUnit, OrderedPerson, House) + + +# Can't run this test under SQLite, because you can't +# get two connections to an in-memory database. +class DeleteLockingTest(TransactionTestCase): + def setUp(self): + # Create a second connection to the default database + new_connections = ConnectionHandler(settings.DATABASES) + self.conn2 = new_connections[DEFAULT_DB_ALIAS] + # Put both DB connections into managed transaction mode + transaction.enter_transaction_management() + transaction.managed(True) + self.conn2._enter_transaction_management(True) + + def tearDown(self): + # Close down the second connection. + transaction.leave_transaction_management() + self.conn2.close() + + @skipUnlessDBFeature('test_db_allows_multiple_connections') + def test_concurrent_delete(self): + "Deletes on concurrent transactions don't collide and lock the database. Regression for #9479" + + # Create some dummy data + b1 = Book(id=1, pagecount=100) + b2 = Book(id=2, pagecount=200) + b3 = Book(id=3, pagecount=300) + b1.save() + b2.save() + b3.save() + + transaction.commit() + + self.assertEqual(3, Book.objects.count()) + + # Delete something using connection 2. + cursor2 = self.conn2.cursor() + cursor2.execute('DELETE from delete_regress_book WHERE id=1') + self.conn2._commit() + + # Now perform a queryset delete that covers the object + # deleted in connection 2. This causes an infinite loop + # under MySQL InnoDB unless we keep track of already + # deleted objects. + Book.objects.filter(pagecount__lt=250).delete() + transaction.commit() + self.assertEqual(1, Book.objects.count()) + transaction.commit() + + +class DeleteCascadeTests(TestCase): + def test_generic_relation_cascade(self): + """ + Django cascades deletes through generic-related objects to their + reverse relations. + + """ + person = Person.objects.create(name='Nelson Mandela') + award = Award.objects.create(name='Nobel', content_object=person) + note = AwardNote.objects.create(note='a peace prize', + award=award) + self.assertEqual(AwardNote.objects.count(), 1) + person.delete() + self.assertEqual(Award.objects.count(), 0) + # first two asserts are just sanity checks, this is the kicker: + self.assertEqual(AwardNote.objects.count(), 0) + + def test_fk_to_m2m_through(self): + """ + If an M2M relationship has an explicitly-specified through model, and + some other model has an FK to that through model, deletion is cascaded + from one of the participants in the M2M, to the through model, to its + related model. + + """ + juan = Child.objects.create(name='Juan') + paints = Toy.objects.create(name='Paints') + played = PlayedWith.objects.create(child=juan, toy=paints, + date=datetime.date.today()) + note = PlayedWithNote.objects.create(played=played, + note='the next Jackson Pollock') + self.assertEqual(PlayedWithNote.objects.count(), 1) + paints.delete() + self.assertEqual(PlayedWith.objects.count(), 0) + # first two asserts just sanity checks, this is the kicker: + self.assertEqual(PlayedWithNote.objects.count(), 0) + + def test_15776(self): + policy = Policy.objects.create(pk=1, policy_number="1234") + version = Version.objects.create(policy=policy) + location = Location.objects.create(version=version) + item = Item.objects.create(version=version, location=location) + policy.delete() + + +class DeleteCascadeTransactionTests(TransactionTestCase): + def test_inheritance(self): + """ + Auto-created many-to-many through tables referencing a parent model are + correctly found by the delete cascade when a child of that parent is + deleted. + + Refs #14896. + """ + r = Researcher.objects.create() + email = Email.objects.create( + label="office-email", email_address="carl@science.edu" + ) + r.contacts.add(email) + + email.delete() + + def test_to_field(self): + """ + Cascade deletion works with ForeignKey.to_field set to non-PK. + + """ + apple = Food.objects.create(name="apple") + eaten = Eaten.objects.create(food=apple, meal="lunch") + + apple.delete() + self.assertFalse(Food.objects.exists()) + self.assertFalse(Eaten.objects.exists()) + + +class LargeDeleteTests(TestCase): + def test_large_deletes(self): + "Regression for #13309 -- if the number of objects > chunk size, deletion still occurs" + for x in range(300): + track = Book.objects.create(pagecount=x+100) + # attach a signal to make sure we will not fast-delete + def noop(*args, **kwargs): + pass + models.signals.post_delete.connect(noop, sender=Book) + Book.objects.all().delete() + models.signals.post_delete.disconnect(noop, sender=Book) + self.assertEqual(Book.objects.count(), 0) + + +class ProxyDeleteTest(TestCase): + """ + Tests on_delete behavior for proxy models. + + See #16128. + + """ + def create_image(self): + """Return an Image referenced by both a FooImage and a FooFile.""" + # Create an Image + test_image = Image() + test_image.save() + foo_image = FooImage(my_image=test_image) + foo_image.save() + + # Get the Image instance as a File + test_file = File.objects.get(pk=test_image.pk) + foo_file = FooFile(my_file=test_file) + foo_file.save() + + return test_image + + + def test_delete_proxy(self): + """ + Deleting the *proxy* instance bubbles through to its non-proxy and + *all* referring objects are deleted. + + """ + self.create_image() + + Image.objects.all().delete() + + # An Image deletion == File deletion + self.assertEqual(len(Image.objects.all()), 0) + self.assertEqual(len(File.objects.all()), 0) + + # The Image deletion cascaded and *all* references to it are deleted. + self.assertEqual(len(FooImage.objects.all()), 0) + self.assertEqual(len(FooFile.objects.all()), 0) + + + def test_delete_proxy_of_proxy(self): + """ + Deleting a proxy-of-proxy instance should bubble through to its proxy + and non-proxy parents, deleting *all* referring objects. + + """ + test_image = self.create_image() + + # Get the Image as a Photo + test_photo = Photo.objects.get(pk=test_image.pk) + foo_photo = FooPhoto(my_photo=test_photo) + foo_photo.save() + + Photo.objects.all().delete() + + # A Photo deletion == Image deletion == File deletion + self.assertEqual(len(Photo.objects.all()), 0) + self.assertEqual(len(Image.objects.all()), 0) + self.assertEqual(len(File.objects.all()), 0) + + # The Photo deletion should have cascaded and deleted *all* + # references to it. + self.assertEqual(len(FooPhoto.objects.all()), 0) + self.assertEqual(len(FooFile.objects.all()), 0) + self.assertEqual(len(FooImage.objects.all()), 0) + + + def test_delete_concrete_parent(self): + """ + Deleting an instance of a concrete model should also delete objects + referencing its proxy subclass. + + """ + self.create_image() + + File.objects.all().delete() + + # A File deletion == Image deletion + self.assertEqual(len(File.objects.all()), 0) + self.assertEqual(len(Image.objects.all()), 0) + + # The File deletion should have cascaded and deleted *all* references + # to it. + self.assertEqual(len(FooFile.objects.all()), 0) + self.assertEqual(len(FooImage.objects.all()), 0) + + + def test_delete_proxy_pair(self): + """ + If a pair of proxy models are linked by an FK from one concrete parent + to the other, deleting one proxy model cascade-deletes the other, and + the deletion happens in the right order (not triggering an + IntegrityError on databases unable to defer integrity checks). + + Refs #17918. + + """ + # Create an Image (proxy of File) and FooFileProxy (proxy of FooFile, + # which has an FK to File) + image = Image.objects.create() + as_file = File.objects.get(pk=image.pk) + FooFileProxy.objects.create(my_file=as_file) + + Image.objects.all().delete() + + self.assertEqual(len(FooFileProxy.objects.all()), 0) + + def test_19187_values(self): + with self.assertRaises(TypeError): + Image.objects.values().delete() + with self.assertRaises(TypeError): + Image.objects.values_list().delete() + +class Ticket19102Tests(TestCase): + """ + Test different queries which alter the SELECT clause of the query. We + also must be using a subquery for the deletion (that is, the original + query has a join in it). The deletion should be done as "fast-path" + deletion (that is, just one query for the .delete() call). + + Note that .values() is not tested here on purpose. .values().delete() + doesn't work for non fast-path deletes at all. + """ + def setUp(self): + self.o1 = OrgUnit.objects.create(name='o1') + self.o2 = OrgUnit.objects.create(name='o2') + self.l1 = Login.objects.create(description='l1', orgunit=self.o1) + self.l2 = Login.objects.create(description='l2', orgunit=self.o2) + + @skipUnlessDBFeature("update_can_self_select") + def test_ticket_19102_annotate(self): + with self.assertNumQueries(1): + Login.objects.order_by('description').filter( + orgunit__name__isnull=False + ).annotate( + n=models.Count('description') + ).filter( + n=1, pk=self.l1.pk + ).delete() + self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists()) + self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists()) + + @skipUnlessDBFeature("update_can_self_select") + def test_ticket_19102_extra(self): + with self.assertNumQueries(1): + Login.objects.order_by('description').filter( + orgunit__name__isnull=False + ).extra( + select={'extraf':'1'} + ).filter( + pk=self.l1.pk + ).delete() + self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists()) + self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists()) + + @skipUnlessDBFeature("update_can_self_select") + @skipUnlessDBFeature('can_distinct_on_fields') + def test_ticket_19102_distinct_on(self): + # Both Login objs should have same description so that only the one + # having smaller PK will be deleted. + Login.objects.update(description='description') + with self.assertNumQueries(1): + Login.objects.distinct('description').order_by('pk').filter( + orgunit__name__isnull=False + ).delete() + # Assumed that l1 which is created first has smaller PK. + self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists()) + self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists()) + + @skipUnlessDBFeature("update_can_self_select") + def test_ticket_19102_select_related(self): + with self.assertNumQueries(1): + Login.objects.filter( + pk=self.l1.pk + ).filter( + orgunit__name__isnull=False + ).order_by( + 'description' + ).select_related('orgunit').delete() + self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists()) + self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists()) + + @skipUnlessDBFeature("update_can_self_select") + def test_ticket_19102_defer(self): + with self.assertNumQueries(1): + Login.objects.filter( + pk=self.l1.pk + ).filter( + orgunit__name__isnull=False + ).order_by( + 'description' + ).only('id').delete() + self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists()) + self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists()) + + +class OrderedDeleteTests(TestCase): + def test_meta_ordered_delete(self): + # When a subquery is performed by deletion code, the subquery must be + # cleared of all ordering. There was a but that caused _meta ordering + # to be used. Refs #19720. + h = House.objects.create(address='Foo') + OrderedPerson.objects.create(name='Jack', lives_in=h) + OrderedPerson.objects.create(name='Bob', lives_in=h) + OrderedPerson.objects.filter(lives_in__address='Foo').delete() + self.assertEqual(OrderedPerson.objects.count(), 0) |