#15776 closed Bug (fixed)
Delete Regression in Django 1.3
Reported by: | Owned by: | Luke Plant | |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | 1.3 |
Severity: | Release blocker | Keywords: | db mysql delete |
Cc: | aaron.l.madison@… | Triage Stage: | Accepted |
Has patch: | no | Needs documentation: | no |
Needs tests: | yes | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
I have found a bug the way Django 1.3 carries out its cascade delete using MySQL. (the operation works fine in django 1.2.5)
Here is a stripped example from our production data.
from django.db import models 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) class ItemRateCode(models.Model): item = models.ForeignKey(Item) class PropertyItem(models.Model): item_rate_code = models.ForeignKey(ItemRateCode) class Coverage(models.Model): version = models.ForeignKey(Version) item_rate_code = models.ForeignKey(ItemRateCode)
After you syncdb, execute the following to load the data and try to delete the policy:
from myapp.models import Policy, Version, Location, Item from myapp.models import ItemRateCode, PropertyItem, Coverage policy = Policy.objects.create(pk=1, policy_number="1234") version = Version.objects.create(policy=policy) location = Location.objects.create(version=version) item1 = Item.objects.create(version=version, location=location) item2 = Item.objects.create(version=version, location=location) # one for each item item_rate_code1 = ItemRateCode.objects.create(item=item1) item_rate_code2 = ItemRateCode.objects.create(item=item2) # one for each item_rate_code coverage1 = Coverage.objects.create(version=version, item_rate_code=item_rate_code1) coverage2 = Coverage.objects.create(version=version, item_rate_code=item_rate_code2) # one for each item_rate_code property_item1 = PropertyItem.objects.create(item_rate_code=item_rate_code1) property_item2 = PropertyItem.objects.create(item_rate_code=item_rate_code2) policy = Policy.objects.get(pk=1) policy.delete()
(I have included a sample project with a failing/blowing up testcase... the test passes on 1.2.5)
For some reason, the delete collector I believe is trying to delete the "Version" before it has deleted the "Item"s
Attachments (3)
Change History (18)
by , 14 years ago
Attachment: | project.zip added |
---|
comment:1 by , 14 years ago
From the django-users thread, here's the failure message:
(delete_bug)amadison@dev-aaron:~/projects/delete_bug/project$ ./manage.py test myapp Creating test database for alias 'default'... E ====================================================================== ERROR: test_deletes_policy_successfully (myapp.tests.DeletePolicyTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/amadison/projects/delete_bug/project/myapp/tests.py", line 34, in test_deletes_policy_successfully self.assertEqual(None, policy.delete()) File "/home/amadison/projects/delete_bug/lib/python2.6/site-packages/django/db/models/base.py", line 581, in delete collector.delete() File "/home/amadison/projects/delete_bug/lib/python2.6/site-packages/django/db/models/deletion.py", line 63, in decorated func(self, *args, **kwargs) File "/home/amadison/projects/delete_bug/lib/python2.6/site-packages/django/db/models/deletion.py", line 254, in delete query.delete_batch(pk_list, self.using) File "/home/amadison/projects/delete_bug/lib/python2.6/site-packages/django/db/models/sql/subqueries.py", line 44, in delete_batch self.do_query(self.model._meta.db_table, where, using=using) File "/home/amadison/projects/delete_bug/lib/python2.6/site-packages/django/db/models/sql/subqueries.py", line 29, in do_query self.get_compiler(using).execute_sql(None) File "/home/amadison/projects/delete_bug/lib/python2.6/site-packages/django/db/models/sql/compiler.py", line 735, in execute_sql cursor.execute(sql, params) File "/home/amadison/projects/delete_bug/lib/python2.6/site-packages/django/db/backends/mysql/base.py", line 86, in execute return self.cursor.execute(query, args) File "/home/amadison/projects/delete_bug/lib/python2.6/site-packages/MySQLdb/cursors.py", line 174, in execute self.errorhandler(self, exc, value) File "/home/amadison/projects/delete_bug/lib/python2.6/site-packages/MySQLdb/connections.py", line 36, in defaulterrorhandler raise errorclass, errorvalue IntegrityError: (1451, 'Cannot delete or update a parent row: a foreign key constraint fails (`test_delete_test`.`myapp_item`, CONSTRAINT `version_id_refs_id_15106961` FOREIGN KEY (`version_id`) REFERENCES `myapp_version` (`id`))') ---------------------------------------------------------------------- Ran 1 test in 0.120s
follow-up: 5 comment:2 by , 14 years ago
I put some print statements in the django.db.models.deletion.py in the Collector.delete method where it does
# delete instances for model, instances in self.data.iteritems(): query = sql.DeleteQuery(model) pk_list = [obj.pk for obj in instances] print "Deleting:", model, pk_list #query.delete_batch(pk_list, self.using)
The output is:
(delete_bug)amadison@dev-aaron:~/projects/delete_bug/project$ ./manage.py test myapp Creating test database for alias 'default'... Deleting: <class 'myapp.models.Version'> [1L] Deleting: <class 'myapp.models.Policy'> [1L] Deleting: <class 'myapp.models.Location'> [1L] Deleting: <class 'myapp.models.PropertyItem'> [2L, 1L] Deleting: <class 'myapp.models.Coverage'> [2L, 1L] Deleting: <class 'myapp.models.ItemRateCode'> [2L, 1L] Deleting: <class 'myapp.models.Item'> [2L, 1L]
the SQL error is happening (i am fairly sure) because the version is trying to be deleted before the Item.
by , 14 years ago
Attachment: | 15776.deletion_order.diff added |
---|
comment:3 by , 14 years ago
It looks as if the fix is that simple. But I haven't checked for sideeffects yet, and in particular not with MySQL.
comment:4 by , 14 years ago
That patch seems to shift the problem. Now it blows up deleting item first when it hadn't already deleted the related item rate code. The integrity issues are not present on sqlite3... I'm not sure about postgres.
IntegrityError: (1451, 'Cannot delete or update a parent row: a foreign key constraint fails (`test_delete_test`.`myapp_itemratecode`, CONSTRAINT `item_id_refs_id_3db45024` FOREIGN KEY (`item_id`) REFERENCES `myapp_item` (`id`))')
comment:5 by , 14 years ago
Based on the relationship structure, the proper delete order would be
PropertyItem
Coverage
ItemRateCode
Item
Location
Version
Policy
I think the confusion is coming because a 'Location' must tie to a 'Version', and 'Item' must tie to a 'Version', but an item could also be tied to a specific 'Location'. Whatever is calculating the dependency order is getting confused with this relationship.
Replying to aaron.l.madison@…:
I put some print statements in the django.db.models.deletion.py in the Collector.delete method where it does
# delete instances for model, instances in self.data.iteritems(): query = sql.DeleteQuery(model) pk_list = [obj.pk for obj in instances] print "Deleting:", model, pk_list #query.delete_batch(pk_list, self.using)The output is:
(delete_bug)amadison@dev-aaron:~/projects/delete_bug/project$ ./manage.py test myapp Creating test database for alias 'default'... Deleting: <class 'myapp.models.Version'> [1L] Deleting: <class 'myapp.models.Policy'> [1L] Deleting: <class 'myapp.models.Location'> [1L] Deleting: <class 'myapp.models.PropertyItem'> [2L, 1L] Deleting: <class 'myapp.models.Coverage'> [2L, 1L] Deleting: <class 'myapp.models.ItemRateCode'> [2L, 1L] Deleting: <class 'myapp.models.Item'> [2L, 1L]the SQL error is happening (i am fairly sure) because the version is trying to be deleted before the Item.
comment:6 by , 14 years ago
I have the same problem with a much simpler model structure in Django 1.3 and MySQL 5.1/5.5 (I tried both):
class Tag(models.Model): name = models.CharField(max_length=150, unique=True) order = models.PositiveSmallIntegerField(default=0) score = models.PositiveSmallIntegerField(default=0) slug = models.SlugField(unique=True) class TagCombo(models.Model): tag = models.ForeignKey('tag.Tag') tag = Tag.objects.get(id=1234) tag.delete()
Exception:
Traceback (most recent call last): File "<console>", line 1, in <module> File "/Users/adam/Development/example-env/lib/python2.7/site-packages/django/db/models/base.py", line 581, in delete collector.delete() File "/Users/adam/Development/example-env/lib/python2.7/site-packages/django/db/models/deletion.py", line 63, in decorated func(self, *args, **kwargs) File "/Users/adam/Development/example-env/lib/python2.7/site-packages/django/db/models/deletion.py", line 254, in delete query.delete_batch(pk_list, self.using) File "/Users/adam/Development/example-env/lib/python2.7/site-packages/django/db/models/sql/subqueries.py", line 44, in delete_batch self.do_query(self.model._meta.db_table, where, using=using) File "/Users/adam/Development/example-env/lib/python2.7/site-packages/django/db/models/sql/subqueries.py", line 29, in do_query self.get_compiler(using).execute_sql(None) File "/Users/adam/Development/example-env/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 735, in execute_sql cursor.execute(sql, params) File "/Users/adam/Development/example-env/lib/python2.7/site-packages/django/db/backends/util.py", line 34, in execute return self.cursor.execute(sql, params) File "/Users/adam/Development/example-env/lib/python2.7/site-packages/django/db/backends/mysql/base.py", line 86, in execute return self.cursor.execute(query, args) File "/Users/adam/Development/example-env/lib/python2.7/site-packages/MySQLdb/cursors.py", line 174, in execute self.errorhandler(self, exc, value) File "/Users/adam/Development/example-env/lib/python2.7/site-packages/MySQLdb/connections.py", line 36, in defaulterrorhandler raise errorclass, errorvalue IntegrityError: (1451, 'Cannot delete or update a parent row: a foreign key constraint fails (`example_local`.`tag_tagcombo`, CONSTRAINT `tag_id_refs_id_39193e2d9b20ff37` FOREIGN KEY (`tag_id`) REFERENCES `tag_tag` (`id`))')
by , 14 years ago
Attachment: | 15776.deletion_order.2.diff added |
---|
comment:7 by , 14 years ago
I could not reproduce the error with adamnelson's example. But I could reproduce it with aaron's code. My first patch was completly wrong.
But the second patch fixes the issue for me with MySQL 5.5.10. Basically, we drop all but the first dependency, which causes the bad deletion order.
comment:8 by , 14 years ago
Triage Stage: | Unreviewed → Accepted |
---|
comment:9 by , 14 years ago
Easy pickings: | unset |
---|---|
Needs tests: | set |
comment:10 by , 14 years ago
Owner: | changed from | to
---|
comment:11 by , 14 years ago
For other people trying to reproduce this, it seems only to be a problem with the InnoDB engine. You'll need this in your settings file:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', # ... 'OPTIONS': {"init_command": "SET storage_engine=INNODB"},
comment:12 by , 14 years ago
OK, I'm about to commit the fix. Some notes, for your information:
The models ItemRateCode
, PropertyItem
and Coverage
are irrelevant and can be removed from the example (so I did in the tests). Same for needing multiple 'Item' objects.
To expand on emulbreh's explanation of his patch:
The bug occurs because the 'Item' object is first reached via the 'Location' object, and since the relation is nullable, the dependency is not added. The second time it is reached, directly from the 'Version' object, it is not 'new', therefore the dependency is not counted. This last bit of logic needs to be removed - an object may have already been seen, but that doesn't mean the route to the object has already been seen, and we need to include the dependency nonetheless.
Sample project file to recreate the problem