diff options
| author | Russell Keith-Magee <russell@keith-magee.com> | 2010-03-15 13:15:01 +0000 |
|---|---|---|
| committer | Russell Keith-Magee <russell@keith-magee.com> | 2010-03-15 13:15:01 +0000 |
| commit | 2d57300f5298c0dc88789fb24922f423e34e9149 (patch) | |
| tree | 81f4a4f794ee6c68cff1817a4e2faf7543b6ee38 /tests | |
| parent | d8c9d21c33ce08f7e90e5996d4da9d8d25d5f8c4 (diff) | |
Fixed #12953 -- Ensure that deletion cascades through generic relations. Also cleans up the special-casing of generic relations in the deleted object discovery process. Thanks to carljm for the report and patch.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@12790 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/regressiontests/delete_regress/models.py | 81 | ||||
| -rw-r--r-- | tests/regressiontests/delete_regress/tests.py | 108 |
2 files changed, 136 insertions, 53 deletions
diff --git a/tests/regressiontests/delete_regress/models.py b/tests/regressiontests/delete_regress/models.py index ff561c9d20..4f61e78513 100644 --- a/tests/regressiontests/delete_regress/models.py +++ b/tests/regressiontests/delete_regress/models.py @@ -1,62 +1,37 @@ -from django.conf import settings -from django.db import models, backend, connection, transaction, DEFAULT_DB_ALIAS -from django.db.models import sql, query -from django.test import TransactionTestCase +from django.db import models -class Book(models.Model): - pagecount = models.IntegerField() - -# Can't run this test under SQLite, because you can't -# get two connections to an in-memory database. -if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.sqlite3': - class DeleteLockingTest(TransactionTestCase): - def setUp(self): - # Create a second connection to the database - conn_settings = settings.DATABASES[DEFAULT_DB_ALIAS] - self.conn2 = backend.DatabaseWrapper({ - 'HOST': conn_settings['HOST'], - 'NAME': conn_settings['NAME'], - 'OPTIONS': conn_settings['OPTIONS'], - 'PASSWORD': conn_settings['PASSWORD'], - 'PORT': conn_settings['PORT'], - 'USER': conn_settings['USER'], - 'TIME_ZONE': settings.TIME_ZONE, - }) +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType - # Put both DB connections into managed transaction mode - transaction.enter_transaction_management() - transaction.managed(True) - self.conn2._enter_transaction_management(True) +class Award(models.Model): + name = models.CharField(max_length=25) + object_id = models.PositiveIntegerField() + content_type = models.ForeignKey(ContentType) + content_object = generic.GenericForeignKey() - def tearDown(self): - # Close down the second connection. - transaction.leave_transaction_management() - self.conn2.close() +class AwardNote(models.Model): + award = models.ForeignKey(Award) + note = models.CharField(max_length=100) - def test_concurrent_delete(self): - "Deletes on concurrent transactions don't collide and lock the database. Regression for #9479" +class Person(models.Model): + name = models.CharField(max_length=25) + awards = generic.GenericRelation(Award) - # 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() +class Book(models.Model): + pagecount = models.IntegerField() - transaction.commit() +class Toy(models.Model): + name = models.CharField(max_length=50) - self.assertEquals(3, Book.objects.count()) +class Child(models.Model): + name = models.CharField(max_length=50) + toys = models.ManyToManyField(Toy, through='PlayedWith') - # Delete something using connection 2. - cursor2 = self.conn2.cursor() - cursor2.execute('DELETE from delete_regress_book WHERE id=1') - self.conn2._commit(); +class PlayedWith(models.Model): + child = models.ForeignKey(Child) + toy = models.ForeignKey(Toy) + date = models.DateField() - # 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.assertEquals(1, Book.objects.count()) +class PlayedWithNote(models.Model): + played = models.ForeignKey(PlayedWith) + note = models.TextField() diff --git a/tests/regressiontests/delete_regress/tests.py b/tests/regressiontests/delete_regress/tests.py new file mode 100644 index 0000000000..7ebe5e86a4 --- /dev/null +++ b/tests/regressiontests/delete_regress/tests.py @@ -0,0 +1,108 @@ +import datetime + +from django.conf import settings +from django.db import backend, connection, transaction, DEFAULT_DB_ALIAS +from django.test import TestCase, TransactionTestCase + +from models import Book, Award, AwardNote, Person, Child, Toy, PlayedWith, PlayedWithNote + +# Can't run this test under SQLite, because you can't +# get two connections to an in-memory database. +if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.sqlite3': + class DeleteLockingTest(TransactionTestCase): + def setUp(self): + # Create a second connection to the default database + conn_settings = settings.DATABASES[DEFAULT_DB_ALIAS] + self.conn2 = backend.DatabaseWrapper({ + 'HOST': conn_settings['HOST'], + 'NAME': conn_settings['NAME'], + 'OPTIONS': conn_settings['OPTIONS'], + 'PASSWORD': conn_settings['PASSWORD'], + 'PORT': conn_settings['PORT'], + 'USER': conn_settings['USER'], + 'TIME_ZONE': settings.TIME_ZONE, + }) + + # 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() + + 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.assertEquals(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.assertEquals(1, Book.objects.count()) + +class DeleteCascadeTests(TestCase): + def test_generic_relation_cascade(self): + """ + Test that Django cascades deletes through generic-related + objects to their reverse relations. + + This might falsely succeed if the database cascades deletes + itself immediately; the postgresql_psycopg2 backend does not + give such a false success because ForeignKeys are created with + DEFERRABLE INITIALLY DEFERRED, so its internal cascade is + delayed until transaction commit. + + """ + 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.assertEquals(AwardNote.objects.count(), 1) + person.delete() + self.assertEquals(Award.objects.count(), 0) + # first two asserts are just sanity checks, this is the kicker: + self.assertEquals(AwardNote.objects.count(), 0) + + def test_fk_to_m2m_through(self): + """ + Test that if a 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. + + Like the above test, this could in theory falsely succeed if + the DB cascades deletes itself immediately. + + """ + 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.assertEquals(PlayedWithNote.objects.count(), 1) + paints.delete() + self.assertEquals(PlayedWith.objects.count(), 0) + # first two asserts just sanity checks, this is the kicker: + self.assertEquals(PlayedWithNote.objects.count(), 0) |
