Skip to content

Commit f4d7ddd

Browse files
committed
Remove $facet in top level group stages
1 parent 359a359 commit f4d7ddd

File tree

7 files changed

+71
-122
lines changed

7 files changed

+71
-122
lines changed

django_mongodb_backend/aggregates.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def count(self, compiler, connection, resolve_inner_expression=False):
5252
# If distinct=True or resolve_inner_expression=False, sum the size of the
5353
# set.
5454
lhs_mql = process_lhs(self, compiler, connection, as_expr=True)
55+
# Wrap null results as an empty array.
56+
lhs_mql = {"$ifNull": [lhs_mql, []]}
5557
# None shouldn't be counted, so subtract 1 if it's present.
5658
exits_null = {"$cond": {"if": {"$in": [{"$literal": None}, lhs_mql]}, "then": -1, "else": 0}}
5759
return {"$add": [{"$size": lhs_mql}, exits_null]}

django_mongodb_backend/compiler.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ def __init__(self, *args, **kwargs):
3838
self.subqueries = []
3939
# Atlas search stage.
4040
self.search_pipeline = []
41+
# The aggregation has no group-by fields and needs wrapping.
42+
self.wrap_for_global_aggregation = False
43+
# HAVING stage match (MongoDB equivalent)
44+
self.having_match_mql = None
4145

4246
def _get_group_alias_column(self, expr, annotation_group_idx):
4347
"""Generate a dummy field for use in the ids fields in $group."""
@@ -234,21 +238,9 @@ def _build_aggregation_pipeline(self, ids, group):
234238
"""Build the aggregation pipeline for grouping."""
235239
pipeline = []
236240
if not ids:
237-
group["_id"] = None
238-
pipeline.append({"$facet": {"group": [{"$group": group}]}})
239-
pipeline.append(
240-
{
241-
"$addFields": {
242-
key: {
243-
"$getField": {
244-
"input": {"$arrayElemAt": ["$group", 0]},
245-
"field": key,
246-
}
247-
}
248-
for key in group
249-
}
250-
}
251-
)
241+
pipeline.append({"$group": {"_id": None, **group}})
242+
# If there are no ids and no having clause, apply a global aggregation
243+
self.wrap_for_global_aggregation = not bool(self.having)
252244
else:
253245
group["_id"] = ids
254246
pipeline.append({"$group": group})

django_mongodb_backend/fields/array.py

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -310,37 +310,24 @@ class ArrayOverlap(ArrayRHSMixin, FieldGetDbPrepValueMixin, Lookup):
310310

311311
def get_subquery_wrapping_pipeline(self, compiler, connection, field_name, expr):
312312
return [
313+
{"$project": {"tmp_name": expr.as_mql(compiler, connection, as_expr=True)}},
313314
{
314-
"$facet": {
315-
"group": [
316-
{"$project": {"tmp_name": expr.as_mql(compiler, connection, as_expr=True)}},
317-
{
318-
"$unwind": "$tmp_name",
319-
},
320-
{
321-
"$group": {
322-
"_id": None,
323-
"tmp_name": {"$addToSet": "$tmp_name"},
324-
}
325-
},
326-
]
327-
}
315+
"$unwind": "$tmp_name",
328316
},
329317
{
330-
"$project": {
331-
field_name: {
332-
"$ifNull": [
333-
{
334-
"$getField": {
335-
"input": {"$arrayElemAt": ["$group", 0]},
336-
"field": "tmp_name",
337-
}
338-
},
339-
[],
340-
]
341-
}
318+
"$group": {
319+
"_id": None,
320+
"tmp_name": {"$addToSet": "$tmp_name"},
342321
}
343322
},
323+
# Workaround for https://jira.mongodb.org/browse/SERVER-114196:
324+
# $$NOW becomes unavailable after $unionWith, so it must be stored
325+
# beforehand to ensure it remains accessible later in the pipeline.
326+
{"$addFields": {"__now": "$$NOW"}},
327+
# Add an empty extra document to handle default values on empty results.
328+
{"$unionWith": {"pipeline": [{"$documents": [{"tmp_name": []}]}]}},
329+
{"$limit": 1},
330+
{"$project": {field_name: "$tmp_name"}},
344331
]
345332

346333
def as_mql_expr(self, compiler, connection):

django_mongodb_backend/fields/embedded_model_array.py

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -150,44 +150,31 @@ def get_subquery_wrapping_pipeline(self, compiler, connection, field_name, expr)
150150
# structure of EmbeddedModelArrayField on the RHS behaves similar to
151151
# ArrayField.
152152
return [
153+
{"$project": {"tmp_name": expr.as_mql(compiler, connection, as_expr=True)}},
154+
# To concatenate all the values from the RHS subquery,
155+
# use an $unwind followed by a $group.
153156
{
154-
"$facet": {
155-
"gathered_data": [
156-
{"$project": {"tmp_name": expr.as_mql(compiler, connection, as_expr=True)}},
157-
# To concatenate all the values from the RHS subquery,
158-
# use an $unwind followed by a $group.
159-
{
160-
"$unwind": "$tmp_name",
161-
},
162-
# The $group stage collects values into an array using
163-
# $addToSet. The use of {_id: null} results in a
164-
# single grouped array. However, because arrays from
165-
# multiple documents are aggregated, the result is a
166-
# list of lists.
167-
{
168-
"$group": {
169-
"_id": None,
170-
"tmp_name": {"$addToSet": "$tmp_name"},
171-
}
172-
},
173-
]
174-
}
157+
"$unwind": "$tmp_name",
175158
},
159+
# The $group stage collects values into an array using
160+
# $addToSet. The use of {_id: null} results in a
161+
# single grouped array. However, because arrays from
162+
# multiple documents are aggregated, the result is a
163+
# list of lists.
176164
{
177-
"$project": {
178-
field_name: {
179-
"$ifNull": [
180-
{
181-
"$getField": {
182-
"input": {"$arrayElemAt": ["$gathered_data", 0]},
183-
"field": "tmp_name",
184-
}
185-
},
186-
[],
187-
]
188-
}
165+
"$group": {
166+
"_id": None,
167+
"tmp_name": {"$addToSet": "$tmp_name"},
189168
}
190169
},
170+
# Workaround for https://jira.mongodb.org/browse/SERVER-114196:
171+
# $$NOW becomes unavailable after $unionWith, so it must be stored
172+
# beforehand to ensure it remains accessible later in the pipeline.
173+
{"$addFields": {"__now": "$$NOW"}},
174+
# Add a dummy document in case of empty result.
175+
{"$unionWith": {"pipeline": [{"$documents": [{"tmp_name": []}]}]}},
176+
{"$limit": 1},
177+
{"$project": {field_name: "$tmp_name"}},
191178
]
192179

193180

django_mongodb_backend/lookups.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,34 +56,20 @@ def inner(self, compiler, connection):
5656
def get_subquery_wrapping_pipeline(self, compiler, connection, field_name, expr): # noqa: ARG001
5757
return [
5858
{
59-
"$facet": {
60-
"group": [
61-
{
62-
"$group": {
63-
"_id": None,
64-
"tmp_name": {
65-
"$addToSet": expr.as_mql(compiler, connection, as_expr=True)
66-
},
67-
}
68-
}
69-
]
70-
}
71-
},
72-
{
73-
"$project": {
74-
field_name: {
75-
"$ifNull": [
76-
{
77-
"$getField": {
78-
"input": {"$arrayElemAt": ["$group", 0]},
79-
"field": "tmp_name",
80-
}
81-
},
82-
[],
83-
]
84-
}
59+
"$group": {
60+
"_id": None,
61+
# use a temporal name in order to support field_name="_id"
62+
"tmp_name": {"$addToSet": expr.as_mql(compiler, connection, as_expr=True)},
8563
}
8664
},
65+
# Workaround for https://jira.mongodb.org/browse/SERVER-114196:
66+
# $$NOW becomes unavailable after $unionWith, so it must be stored
67+
# beforehand to ensure it remains accessible later in the pipeline.
68+
{"$addFields": {"__now": "$$NOW"}},
69+
# Add an empty extra document to handle default values on empty results.
70+
{"$unionWith": {"pipeline": [{"$documents": [{"tmp_name": []}]}]}},
71+
{"$limit": 1},
72+
{"$project": {field_name: "$tmp_name"}},
8773
]
8874

8975

django_mongodb_backend/query.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def __init__(self, compiler):
5656
# $lookup stage that encapsulates the pipeline for performing a nested
5757
# subquery.
5858
self.subquery_lookup = None
59+
self.wrap_for_global_aggregation = compiler.wrap_for_global_aggregation
5960

6061
def __repr__(self):
6162
return f"<MongoQuery: {self.match_mql!r} ORDER {self.ordering!r}>"
@@ -91,6 +92,17 @@ def get_pipeline(self):
9192
pipeline.append({"$match": self.match_mql})
9293
if self.aggregation_pipeline:
9394
pipeline.extend(self.aggregation_pipeline)
95+
if self.wrap_for_global_aggregation:
96+
pipeline.extend(
97+
[
98+
# Workaround for https://jira.mongodb.org/browse/SERVER-114196:
99+
# $$NOW becomes unavailable after $unionWith, so it must be stored
100+
# beforehand to ensure it remains accessible later in the pipeline.
101+
{"$addFields": {"__now": "$$NOW"}},
102+
# Add an empty extra document to handle default values on empty results.
103+
{"$unionWith": {"pipeline": [{"$documents": [{}]}]}},
104+
]
105+
)
94106
if self.project_fields:
95107
pipeline.append({"$project": self.project_fields})
96108
if self.combinator_pipeline:

tests/lookup_/tests.py

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -137,28 +137,11 @@ def test_subquery_filter_constant(self):
137137
"let": {},
138138
"pipeline": [
139139
{"$match": {"num": {"$gt": 2}}},
140-
{
141-
"$facet": {
142-
"group": [
143-
{"$group": {"_id": None, "tmp_name": {"$addToSet": "$num"}}}
144-
]
145-
}
146-
},
147-
{
148-
"$project": {
149-
"num": {
150-
"$ifNull": [
151-
{
152-
"$getField": {
153-
"input": {"$arrayElemAt": ["$group", 0]},
154-
"field": "tmp_name",
155-
}
156-
},
157-
[],
158-
]
159-
}
160-
}
161-
},
140+
{"$group": {"_id": None, "tmp_name": {"$addToSet": "$num"}}},
141+
{"$addFields": {"__now": "$$NOW"}},
142+
{"$unionWith": {"pipeline": [{"$documents": [{"tmp_name": []}]}]}},
143+
{"$limit": 1},
144+
{"$project": {"num": "$tmp_name"}},
162145
],
163146
}
164147
},

0 commit comments

Comments
 (0)