Skip to content

Commit a58e5e0

Browse files
WaVEVtimgraham
authored andcommitted
add support for ordering by nulls first & last
1 parent 8dd30d3 commit a58e5e0

File tree

5 files changed

+26
-22
lines changed

5 files changed

+26
-22
lines changed

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,6 @@ Migrations for 'admin':
122122
- `Subquery`, `Exists`, and using a `QuerySet` in `QuerySet.annotate()` aren't
123123
supported.
124124

125-
* Ordering a `QuerySet` by `nulls_first` or `nulls_last` isn't supported.
126-
127125
- `DateTimeField` doesn't support microsecond precision, and correspondingly,
128126
`DurationField` stores milliseconds rather than microseconds.
129127

django_mongodb/compiler.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
from django.db import DatabaseError, IntegrityError, NotSupportedError
77
from django.db.models import Count, Expression
88
from django.db.models.aggregates import Aggregate, Variance
9-
from django.db.models.expressions import Col, Ref, Value
9+
from django.db.models.expressions import Case, Col, Ref, Value, When
1010
from django.db.models.functions.comparison import Coalesce
1111
from django.db.models.functions.math import Power
12+
from django.db.models.lookups import IsNull
1213
from django.db.models.sql import compiler
1314
from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, MULTI, SINGLE
1415
from django.utils.functional import cached_property
@@ -349,8 +350,14 @@ def build_query(self, columns=None):
349350
ordering_fields, sort_ordering, extra_fields = self._get_ordering()
350351
query.project_fields = self.get_project_fields(columns, ordering_fields)
351352
query.ordering = sort_ordering
352-
if extra_fields and columns is None:
353-
query.extra_fields = self.get_project_fields(extra_fields)
353+
# If columns is None, then get_project_fields() won't add
354+
# ordering_fields to $project. Use $addFields (extra_fields) instead.
355+
if columns is None:
356+
extra_fields += ordering_fields
357+
if extra_fields:
358+
query.extra_fields = {
359+
field_name: expr.as_mql(self, self.connection) for field_name, expr in extra_fields
360+
}
354361
where = self.get_where()
355362
try:
356363
expr = where.as_mql(self, self.connection) if where else {}
@@ -462,13 +469,21 @@ def _get_ordering(self):
462469
extra_fields = {}
463470
idx = itertools.count(start=1)
464471
for order in self.order_by_objs or []:
465-
if isinstance(order.expression, Col | Ref):
472+
if isinstance(order.expression, Col):
473+
field_name = order.expression.as_mql(self, self.connection).removeprefix("$")
474+
fields[field_name] = order.expression
475+
elif isinstance(order.expression, Ref):
466476
field_name = order.expression.as_mql(self, self.connection).removeprefix("$")
467477
else:
468-
# The expression must be added to extra_fields with an alias.
469478
field_name = f"__order{next(idx)}"
470-
extra_fields[field_name] = order.expression
471-
fields[field_name] = order.expression
479+
fields[field_name] = order.expression
480+
# If the expression is ordered by NULLS FIRST or NULLS LAST,
481+
# add a field for sorting that's 1 if null or 0 if not.
482+
if order.nulls_first or order.nulls_last:
483+
null_fieldname = f"__order{next(idx)}"
484+
condition = When(IsNull(order.expression, True), then=Value(1))
485+
extra_fields[null_fieldname] = Case(condition, default=Value(0))
486+
sort_ordering[null_fieldname] = DESCENDING if order.nulls_first else ASCENDING
472487
sort_ordering[field_name] = DESCENDING if order.descending else ASCENDING
473488
return tuple(fields.items()), sort_ordering, tuple(extra_fields.items())
474489

django_mongodb/features.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ def django_test_expected_failures(self):
376376
"QuerySet.distinct() is not supported.": {
377377
"aggregation.tests.AggregateTestCase.test_sum_distinct_aggregate",
378378
"lookup.tests.LookupTests.test_lookup_collision_distinct",
379+
"ordering.tests.OrderingTests.test_orders_nulls_first_on_filtered_subquery",
379380
"queries.tests.ExcludeTest17600.test_exclude_plain_distinct",
380381
"queries.tests.ExcludeTest17600.test_exclude_with_q_is_equal_to_plain_exclude",
381382
"queries.tests.ExcludeTest17600.test_exclude_with_q_object_distinct",
@@ -419,11 +420,6 @@ def django_test_expected_failures(self):
419420
"queries.tests.ValuesQuerysetTests.test_named_values_list_without_fields",
420421
"select_related.tests.SelectRelatedTests.test_select_related_with_extra",
421422
},
422-
"Ordering a QuerySet by null_first/nulls_last is not supported on MongoDB.": {
423-
"ordering.tests.OrderingTests.test_order_by_nulls_first",
424-
"ordering.tests.OrderingTests.test_order_by_nulls_last",
425-
"ordering.tests.OrderingTests.test_orders_nulls_first_on_filtered_subquery",
426-
},
427423
"QuerySet.update() crash: Unrecognized expression '$count'": {
428424
"update.tests.AdvancedTests.test_update_annotated_multi_table_queryset",
429425
},

django_mongodb/functions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def wrapped(self, compiler, connection):
138138
lhs_mql = process_lhs(self, compiler, connection)
139139
return {
140140
"$cond": {
141-
"if": {"$eq": [lhs_mql, None]},
141+
"if": connection.mongo_operators["isnull"](lhs_mql, True),
142142
"then": None,
143143
"else": {f"${operator}": lhs_mql},
144144
}

django_mongodb/operations.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
from bson.decimal128 import Decimal128
88
from django.conf import settings
9-
from django.db import DataError, NotSupportedError
9+
from django.db import DataError
1010
from django.db.backends.base.operations import BaseDatabaseOperations
11-
from django.db.models.expressions import Combinable, OrderBy
11+
from django.db.models.expressions import Combinable
1212
from django.utils import timezone
1313
from django.utils.regex_helper import _lazy_re_compile
1414

@@ -140,11 +140,6 @@ def convert_uuidfield_value(self, value, expression, connection):
140140
value = uuid.UUID(value)
141141
return value
142142

143-
def check_expression_support(self, expression):
144-
if isinstance(expression, OrderBy) and (expression.nulls_first or expression.nulls_last):
145-
option = "null_first" if expression.nulls_first else "nulls_last"
146-
raise NotSupportedError(f"Ordering a QuerySet by {option} is not supported on MongoDB.")
147-
148143
def combine_expression(self, connector, sub_expressions):
149144
lhs, rhs = sub_expressions
150145
if connector == Combinable.BITLEFTSHIFT:

0 commit comments

Comments
 (0)