diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/modeltests/delete/models.py | 20 | ||||
| -rw-r--r-- | tests/modeltests/delete/tests.py | 101 | ||||
| -rw-r--r-- | tests/regressiontests/admin_util/models.py | 3 | ||||
| -rw-r--r-- | tests/regressiontests/admin_util/tests.py | 13 | ||||
| -rw-r--r-- | tests/regressiontests/delete_regress/tests.py | 11 | ||||
| -rw-r--r-- | tests/regressiontests/dispatch/tests/test_dispatcher.py | 12 |
6 files changed, 148 insertions, 12 deletions
diff --git a/tests/modeltests/delete/models.py b/tests/modeltests/delete/models.py index e0cec426ea..65d4e6f725 100644 --- a/tests/modeltests/delete/models.py +++ b/tests/modeltests/delete/models.py @@ -95,7 +95,7 @@ class MRNull(models.Model): class Avatar(models.Model): - pass + desc = models.TextField(null=True) class User(models.Model): @@ -108,3 +108,21 @@ class HiddenUser(models.Model): class HiddenUserProfile(models.Model): user = models.ForeignKey(HiddenUser) + +class M2MTo(models.Model): + pass + +class M2MFrom(models.Model): + m2m = models.ManyToManyField(M2MTo) + +class Parent(models.Model): + pass + +class Child(Parent): + pass + +class Base(models.Model): + pass + +class RelToBase(models.Model): + base = models.ForeignKey(Base, on_delete=models.DO_NOTHING) diff --git a/tests/modeltests/delete/tests.py b/tests/modeltests/delete/tests.py index 26f2fd52c1..2610cb4b39 100644 --- a/tests/modeltests/delete/tests.py +++ b/tests/modeltests/delete/tests.py @@ -1,11 +1,12 @@ from __future__ import absolute_import -from django.db import models, IntegrityError +from django.db import models, IntegrityError, connection from django.test import TestCase, skipUnlessDBFeature, skipIfDBFeature from django.utils.six.moves import xrange from .models import (R, RChild, S, T, U, A, M, MR, MRNull, - create_a, get_default_r, User, Avatar, HiddenUser, HiddenUserProfile) + create_a, get_default_r, User, Avatar, HiddenUser, HiddenUserProfile, + M2MTo, M2MFrom, Parent, Child, Base) class OnDeleteTests(TestCase): @@ -74,6 +75,16 @@ class OnDeleteTests(TestCase): self.assertEqual(replacement_r, a.donothing) models.signals.pre_delete.disconnect(check_do_nothing) + def test_do_nothing_qscount(self): + """ + Test that a models.DO_NOTHING relation doesn't trigger a query. + """ + b = Base.objects.create() + with self.assertNumQueries(1): + # RelToBase should not be queried. + b.delete() + self.assertEqual(Base.objects.count(), 0) + def test_inheritance_cascade_up(self): child = RChild.objects.create() child.delete() @@ -229,16 +240,34 @@ class DeletionTests(TestCase): # 1 query to delete the avatar # The important thing is that when we can defer constraint checks there # is no need to do an UPDATE on User.avatar to null it out. + + # Attach a signal to make sure we will not do fast_deletes. + calls = [] + def noop(*args, **kwargs): + calls.append('') + models.signals.post_delete.connect(noop, sender=User) + self.assertNumQueries(3, a.delete) self.assertFalse(User.objects.exists()) self.assertFalse(Avatar.objects.exists()) + self.assertEquals(len(calls), 1) + models.signals.post_delete.disconnect(noop, sender=User) @skipIfDBFeature("can_defer_constraint_checks") def test_cannot_defer_constraint_checks(self): u = User.objects.create( avatar=Avatar.objects.create() ) + # Attach a signal to make sure we will not do fast_deletes. + calls = [] + def noop(*args, **kwargs): + calls.append('') + models.signals.post_delete.connect(noop, sender=User) + a = Avatar.objects.get(pk=u.avatar_id) + # The below doesn't make sense... Why do we need to null out + # user.avatar if we are going to delete the user immediately after it, + # and there are no more cascades. # 1 query to find the users for the avatar. # 1 query to delete the user # 1 query to null out user.avatar, because we can't defer the constraint @@ -246,6 +275,8 @@ class DeletionTests(TestCase): self.assertNumQueries(4, a.delete) self.assertFalse(User.objects.exists()) self.assertFalse(Avatar.objects.exists()) + self.assertEquals(len(calls), 1) + models.signals.post_delete.disconnect(noop, sender=User) def test_hidden_related(self): r = R.objects.create() @@ -254,3 +285,69 @@ class DeletionTests(TestCase): r.delete() self.assertEqual(HiddenUserProfile.objects.count(), 0) + +class FastDeleteTests(TestCase): + + def test_fast_delete_fk(self): + u = User.objects.create( + avatar=Avatar.objects.create() + ) + a = Avatar.objects.get(pk=u.avatar_id) + # 1 query to fast-delete the user + # 1 query to delete the avatar + self.assertNumQueries(2, a.delete) + self.assertFalse(User.objects.exists()) + self.assertFalse(Avatar.objects.exists()) + + def test_fast_delete_m2m(self): + t = M2MTo.objects.create() + f = M2MFrom.objects.create() + f.m2m.add(t) + # 1 to delete f, 1 to fast-delete m2m for f + self.assertNumQueries(2, f.delete) + + def test_fast_delete_revm2m(self): + t = M2MTo.objects.create() + f = M2MFrom.objects.create() + f.m2m.add(t) + # 1 to delete t, 1 to fast-delete t's m_set + self.assertNumQueries(2, f.delete) + + def test_fast_delete_qs(self): + u1 = User.objects.create() + u2 = User.objects.create() + self.assertNumQueries(1, User.objects.filter(pk=u1.pk).delete) + self.assertEquals(User.objects.count(), 1) + self.assertTrue(User.objects.filter(pk=u2.pk).exists()) + + def test_fast_delete_joined_qs(self): + a = Avatar.objects.create(desc='a') + User.objects.create(avatar=a) + u2 = User.objects.create() + expected_queries = 1 if connection.features.update_can_self_select else 2 + self.assertNumQueries(expected_queries, + User.objects.filter(avatar__desc='a').delete) + self.assertEquals(User.objects.count(), 1) + self.assertTrue(User.objects.filter(pk=u2.pk).exists()) + + def test_fast_delete_inheritance(self): + c = Child.objects.create() + p = Parent.objects.create() + # 1 for self, 1 for parent + # However, this doesn't work as child.parent access creates a query, + # and this means we will be generating extra queries (a lot for large + # querysets). This is not a fast-delete problem. + # self.assertNumQueries(2, c.delete) + c.delete() + self.assertFalse(Child.objects.exists()) + self.assertEquals(Parent.objects.count(), 1) + self.assertEquals(Parent.objects.filter(pk=p.pk).count(), 1) + # 1 for self delete, 1 for fast delete of empty "child" qs. + self.assertNumQueries(2, p.delete) + self.assertFalse(Parent.objects.exists()) + # 1 for self delete, 1 for fast delete of empty "child" qs. + c = Child.objects.create() + p = c.parent_ptr + self.assertNumQueries(2, p.delete) + self.assertFalse(Parent.objects.exists()) + self.assertFalse(Child.objects.exists()) diff --git a/tests/regressiontests/admin_util/models.py b/tests/regressiontests/admin_util/models.py index b3504a1fa4..32a6cd6291 100644 --- a/tests/regressiontests/admin_util/models.py +++ b/tests/regressiontests/admin_util/models.py @@ -39,3 +39,6 @@ class Guest(models.Model): class Meta: verbose_name = "awesome guest" + +class EventGuide(models.Model): + event = models.ForeignKey(Event, on_delete=models.DO_NOTHING) diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py index d04740ce95..ef8a91d1db 100644 --- a/tests/regressiontests/admin_util/tests.py +++ b/tests/regressiontests/admin_util/tests.py @@ -17,7 +17,7 @@ from django.utils.formats import localize from django.utils.safestring import mark_safe from django.utils import six -from .models import Article, Count, Event, Location +from .models import Article, Count, Event, Location, EventGuide class NestedObjectsTests(TestCase): @@ -71,6 +71,17 @@ class NestedObjectsTests(TestCase): # Should not require additional queries to populate the nested graph. self.assertNumQueries(2, self._collect, 0) + def test_on_delete_do_nothing(self): + """ + Check that the nested collector doesn't query for DO_NOTHING objects. + """ + n = NestedObjects(using=DEFAULT_DB_ALIAS) + objs = [Event.objects.create()] + EventGuide.objects.create(event=objs[0]) + with self.assertNumQueries(2): + # One for Location, one for Guest, and no query for EventGuide + n.collect(objs) + class UtilTests(unittest.TestCase): def test_values_from_lookup_field(self): """ diff --git a/tests/regressiontests/delete_regress/tests.py b/tests/regressiontests/delete_regress/tests.py index 32feae2ded..ebe59bffd7 100644 --- a/tests/regressiontests/delete_regress/tests.py +++ b/tests/regressiontests/delete_regress/tests.py @@ -3,7 +3,7 @@ from __future__ import absolute_import import datetime from django.conf import settings -from django.db import backend, transaction, DEFAULT_DB_ALIAS +from django.db import backend, transaction, DEFAULT_DB_ALIAS, models from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature from .models import (Book, Award, AwardNote, Person, Child, Toy, PlayedWith, @@ -139,17 +139,24 @@ class DeleteCascadeTransactionTests(TransactionTestCase): 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. diff --git a/tests/regressiontests/dispatch/tests/test_dispatcher.py b/tests/regressiontests/dispatch/tests/test_dispatcher.py index 4e4669d34c..5f8f92acaf 100644 --- a/tests/regressiontests/dispatch/tests/test_dispatcher.py +++ b/tests/regressiontests/dispatch/tests/test_dispatcher.py @@ -127,15 +127,15 @@ class DispatcherTests(unittest.TestCase): self._testIsClean(a_signal) def test_has_listeners(self): - self.assertIs(a_signal.has_listeners(), False) - self.assertIs(a_signal.has_listeners(sender=object()), False) + self.assertFalse(a_signal.has_listeners()) + self.assertFalse(a_signal.has_listeners(sender=object())) receiver_1 = Callable() a_signal.connect(receiver_1) - self.assertIs(a_signal.has_listeners(), True) - self.assertIs(a_signal.has_listeners(sender=object()), True) + self.assertTrue(a_signal.has_listeners()) + self.assertTrue(a_signal.has_listeners(sender=object())) a_signal.disconnect(receiver_1) - self.assertIs(a_signal.has_listeners(), False) - self.assertIs(a_signal.has_listeners(sender=object()), False) + self.assertFalse(a_signal.has_listeners()) + self.assertFalse(a_signal.has_listeners(sender=object())) class ReceiverTestCase(unittest.TestCase): |
