Skip to content

Commit f33c42c

Browse files
WaVEVtimgraham
authored andcommitted
add support for ordering by nulls first & last
1 parent 1988672 commit f33c42c

File tree

5 files changed

+24
-18
lines changed

5 files changed

+24
-18
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: 20 additions & 3 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
@@ -351,8 +352,13 @@ def build_query(self, columns=None):
351352
ordering_fields, sort_ordering, extra_fields = self._get_ordering()
352353
query.project_fields = self.get_project_fields(columns, ordering_fields)
353354
query.ordering = sort_ordering
354-
if extra_fields and columns is None:
355-
query.extra_fields = self.get_project_fields(extra_fields)
355+
# extra_fields may contain references to other columns or expressions.
356+
if columns is None:
357+
extra_fields += ordering_fields
358+
if extra_fields:
359+
query.extra_fields = {
360+
field_name: expr.as_mql(self, self.connection) for field_name, expr in extra_fields
361+
}
356362
where = self.get_where()
357363
try:
358364
expr = where.as_mql(self, self.connection) if where else {}
@@ -465,13 +471,24 @@ def _get_ordering(self):
465471
idx = itertools.count(start=1)
466472
for order in self.order_by_expressions or []:
467473
if isinstance(order.expression, Ref):
474+
# TODO change?
475+
# field_name = order.expression.as_mql(self, self.connection).removeprefix("$")
476+
# extra_fields[field_name] = order.expression
468477
field_name = order.expression.refs
469478
elif isinstance(order.expression, Col):
470479
field_name = order.expression.as_mql(self, self.connection).removeprefix("$")
480+
fields[field_name] = order.expression
471481
else:
472482
# The expression must be added to extra_fields with an alias.
473483
field_name = f"__order{next(idx)}"
474484
extra_fields[field_name] = order.expression
485+
# If the expression is ordered by NULLS FIRST or NULLS LAST,
486+
# add a field for sorting that's 1 if null or 0 if not.
487+
if order.nulls_first or order.nulls_last:
488+
null_fieldname = f"__order{next(idx)}"
489+
condition = When(IsNull(order.expression, True), then=Value(1))
490+
extra_fields[null_fieldname] = Case(condition, default=Value(0))
491+
sort_ordering[null_fieldname] = DESCENDING if order.nulls_first else ASCENDING
475492
fields[field_name] = order.expression
476493
sort_ordering[field_name] = DESCENDING if order.descending else ASCENDING
477494
return tuple(fields.items()), sort_ordering, tuple(extra_fields.items())

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)