From e43fae86f1e70ce517c518210171f09d9984286b Mon Sep 17 00:00:00 2001 From: emilecaron Date: Thu, 25 Jun 2015 15:37:15 +0000 Subject: [PATCH 1/4] reproduce RuntimeError --- tests/queryset/queryset.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 4f00e1c6..1e279744 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1413,6 +1413,20 @@ class QuerySetTest(unittest.TestCase): self.Person.objects(name='Test User').delete() self.assertEqual(1, BlogPost.objects.count()) + def test_reverse_delete_rule_cascade_cycle(self): + """Ensure reference cascading doesn't loop if reference graph isn't + a tree + """ + class Category(Document): + reference = ReferenceField('self', reverse_delete_rule=CASCADE) + + base = Category().save() + other = Category(reference=base).save() + base.reference = other + base.save() + + self.assertEqual(2, base.delete()) + def test_reverse_delete_rule_cascade_self_referencing(self): """Ensure self-referencing CASCADE deletes do not result in infinite loop From 1e3d2df9e7430593b73d4515553612cd737f6c8d Mon Sep 17 00:00:00 2001 From: emilecaron Date: Thu, 25 Jun 2015 15:40:12 +0000 Subject: [PATCH 2/4] fix illogicality --- mongoengine/queryset/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 50b2ee19..21d20dae 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -404,8 +404,7 @@ class BaseQuerySet(object): if rule == CASCADE: ref_q = document_cls.objects(**{field_name + '__in': self}) ref_q_count = ref_q.count() - if (doc != document_cls and ref_q_count > 0 or - (doc == document_cls and ref_q_count > 0)): + if ref_q_count > 0: ref_q.delete(write_concern=write_concern) elif rule == NULLIFY: document_cls.objects(**{field_name + '__in': self}).update( From 02f61c323df31e6966e93ebd18c64ad9da7650ae Mon Sep 17 00:00:00 2001 From: emilecaron Date: Thu, 25 Jun 2015 18:26:52 +0000 Subject: [PATCH 3/4] update test --- tests/queryset/queryset.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 1e279744..050b2d03 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1417,15 +1417,18 @@ class QuerySetTest(unittest.TestCase): """Ensure reference cascading doesn't loop if reference graph isn't a tree """ - class Category(Document): + class Dummy(Document): reference = ReferenceField('self', reverse_delete_rule=CASCADE) - base = Category().save() - other = Category(reference=base).save() + base = Dummy().save() + other = Dummy(reference=base).save() base.reference = other base.save() - self.assertEqual(2, base.delete()) + base.delete() + + self.assertRaises(DoesNotExist, base.reload) + self.assertRaises(DoesNotExist, other.reload) def test_reverse_delete_rule_cascade_self_referencing(self): """Ensure self-referencing CASCADE deletes do not result in infinite From 646baddce45512e01367346a07195c6a30a613b3 Mon Sep 17 00:00:00 2001 From: emilecaron Date: Thu, 25 Jun 2015 18:27:22 +0000 Subject: [PATCH 4/4] fix cascade delete cycle issuue --- mongoengine/queryset/base.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 21d20dae..df628c35 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -346,7 +346,7 @@ class BaseQuerySet(object): return 0 return self._cursor.count(with_limit_and_skip=with_limit_and_skip) - def delete(self, write_concern=None, _from_doc_delete=False): + def delete(self, write_concern=None, _from_doc_delete=False, cascade_refs=None): """Delete the documents matched by the query. :param write_concern: Extra keyword arguments are passed down which @@ -363,6 +363,11 @@ class BaseQuerySet(object): queryset = self.clone() doc = queryset._document + cascade_refs = set() if cascade_refs is None else cascade_refs + if doc in cascade_refs: + return 0 + cascade_refs.add(doc) + if write_concern is None: write_concern = {} @@ -405,7 +410,7 @@ class BaseQuerySet(object): ref_q = document_cls.objects(**{field_name + '__in': self}) ref_q_count = ref_q.count() if ref_q_count > 0: - ref_q.delete(write_concern=write_concern) + ref_q.delete(write_concern=write_concern, cascade_refs=cascade_refs) elif rule == NULLIFY: document_cls.objects(**{field_name + '__in': self}).update( write_concern=write_concern, **{'unset__%s' % field_name: 1})