diff --git a/docs/changelog.rst b/docs/changelog.rst index 3ada301b1..7612fff48 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. #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 @@ -66,6 +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 +- 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 5556181a4..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. @@ -2023,6 +2019,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] diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index d6549f8dd..130eac35f 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, @@ -4724,6 +4725,35 @@ 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) + + # Create one with both values + Container( + source=EmbeddedModelA(designator="value1"), + target=EmbeddedModelA(designator="value2"), + ).save() + + # Create one with a null target, but the source value will match the query + Container( + source=EmbeddedModelA(designator="value1"), + target=None, + ).save() + + queryset = Container.objects.filter( + Q(source__designator="value1") | Q(target__designator="value2") + ).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)] + def test_scalar_decimal(self): from decimal import Decimal