Skip to content

Commit 4921adc

Browse files
committed
Remove $facet when an ArrayField is used as rhs
1 parent 4403f0a commit 4921adc

File tree

3 files changed

+25
-15
lines changed

3 files changed

+25
-15
lines changed

django_mongodb_backend/aggregates.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.db.models.aggregates import Aggregate, Count, StdDev, Variance
22
from django.db.models.expressions import Case, Value, When
3+
from django.db.models.functions.comparison import Coalesce
34
from django.db.models.lookups import IsNull
45
from django.db.models.sql.where import WhereNode
56

@@ -44,6 +45,8 @@ def count(self, compiler, connection, resolve_inner_expression=False):
4445
if resolve_inner_expression:
4546
return inner_expression
4647
return {"$sum": inner_expression}
48+
# Normalize empty documents (introduced upstream) to an empty set fallback.
49+
agg_expression = Coalesce(agg_expression, [])
4750
# If distinct=True or resolve_inner_expression=False, sum the size of the
4851
# set.
4952
return {"$size": agg_expression.as_mql(compiler, connection, as_expr=True)}

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/query.py

Lines changed: 15 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,20 @@ 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+
# Limiting to one document ensures the original result takes precedence
105+
# when present. Otherwise, the injected empty document is used.
106+
{"$limit": 1},
107+
]
108+
)
94109
if self.project_fields:
95110
pipeline.append({"$project": self.project_fields})
96111
if self.combinator_pipeline:

0 commit comments

Comments
 (0)