diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 50b2ee19..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 = {} @@ -404,9 +409,8 @@ 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)): - 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..050b2d03 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1413,6 +1413,23 @@ 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_self_referencing(self): """Ensure self-referencing CASCADE deletes do not result in infinite loop