From 5e3f33b78b35175b3bc3e24cdbc925274c0306be Mon Sep 17 00:00:00 2001 From: Elijah Frederickson Date: Wed, 20 Aug 2025 07:59:11 -0400 Subject: [PATCH 1/6] fix: exception on null embedded scalar values --- mongoengine/queryset/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 2db97ddb7..973912d16 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -2036,6 +2036,8 @@ def lookup(obj, name): chunks = name.split("__") for chunk in chunks: obj = getattr(obj, chunk) + if obj is None: + break return obj data = [lookup(doc, n) for n in self._scalar] From d480a9ded0bbbe248a93e85d0267ffb8f423e740 Mon Sep 17 00:00:00 2001 From: Elijah Frederickson Date: Wed, 20 Aug 2025 08:18:54 -0400 Subject: [PATCH 2/6] add unit test --- tests/queryset/test_queryset.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index 8386249f2..6d2424035 100644 --- a/tests/queryset/test_queryset.py +++ b/tests/queryset/test_queryset.py @@ -21,6 +21,7 @@ from mongoengine.queryset import ( DoesNotExist, MultipleObjectsReturned, + Q, QuerySet, QuerySetManager, queryset_manager, @@ -4674,6 +4675,34 @@ class Person(Document): ("Gabriel Falcao", 23, "New York"), ] + def test_scalar_embedded_null_parents(self): + """Test a multi-scalar query on embedded fields raises an exception when the parent field is null.""" + + class EmbeddedModelA(EmbeddedDocument): + designator = StringField() + + class Container(Document): + source = EmbeddedDocumentField(EmbeddedModelA) + target = EmbeddedDocumentField(EmbeddedModelA, null=True) + + Container( + source=EmbeddedModelA(designator="value1"), + target=EmbeddedModelB(designator="value2"), + ).save() + + Container( + source=EmbeddedModelA(designator="value1"), + target=None, + ).save() + + queryset = Container.objects.filter( + Q(source__designator="value1") | Q(target__designator="value2") + ).values_list("source__designator", "target__designator") + # This should not raise an AttributeError on NoneType for the second Container's + # target__designator + values = list(queryset) + assert values == ["value1", "value2"] + def test_scalar_decimal(self): from decimal import Decimal From 5fd6b23c6ce00ceb875c06aa23ab207bec9a02d3 Mon Sep 17 00:00:00 2001 From: Elijah Frederickson Date: Wed, 20 Aug 2025 08:23:55 -0400 Subject: [PATCH 3/6] add comments --- tests/queryset/test_queryset.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index 6d2424035..049e36c91 100644 --- a/tests/queryset/test_queryset.py +++ b/tests/queryset/test_queryset.py @@ -4685,11 +4685,13 @@ class Container(Document): source = EmbeddedDocumentField(EmbeddedModelA) target = EmbeddedDocumentField(EmbeddedModelA, null=True) + # Create one with both values Container( source=EmbeddedModelA(designator="value1"), target=EmbeddedModelB(designator="value2"), ).save() + # Create one with a null target, but the source value will match the query Container( source=EmbeddedModelA(designator="value1"), target=None, @@ -4698,10 +4700,9 @@ class Container(Document): queryset = Container.objects.filter( Q(source__designator="value1") | Q(target__designator="value2") ).values_list("source__designator", "target__designator") - # This should not raise an AttributeError on NoneType for the second Container's - # target__designator + # This should not raise an AttributeError on NoneType for the second Container's target__designator values = list(queryset) - assert values == ["value1", "value2"] + assert values == ["value1", "value2", None] def test_scalar_decimal(self): from decimal import Decimal From c42f5c9c26bfc08d6ed94116b6f62a32dc2d6a7f Mon Sep 17 00:00:00 2001 From: Elijah Frederickson Date: Wed, 20 Aug 2025 08:29:04 -0400 Subject: [PATCH 4/6] whoops --- tests/queryset/test_queryset.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index 049e36c91..090f09c16 100644 --- a/tests/queryset/test_queryset.py +++ b/tests/queryset/test_queryset.py @@ -4680,17 +4680,17 @@ def test_scalar_embedded_null_parents(self): class EmbeddedModelA(EmbeddedDocument): designator = StringField() - + class Container(Document): source = EmbeddedDocumentField(EmbeddedModelA) target = EmbeddedDocumentField(EmbeddedModelA, null=True) - + # Create one with both values Container( source=EmbeddedModelA(designator="value1"), - target=EmbeddedModelB(designator="value2"), + target=EmbeddedModelA(designator="value2"), ).save() - + # Create one with a null target, but the source value will match the query Container( source=EmbeddedModelA(designator="value1"), From ee99b99a4d8f917322f12d6dcc4349d33f94ed5a Mon Sep 17 00:00:00 2001 From: Elijah Frederickson Date: Wed, 20 Aug 2025 08:34:45 -0400 Subject: [PATCH 5/6] fix comparison --- tests/queryset/test_queryset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index 090f09c16..eed4d660d 100644 --- a/tests/queryset/test_queryset.py +++ b/tests/queryset/test_queryset.py @@ -4702,7 +4702,7 @@ class Container(Document): ).values_list("source__designator", "target__designator") # This should not raise an AttributeError on NoneType for the second Container's target__designator values = list(queryset) - assert values == ["value1", "value2", None] + assert values == [("value1", "value2"), ("value1", None)] def test_scalar_decimal(self): from decimal import Decimal From 7fc67f73c22be39c24f1aabb01e33c0cf447cad9 Mon Sep 17 00:00:00 2001 From: Elijah Frederickson Date: Wed, 20 Aug 2025 08:44:37 -0400 Subject: [PATCH 6/6] update changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index ff2dd38c1..65d560442 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,6 +24,7 @@ Development - BugFix - Calling .clear on a ListField wasn't being marked as changed (and flushed to db upon .save()) #2858 - Improve error message in case a document assigned to a ReferenceField wasn't saved yet #1955 - BugFix - Take `where()` into account when using `.modify()`, as in MyDocument.objects().where("this[field] >= this[otherfield]").modify(field='new') #2044 +- fix: exception on null embedded scalar values Changes in 0.29.0 =================