From 5e3f33b78b35175b3bc3e24cdbc925274c0306be Mon Sep 17 00:00:00 2001 From: Elijah Frederickson Date: Wed, 20 Aug 2025 07:59:11 -0400 Subject: [PATCH 1/8] 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/8] 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/8] 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/8] 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/8] 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/8] 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 ================= From f6a9fde86da6f43be34effbce7dd3692e2a7d052 Mon Sep 17 00:00:00 2001 From: Bastien Gerard Date: Sun, 30 Aug 2026 22:18:38 +0200 Subject: [PATCH 7/8] remove_values_list_alias --- docs/changelog.rst | 3 ++- mongoengine/queryset/base.py | 4 ---- tests/queryset/test_queryset.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 93715bb5e..d976d13ac 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -35,6 +35,7 @@ Changes in 1.0.0 ``(changed_fields, unset_fields)`` tuple containing two disjoint lists of database paths. ``_clear_changed_fields()`` was renamed to ``_clear_updated_fields()``. +- BREAKING CHANGE: Remove ``QuerySet.values_list()`` as it was just an alias to scalar. Use ``QuerySet.scalar()`` instead. - Add a warning that ``mongoengine.org`` is no longer controlled by the MongoEngine project and appears to be an expired domain takeover. - Bug Fix - Fix querying GenericReferenceField with __in operator #2886 @@ -66,7 +67,7 @@ Changes in 1.0.0 - Improve error message in case a document assigned to a ReferenceField wasn't saved yet #1955 - BugFix - Fix inc/dec atomic updates rejecting deltas outside a field's min_value/max_value #2339 - 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 +- BugFix: null embedded scalar values raises AttributeError #2896 Changes in 0.29.3 ================= diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 368c7a165..05db87214 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1296,10 +1296,6 @@ def scalar(self, *fields): return queryset - def values_list(self, *fields): - """An alias for scalar""" - return self.scalar(*fields) - def as_pymongo(self): """Instead of returning Document instances, return raw values from pymongo. diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index e9c17ca3c..130eac35f 100644 --- a/tests/queryset/test_queryset.py +++ b/tests/queryset/test_queryset.py @@ -4749,7 +4749,7 @@ class Container(Document): queryset = Container.objects.filter( Q(source__designator="value1") | Q(target__designator="value2") - ).values_list("source__designator", "target__designator") + ).scalar("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"), ("value1", None)] From b10a32ef04575a22beb3abf9b86c6fb8183786d4 Mon Sep 17 00:00:00 2001 From: Bastien Gerard Date: Sun, 30 Aug 2026 22:20:28 +0200 Subject: [PATCH 8/8] update changelog w ticket --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d976d13ac..7612fff48 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -35,7 +35,7 @@ Changes in 1.0.0 ``(changed_fields, unset_fields)`` tuple containing two disjoint lists of database paths. ``_clear_changed_fields()`` was renamed to ``_clear_updated_fields()``. -- BREAKING CHANGE: Remove ``QuerySet.values_list()`` as it was just an alias to scalar. Use ``QuerySet.scalar()`` instead. +- BREAKING CHANGE: Remove ``QuerySet.values_list()`` as it was just an alias to scalar. Use ``QuerySet.scalar()`` instead. #2931 - Add a warning that ``mongoengine.org`` is no longer controlled by the MongoEngine project and appears to be an expired domain takeover. - Bug Fix - Fix querying GenericReferenceField with __in operator #2886