Skip to content

Commit c05c3a2

Browse files
committed
add support for order_by() on related collections
Also add support for ordering by F and OrderBy expressions.
1 parent fe44bb5 commit c05c3a2

File tree

5 files changed

+63
-58
lines changed

5 files changed

+63
-58
lines changed

README.md

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

124+
* Ordering a `QuerySet` by `nulls_first` or `nulls_last` isn't supported.
125+
Neither is randomized ordering.
126+
124127
- `DateTimeField` doesn't support microsecond precision, and correspondingly,
125128
`DurationField` stores milliseconds rather than microseconds.
126129

django_mongodb/compiler.py

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from django.core.exceptions import EmptyResultSet, FieldDoesNotExist, FullResultSet
1+
from django.core.exceptions import EmptyResultSet, FullResultSet
22
from django.db import DatabaseError, IntegrityError, NotSupportedError
33
from django.db.models import Count, Expression
44
from django.db.models.aggregates import Aggregate
5-
from django.db.models.constants import LOOKUP_SEP
5+
from django.db.models.expressions import OrderBy
66
from django.db.models.sql import compiler
7-
from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, MULTI
7+
from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, MULTI, ORDER_DIR
88
from django.utils.functional import cached_property
99

1010
from .base import Cursor
@@ -199,31 +199,37 @@ def _get_ordering(self):
199199
if self.query.default_ordering
200200
else self.query.order_by
201201
)
202-
203202
if not ordering:
204203
return self.query.standard_ordering
205-
204+
default_order, _ = ORDER_DIR["ASC" if self.query.standard_ordering else "DESC"]
206205
column_ordering = []
206+
columns_seen = set()
207207
for order in ordering:
208-
if LOOKUP_SEP in order:
209-
raise NotSupportedError("Ordering can't span tables on MongoDB (%s)." % order)
210208
if order == "?":
211209
raise NotSupportedError("Randomized ordering isn't supported by MongoDB.")
212-
213-
ascending = not order.startswith("-")
214-
if not self.query.standard_ordering:
215-
ascending = not ascending
216-
217-
name = order.lstrip("+-")
218-
if name == "pk":
219-
name = opts.pk.name
220-
221-
try:
222-
column = opts.get_field(name).column
223-
except FieldDoesNotExist:
224-
# `name` is an annotation in $project.
225-
column = name
226-
column_ordering.append((column, ascending))
210+
if hasattr(order, "resolve_expression"):
211+
# order is an expression like OrderBy, F, or database function.
212+
orderby = order if isinstance(order, OrderBy) else order.asc()
213+
orderby = orderby.resolve_expression(self.query, allow_joins=True, reuse=None)
214+
ascending = not orderby.descending
215+
# If the query is reversed, ascending and descending are inverted.
216+
if not self.query.standard_ordering:
217+
ascending = not ascending
218+
else:
219+
# order is a string like "field" or "field__other_field".
220+
orderby, _ = self.find_ordering_name(
221+
order, self.query.get_meta(), default_order=default_order
222+
)[0]
223+
ascending = not orderby.descending
224+
column = orderby.expression.as_mql(self, self.connection)
225+
if isinstance(column, dict):
226+
raise NotSupportedError("order_by() expression not supported.")
227+
# $sort references must not include the dollar sign.
228+
column = column.removeprefix("$")
229+
# Don't add the same column twice.
230+
if column not in columns_seen:
231+
columns_seen.add(column)
232+
column_ordering.append((column, ascending))
227233
return column_ordering
228234

229235
@cached_property

django_mongodb/expressions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
Col,
1010
CombinedExpression,
1111
ExpressionWrapper,
12+
F,
1213
NegatedExpression,
1314
Ref,
15+
ResolvedOuterRef,
1416
Subquery,
1517
Value,
1618
When,
@@ -61,6 +63,10 @@ def expression_wrapper(self, compiler, connection):
6163
return self.expression.as_mql(compiler, connection)
6264

6365

66+
def f(self, compiler, connection): # noqa: ARG001
67+
return f"${self.name}"
68+
69+
6470
def negated_expression(self, compiler, connection):
6571
return {"$not": expression_wrapper(self, compiler, connection)}
6672

@@ -102,9 +108,11 @@ def register_expressions():
102108
Col.as_mql = col
103109
CombinedExpression.as_mql = combined_expression
104110
ExpressionWrapper.as_mql = expression_wrapper
111+
F.as_mql = f
105112
NegatedExpression.as_mql = negated_expression
106113
Query.as_mql = query
107114
Ref.as_mql = ref
115+
ResolvedOuterRef.as_mql = ResolvedOuterRef.as_sql
108116
Subquery.as_mql = subquery
109117
When.as_mql = when
110118
Value.as_mql = value

django_mongodb/features.py

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -32,40 +32,25 @@ class DatabaseFeatures(BaseDatabaseFeatures):
3232
"lookup.tests.LookupTests.test_exact_none_transform",
3333
# "Save with update_fields did not affect any rows."
3434
"basic.tests.SelectOnSaveTests.test_select_on_save_lying_update",
35-
# Lookup in order_by() not supported:
36-
# argument of type '<database function>' is not iterable
35+
# Order by constant not supported:
36+
# AttributeError: 'Field' object has no attribute 'model'
37+
"ordering.tests.OrderingTests.test_order_by_constant_value",
38+
"expressions.tests.NegatedExpressionTests.test_filter",
39+
"expressions_case.tests.CaseExpressionTests.test_order_by_conditional_implicit",
40+
# NotSupportedError: order_by() expression not supported.
3741
"db_functions.comparison.test_coalesce.CoalesceTests.test_ordering",
3842
"db_functions.tests.FunctionTests.test_nested_function_ordering",
3943
"db_functions.text.test_length.LengthTests.test_ordering",
4044
"db_functions.text.test_strindex.StrIndexTests.test_order_by",
41-
"expressions.tests.BasicExpressionsTests.test_order_by_exists",
42-
"expressions.tests.BasicExpressionsTests.test_order_by_multiline_sql",
4345
"expressions_case.tests.CaseExpressionTests.test_order_by_conditional_explicit",
4446
"lookup.tests.LookupQueryingTests.test_lookup_in_order_by",
45-
"ordering.tests.OrderingTests.test_default_ordering",
46-
"ordering.tests.OrderingTests.test_default_ordering_by_f_expression",
47-
"ordering.tests.OrderingTests.test_default_ordering_does_not_affect_group_by",
48-
"ordering.tests.OrderingTests.test_order_by_constant_value",
4947
"ordering.tests.OrderingTests.test_order_by_expr_query_reuse",
5048
"ordering.tests.OrderingTests.test_order_by_expression_ref",
51-
"ordering.tests.OrderingTests.test_order_by_f_expression",
52-
"ordering.tests.OrderingTests.test_order_by_f_expression_duplicates",
53-
"ordering.tests.OrderingTests.test_order_by_fk_attname",
54-
"ordering.tests.OrderingTests.test_order_by_nulls_first",
55-
"ordering.tests.OrderingTests.test_order_by_nulls_last",
5649
"ordering.tests.OrderingTests.test_ordering_select_related_collision",
57-
"ordering.tests.OrderingTests.test_order_by_self_referential_fk",
58-
"ordering.tests.OrderingTests.test_orders_nulls_first_on_filtered_subquery",
59-
"ordering.tests.OrderingTests.test_related_ordering_duplicate_table_reference",
60-
"ordering.tests.OrderingTests.test_reverse_ordering_pure",
61-
"ordering.tests.OrderingTests.test_reverse_meta_ordering_pure",
62-
"ordering.tests.OrderingTests.test_reversed_ordering",
50+
"queries.tests.Queries1Tests.test_order_by_related_field_transform",
6351
"update.tests.AdvancedTests.test_update_ordered_by_inline_m2m_annotation",
6452
"update.tests.AdvancedTests.test_update_ordered_by_m2m_annotation",
6553
"update.tests.AdvancedTests.test_update_ordered_by_m2m_annotation_desc",
66-
# 'ManyToOneRel' object has no attribute 'column'
67-
"m2m_through.tests.M2mThroughTests.test_order_by_relational_field_through_model",
68-
"queries.tests.Queries4Tests.test_order_by_reverse_fk",
6954
# pymongo: ValueError: update cannot be empty
7055
"update.tests.SimpleTest.test_empty_update_with_inheritance",
7156
"update.tests.SimpleTest.test_nonempty_update_with_inheritance",
@@ -137,6 +122,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
137122
# QuerySet.explain() not implemented:
138123
# https://github.com/mongodb-labs/django-mongodb/issues/28
139124
"queries.test_explain.ExplainUnsupportedTests.test_message",
125+
# filter() on related model + update() doesn't work.
126+
"queries.tests.Queries5Tests.test_ticket9848",
140127
}
141128
# $bitAnd, #bitOr, and $bitXor are new in MongoDB 6.3.
142129
_django_test_expected_failures_bitwise = {
@@ -320,6 +307,7 @@ def django_test_expected_failures(self):
320307
"expressions.tests.BasicExpressionsTests.test_boolean_expression_in_Q",
321308
"expressions.tests.BasicExpressionsTests.test_case_in_filter_if_boolean_output_field",
322309
"expressions.tests.BasicExpressionsTests.test_exists_in_filter",
310+
"expressions.tests.BasicExpressionsTests.test_order_by_exists",
323311
"expressions.tests.BasicExpressionsTests.test_subquery",
324312
"expressions.tests.ExistsTests.test_filter_by_empty_exists",
325313
"expressions.tests.ExistsTests.test_negated_empty_exists",
@@ -438,6 +426,7 @@ def django_test_expected_failures(self):
438426
"expressions.tests.FieldTransformTests.test_month_aggregation",
439427
"expressions_case.tests.CaseDocumentationExamples.test_conditional_aggregation_example",
440428
"model_fields.test_jsonfield.TestQuerying.test_ordering_grouping_by_count",
429+
"ordering.tests.OrderingTests.test_default_ordering_does_not_affect_group_by",
441430
"queries.tests.Queries1Tests.test_ticket_20250",
442431
"queries.tests.ValuesQuerysetTests.test_named_values_list_expression_with_default_alias",
443432
},
@@ -514,6 +503,11 @@ def django_test_expected_failures(self):
514503
"queries.tests.ValuesQuerysetTests.test_named_values_list_without_fields",
515504
"select_related.tests.SelectRelatedTests.test_select_related_with_extra",
516505
},
506+
"Ordering a QuerySet by null_first/nulls_last is not supported on MongoDB.": {
507+
"ordering.tests.OrderingTests.test_order_by_nulls_first",
508+
"ordering.tests.OrderingTests.test_order_by_nulls_last",
509+
"ordering.tests.OrderingTests.test_orders_nulls_first_on_filtered_subquery",
510+
},
517511
"QuerySet.update() crash: Unrecognized expression '$count'": {
518512
"update.tests.AdvancedTests.test_update_annotated_multi_table_queryset",
519513
},
@@ -529,6 +523,7 @@ def django_test_expected_failures(self):
529523
"delete_regress.tests.DeleteLockingTest.test_concurrent_delete",
530524
"expressions.tests.BasicExpressionsTests.test_annotate_values_filter",
531525
"expressions.tests.BasicExpressionsTests.test_filtering_on_rawsql_that_is_boolean",
526+
"expressions.tests.BasicExpressionsTests.test_order_by_multiline_sql",
532527
"model_fields.test_jsonfield.TestQuerying.test_key_sql_injection_escape",
533528
"model_fields.test_jsonfield.TestQuerying.test_key_transform_raw_expression",
534529
"model_fields.test_jsonfield.TestQuerying.test_nested_key_transform_raw_expression",
@@ -617,18 +612,6 @@ def django_test_expected_failures(self):
617612
"Randomized ordering isn't supported by MongoDB.": {
618613
"ordering.tests.OrderingTests.test_random_ordering",
619614
},
620-
# https://github.com/mongodb-labs/django-mongodb/issues/34
621-
"Ordering can't span tables on MongoDB": {
622-
"queries.tests.ConditionalTests.test_infinite_loop",
623-
"queries.tests.NullableRelOrderingTests.test_join_already_in_query",
624-
"queries.tests.Queries1Tests.test_order_by_related_field_transform",
625-
"queries.tests.Queries1Tests.test_ticket7181",
626-
"queries.tests.Queries1Tests.test_tickets_2076_7256",
627-
"queries.tests.Queries1Tests.test_tickets_2874_3002",
628-
"queries.tests.Queries5Tests.test_ordering",
629-
"queries.tests.Queries5Tests.test_ticket9848",
630-
"queries.tests.Ticket14056Tests.test_ticket_14056",
631-
},
632615
"Queries without a collection aren't supported on MongoDB.": {
633616
"queries.test_q.QCheckTests",
634617
"queries.test_query.TestQueryNoModel",

django_mongodb/operations.py

Lines changed: 7 additions & 2 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
9+
from django.db import DataError, NotSupportedError
1010
from django.db.backends.base.operations import BaseDatabaseOperations
11-
from django.db.models.expressions import Combinable
11+
from django.db.models.expressions import Combinable, OrderBy
1212
from django.utils import timezone
1313
from django.utils.regex_helper import _lazy_re_compile
1414

@@ -140,6 +140,11 @@ 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+
143148
def combine_expression(self, connector, sub_expressions):
144149
lhs, rhs = sub_expressions
145150
if connector == Combinable.BITLEFTSHIFT:

0 commit comments

Comments
 (0)