mirror of https://github.com/django/django.git
Fixed #34013 -- Added QuerySet.order_by() support for annotation transforms.
Thanks Eugene Morozov and Ben Nace for the reports.
This commit is contained in:
parent
fcf95e5927
commit
b0ad41198b
|
@ -387,18 +387,24 @@ class SQLCompiler:
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if col in self.query.annotations:
|
|
||||||
# References to an expression which is masked out of the SELECT
|
ref, *transforms = col.split(LOOKUP_SEP)
|
||||||
# clause.
|
if expr := self.query.annotations.get(ref):
|
||||||
if self.query.combinator and self.select:
|
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
|
# Don't use the resolved annotation because other
|
||||||
# combinated queries might define it differently.
|
# combined queries might define it differently.
|
||||||
expr = F(col)
|
expr = F(ref)
|
||||||
else:
|
if transforms:
|
||||||
expr = self.query.annotations[col]
|
for name in transforms:
|
||||||
if isinstance(expr, Value):
|
expr = self.query.try_transform(expr, name)
|
||||||
# output_field must be resolved for constants.
|
if isinstance(expr, Value):
|
||||||
expr = Cast(expr, expr.output_field)
|
# output_field must be resolved for constants.
|
||||||
|
expr = Cast(expr, expr.output_field)
|
||||||
yield OrderBy(expr, descending=descending), False
|
yield OrderBy(expr, descending=descending), False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
@ -181,6 +181,9 @@ Models
|
||||||
:class:`~django.db.models.expressions.ValueRange` allows excluding rows,
|
:class:`~django.db.models.expressions.ValueRange` allows excluding rows,
|
||||||
groups, and ties from the window frames.
|
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
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ from django.db.models import (
|
||||||
Subquery,
|
Subquery,
|
||||||
Sum,
|
Sum,
|
||||||
TimeField,
|
TimeField,
|
||||||
|
Transform,
|
||||||
Value,
|
Value,
|
||||||
Variance,
|
Variance,
|
||||||
When,
|
When,
|
||||||
|
@ -1727,6 +1728,28 @@ class AggregateTestCase(TestCase):
|
||||||
ordered=False,
|
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):
|
def test_empty_result_optimization(self):
|
||||||
with self.assertNumQueries(0):
|
with self.assertNumQueries(0):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|
|
@ -12,7 +12,12 @@ from ..models import Article, Author
|
||||||
class JSONObjectTests(TestCase):
|
class JSONObjectTests(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
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):
|
def test_empty(self):
|
||||||
obj = Author.objects.annotate(json_object=JSONObject()).first()
|
obj = Author.objects.annotate(json_object=JSONObject()).first()
|
||||||
|
@ -88,6 +93,18 @@ class JSONObjectTests(TestCase):
|
||||||
obj = Article.objects.annotate(json_object=JSONObject(text=F("text"))).first()
|
obj = Article.objects.annotate(json_object=JSONObject(text=F("text"))).first()
|
||||||
self.assertEqual(obj.json_object, {"text": "x" * 4000})
|
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")
|
@skipIfDBFeature("has_json_object_function")
|
||||||
class JSONObjectNotSupportedTests(TestCase):
|
class JSONObjectNotSupportedTests(TestCase):
|
||||||
|
|
|
@ -469,6 +469,16 @@ class TestQuerying(PostgreSQLTestCase):
|
||||||
self.assertIn("GROUP BY 2", sql)
|
self.assertIn("GROUP BY 2", sql)
|
||||||
self.assertIn("ORDER 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):
|
def test_index(self):
|
||||||
self.assertSequenceEqual(
|
self.assertSequenceEqual(
|
||||||
NullableIntegerArrayModel.objects.filter(field__0=2), self.objs[1:3]
|
NullableIntegerArrayModel.objects.filter(field__0=2), self.objs[1:3]
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
import operator
|
import operator
|
||||||
|
|
||||||
from django.db import DatabaseError, NotSupportedError, connection
|
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 import TestCase, skipIfDBFeature, skipUnlessDBFeature
|
||||||
from django.test.utils import CaptureQueriesContext
|
from django.test.utils import CaptureQueriesContext
|
||||||
|
|
||||||
|
@ -322,6 +331,23 @@ class QuerySetSetOperationTests(TestCase):
|
||||||
operator.itemgetter("num"),
|
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):
|
def test_union_with_select_related_and_order(self):
|
||||||
e1 = ExtraInfo.objects.create(value=7, info="e1")
|
e1 = ExtraInfo.objects.create(value=7, info="e1")
|
||||||
a1 = Author.objects.create(name="a1", num=1, extra=e1)
|
a1 = Author.objects.create(name="a1", num=1, extra=e1)
|
||||||
|
|
Loading…
Reference in New Issue