diff --git a/AUTHORS b/AUTHORS index 29993c84..b0400f4d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -226,4 +226,4 @@ that much better: * Emmanuel Leblond (https://github.com/touilleMan) * Breeze.Kay (https://github.com/9nix00) * Vicki Donchenko (https://github.com/kivistein) - + * Emile Caron (https://github.com/emilecaron) diff --git a/docs/changelog.rst b/docs/changelog.rst index d35551ce..de47000d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.10.1 - DEV ======================= +- Fix infinite recursion with CASCADE delete rules under specific conditions. #1046 Changes in 0.10.0 ================= diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 50b2ee19..8db8351f 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 @@ -402,11 +402,13 @@ class BaseQuerySet(object): continue rule = doc._meta['delete_rules'][rule_entry] if rule == CASCADE: - ref_q = document_cls.objects(**{field_name + '__in': self}) + cascade_refs = set() if cascade_refs is None else cascade_refs + for ref in queryset: + cascade_refs.add(ref.id) + ref_q = document_cls.objects(**{field_name + '__in': self, 'id__nin': cascade_refs}) ref_q_count = ref_q.count() - if (doc != document_cls and ref_q_count > 0 or - (doc == document_cls and ref_q_count > 0)): - ref_q.delete(write_concern=write_concern) + if ref_q_count > 0: + 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}) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 4f00e1c6..9f4cd9b9 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1413,6 +1413,47 @@ 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 Dummy(Document): + reference = ReferenceField('self', reverse_delete_rule=CASCADE) + + base = Dummy().save() + other = Dummy(reference=base).save() + base.reference = other + base.save() + + base.delete() + + self.assertRaises(DoesNotExist, base.reload) + self.assertRaises(DoesNotExist, other.reload) + + def test_reverse_delete_rule_cascade_complex_cycle(self): + """Ensure reference cascading doesn't loop if reference graph isn't + a tree + """ + class Category(Document): + name = StringField() + + class Dummy(Document): + reference = ReferenceField('self', reverse_delete_rule=CASCADE) + cat = ReferenceField(Category, reverse_delete_rule=CASCADE) + + cat = Category(name='cat').save() + base = Dummy(cat=cat).save() + other = Dummy(reference=base).save() + other2 = Dummy(reference=other).save() + base.reference = other + base.save() + + cat.delete() + + self.assertRaises(DoesNotExist, base.reload) + self.assertRaises(DoesNotExist, other.reload) + self.assertRaises(DoesNotExist, other2.reload) + def test_reverse_delete_rule_cascade_self_referencing(self): """Ensure self-referencing CASCADE deletes do not result in infinite loop