Skip to content

Commit 11943d9

Browse files
authored
Merge pull request #2856 from bagerard/geonear_and_collstats_must_come_first_pipeline
Geonear and collstats must come first pipeline
2 parents 38df6f1 + fd109d8 commit 11943d9

File tree

3 files changed

+61
-5
lines changed

3 files changed

+61
-5
lines changed

docs/changelog.rst

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Development
1313
- make sure to read https://www.mongodb.com/docs/manual/core/transactions-in-applications/#callback-api-vs-core-api
1414
- run_in_transaction context manager relies on Pymongo coreAPI, it will retry automatically in case of `UnknownTransactionCommitResult` but not `TransientTransactionError` exceptions
1515
- Using .count() in a transaction will always use Collection.count_document (as estimated_document_count is not supported in transactions)
16+
- Fix use of $geoNear or $collStats in aggregate #2493
1617
- BREAKING CHANGE: Further to the deprecation warning, remove ability to use an unpacked list to `Queryset.aggregate(*pipeline)`, a plain list must be provided instead `Queryset.aggregate(pipeline)`, as it's closer to pymongo interface
1718
- BREAKING CHANGE: Further to the deprecation warning, remove `full_response` from `QuerySet.modify` as it wasn't supported with Pymongo 3+
1819
- Fixed stacklevel of many warnings (to point places emitting the warning more accurately)

mongoengine/queryset/base.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,14 @@ def from_json(self, json_data):
13501350
def aggregate(self, pipeline, **kwargs):
13511351
"""Perform an aggregate function based on your queryset params
13521352
1353+
If the queryset contains a query or skip/limit/sort or if the target Document class
1354+
uses inheritance, this method will add steps prior to the provided pipeline in an arbitrary order.
1355+
This may affect the performance or outcome of the aggregation, so use it consciously.
1356+
1357+
For complex/critical pipelines, we recommended to use the aggregation framework of Pymongo directly,
1358+
it is available through the collection object (YourDocument._collection.aggregate) and will guarantee
1359+
that you have full control on the pipeline.
1360+
13531361
:param pipeline: list of aggregation commands,
13541362
see: https://www.mongodb.com/docs/manual/core/aggregation-pipeline/
13551363
:param kwargs: (optional) kwargs dictionary to be passed to pymongo's aggregate call
@@ -1381,7 +1389,18 @@ def aggregate(self, pipeline, **kwargs):
13811389
if self._skip is not None:
13821390
initial_pipeline.append({"$skip": self._skip})
13831391

1384-
final_pipeline = initial_pipeline + pipeline
1392+
# geoNear and collStats must be the first stages in the pipeline if present
1393+
first_step = []
1394+
new_user_pipeline = []
1395+
for step_step in pipeline:
1396+
if "$geoNear" in step_step:
1397+
first_step.append(step_step)
1398+
elif "$collStats" in step_step:
1399+
first_step.append(step_step)
1400+
else:
1401+
new_user_pipeline.append(step_step)
1402+
1403+
final_pipeline = first_step + initial_pipeline + new_user_pipeline
13851404

13861405
collection = self._collection
13871406
if self._read_preference is not None or self._read_concern is not None:

tests/queryset/test_queryset_aggregation.py

+40-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import unittest
2-
31
import pytest
42
from pymongo.read_preferences import ReadPreference
53

@@ -334,6 +332,44 @@ class Person(Document):
334332

335333
assert list(data) == []
336334

335+
def test_aggregate_geo_near_used_as_initial_step_before_cls_implicit_step(self):
336+
class BaseClass(Document):
337+
meta = {"allow_inheritance": True}
338+
339+
class Aggr(BaseClass):
340+
name = StringField()
341+
c = PointField()
342+
343+
BaseClass.drop_collection()
344+
345+
x = Aggr(name="X", c=[10.634584, 35.8245029]).save()
346+
y = Aggr(name="Y", c=[10.634584, 35.8245029]).save()
347+
348+
pipeline = [
349+
{
350+
"$geoNear": {
351+
"near": {"type": "Point", "coordinates": [10.634584, 35.8245029]},
352+
"distanceField": "c",
353+
"spherical": True,
354+
}
355+
}
356+
]
357+
res = list(Aggr.objects.aggregate(pipeline))
358+
assert res == [
359+
{"_cls": "BaseClass.Aggr", "_id": x.id, "c": 0.0, "name": "X"},
360+
{"_cls": "BaseClass.Aggr", "_id": y.id, "c": 0.0, "name": "Y"},
361+
]
362+
363+
def test_aggregate_collstats_used_as_initial_step_before_cls_implicit_step(self):
364+
class SomeDoc(Document):
365+
name = StringField()
366+
367+
SomeDoc.drop_collection()
368+
369+
SomeDoc(name="X").save()
370+
SomeDoc(name="Y").save()
337371

338-
if __name__ == "__main__":
339-
unittest.main()
372+
pipeline = [{"$collStats": {"count": {}}}]
373+
res = list(SomeDoc.objects.aggregate(pipeline))
374+
assert len(res) == 1
375+
assert res[0]["count"] == 2

0 commit comments

Comments
 (0)