Skip to content

Commit

Permalink
Fixed #34013 -- Added QuerySet.order_by() support for annotation tran…
Browse files Browse the repository at this point in the history
…sforms.

Thanks Eugene Morozov and Ben Nace for the reports.
  • Loading branch information
charettes authored and felixxm committed Dec 12, 2023
1 parent fcf95e5 commit b0ad411
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 12 deletions.
26 changes: 16 additions & 10 deletions django/db/models/sql/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,18 +387,24 @@ def _order_by_pairs(self):
True,
)
continue
if col in self.query.annotations:
# References to an expression which is masked out of the SELECT
# clause.

ref, *transforms = col.split(LOOKUP_SEP)
if expr := self.query.annotations.get(ref):
if self.query.combinator and self.select:
if transforms:
raise NotImplementedError(
"Ordering combined queries by transforms is not "
"implemented."
)
# Don't use the resolved annotation because other
# combinated queries might define it differently.
expr = F(col)
else:
expr = self.query.annotations[col]
if isinstance(expr, Value):
# output_field must be resolved for constants.
expr = Cast(expr, expr.output_field)
# combined queries might define it differently.
expr = F(ref)
if transforms:
for name in transforms:
expr = self.query.try_transform(expr, name)
if isinstance(expr, Value):
# output_field must be resolved for constants.
expr = Cast(expr, expr.output_field)
yield OrderBy(expr, descending=descending), False
continue

Expand Down
3 changes: 3 additions & 0 deletions docs/releases/5.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ Models
:class:`~django.db.models.expressions.ValueRange` allows excluding rows,
groups, and ties from the window frames.

* :meth:`.QuerySet.order_by` now supports ordering by annotation transforms
such as ``JSONObject`` keys and ``ArrayAgg`` indices.

Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~

Expand Down
23 changes: 23 additions & 0 deletions tests/aggregation/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
Subquery,
Sum,
TimeField,
Transform,
Value,
Variance,
When,
Expand Down Expand Up @@ -1727,6 +1728,28 @@ def test_aggregation_random_ordering(self):
ordered=False,
)

def test_order_by_aggregate_transform(self):
class Mod100(Mod, Transform):
def __init__(self, expr):
super().__init__(expr, 100)

sum_field = IntegerField()
sum_field.register_instance_lookup(Mod100, "mod100")
publisher_pages = (
Book.objects.values("publisher")
.annotate(sum_pages=Sum("pages", output_field=sum_field))
.order_by("sum_pages__mod100")
)
self.assertQuerySetEqual(
publisher_pages,
[
{"publisher": self.p2.id, "sum_pages": 528},
{"publisher": self.p4.id, "sum_pages": 946},
{"publisher": self.p1.id, "sum_pages": 747},
{"publisher": self.p3.id, "sum_pages": 1482},
],
)

def test_empty_result_optimization(self):
with self.assertNumQueries(0):
self.assertEqual(
Expand Down
19 changes: 18 additions & 1 deletion tests/db_functions/comparison/test_json_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
class JSONObjectTests(TestCase):
@classmethod
def setUpTestData(cls):
Author.objects.create(name="Ivan Ivanov", alias="iivanov")
Author.objects.bulk_create(
[
Author(name="Ivan Ivanov", alias="iivanov"),
Author(name="Bertha Berthy", alias="bberthy"),
]
)

def test_empty(self):
obj = Author.objects.annotate(json_object=JSONObject()).first()
Expand Down Expand Up @@ -88,6 +93,18 @@ def test_textfield(self):
obj = Article.objects.annotate(json_object=JSONObject(text=F("text"))).first()
self.assertEqual(obj.json_object, {"text": "x" * 4000})

def test_order_by_key(self):
qs = Author.objects.annotate(attrs=JSONObject(alias=F("alias"))).order_by(
"attrs__alias"
)
self.assertQuerySetEqual(qs, Author.objects.order_by("alias"))

def test_order_by_nested_key(self):
qs = Author.objects.annotate(
attrs=JSONObject(nested=JSONObject(alias=F("alias")))
).order_by("-attrs__nested__alias")
self.assertQuerySetEqual(qs, Author.objects.order_by("-alias"))


@skipIfDBFeature("has_json_object_function")
class JSONObjectNotSupportedTests(TestCase):
Expand Down
10 changes: 10 additions & 0 deletions tests/postgres_tests/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,16 @@ def test_group_by_order_by_select_index(self):
self.assertIn("GROUP BY 2", sql)
self.assertIn("ORDER BY 2", sql)

def test_order_by_arrayagg_index(self):
qs = (
NullableIntegerArrayModel.objects.values("order")
.annotate(ids=ArrayAgg("id"))
.order_by("-ids__0")
)
self.assertQuerySetEqual(
qs, [{"order": obj.order, "ids": [obj.id]} for obj in reversed(self.objs)]
)

def test_index(self):
self.assertSequenceEqual(
NullableIntegerArrayModel.objects.filter(field__0=2), self.objs[1:3]
Expand Down
28 changes: 27 additions & 1 deletion tests/queries/test_qs_combinators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import operator

from django.db import DatabaseError, NotSupportedError, connection
from django.db.models import Exists, F, IntegerField, OuterRef, Subquery, Value
from django.db.models import (
Exists,
F,
IntegerField,
OuterRef,
Subquery,
Transform,
Value,
)
from django.db.models.functions import Mod
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
from django.test.utils import CaptureQueriesContext

Expand Down Expand Up @@ -322,6 +331,23 @@ def test_union_with_values_list_and_order_on_annotation(self):
operator.itemgetter("num"),
)

def test_order_by_annotation_transform(self):
class Mod2(Mod, Transform):
def __init__(self, expr):
super().__init__(expr, 2)

output_field = IntegerField()
output_field.register_instance_lookup(Mod2, "mod2")
qs1 = Number.objects.annotate(
annotation=Value(1, output_field=output_field),
)
qs2 = Number.objects.annotate(
annotation=Value(2, output_field=output_field),
)
msg = "Ordering combined queries by transforms is not implemented."
with self.assertRaisesMessage(NotImplementedError, msg):
list(qs1.union(qs2).order_by("annotation__mod2"))

def test_union_with_select_related_and_order(self):
e1 = ExtraInfo.objects.create(value=7, info="e1")
a1 = Author.objects.create(name="a1", num=1, extra=e1)
Expand Down

0 comments on commit b0ad411

Please sign in to comment.