Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 36 additions & 14 deletions django/db/models/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@
from django.db.models import AutoField, DateField, DateTimeField, Field, Max, sql
from django.db.models.constants import LOOKUP_SEP, OnConflict
from django.db.models.deletion import Collector
from django.db.models.expressions import Case, DatabaseDefault, F, OrderBy, Value, When
from django.db.models.expressions import (
Case,
Col,
ColPairs,
DatabaseDefault,
F,
OrderBy,
Value,
When,
)
from django.db.models.fetch_modes import FETCH_ONE
from django.db.models.functions import Cast, Trunc
from django.db.models.query_utils import PROHIBITED_FILTER_KWARGS, FilteredRelation, Q
Expand Down Expand Up @@ -2048,7 +2057,7 @@ def totally_ordered(self):
return False
opts = self.model._meta
pk_fields = {f.attname for f in opts.pk_fields}
ordering_fields = set()
candidate_fields = set()
for part in ordering:
# Search for single field providing a total ordering.
field_name = None
Expand All @@ -2058,7 +2067,17 @@ def totally_ordered(self):
field_name = part.name
elif isinstance(part, OrderBy) and isinstance(part.expression, F):
field_name = part.expression.name
if field_name:
if annotation_col := self.query.annotations.get(field_name):
if isinstance(annotation_col, Col):
if annotation_col.alias == self.query.base_table:
candidate_fields.add(annotation_col.target)
elif isinstance(annotation_col, ColPairs):
candidate_fields |= {
c.target
for c in annotation_col.get_cols()
if c.alias == self.query.base_table
}
elif field_name:
if field_name == "pk":
return True
# Normalize attname references by using get_field().
Expand All @@ -2068,18 +2087,21 @@ def totally_ordered(self):
# Could be "?" for random ordering or a related field
# lookup. Skip this part of introspection for now.
continue
# Ordering by a related field name orders by the referenced
# model's ordering. Skip this part of introspection for now.
if field.remote_field and field_name == field.name:
continue
if field.attname in pk_fields and len(pk_fields) == 1:
return True
if field.unique and not field.null:
return True
ordering_fields.add(field.attname)
else:
# Ordering by a related field name orders by the referenced
# model's ordering. Skip this introspection for now.
if field.remote_field and field_name == field.name:
continue
candidate_fields.add(field)

candidate_attnames = set()
for field in candidate_fields:
if field.unique and not field.null:
return True
candidate_attnames.add(field.attname)

# Account for members of a CompositePrimaryKey.
if ordering_fields.issuperset(pk_fields):
if candidate_attnames.issuperset(pk_fields):
return True
# No single total ordering field, try unique_together and total
# unique constraints.
Expand All @@ -2097,7 +2119,7 @@ def totally_ordered(self):
# cannot ensure total ordering.
if any(field.null for field in fields):
continue
if ordering_fields.issuperset(field.attname for field in fields):
if candidate_attnames.issuperset(field.attname for field in fields):
return True

return False
Expand Down
6 changes: 6 additions & 0 deletions tests/composite_pk/test_order_by.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,9 @@ def test_order_comments_by_pk_expr(self):
Comment.objects.order_by("-pk"),
Comment.objects.order_by(F("pk").desc(nulls_last=True)),
)

def test_alias_totally_ordered(self):
self.assertIs(
Comment.objects.alias(my_pk=F("pk")).order_by("my_pk").totally_ordered,
True,
)
2 changes: 2 additions & 0 deletions tests/ordering/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ class OrderedByExpressionGrandChild(models.Model):

class BarcodedArticle(models.Model):
rank = models.IntegerField(unique=True, null=True)
unique_rank = models.IntegerField(unique=True)
headline = models.CharField(max_length=100)
slug = models.CharField(max_length=100, default="slug")
pub_date = models.DateField(null=True)
barcode = models.CharField(max_length=30, default="bar")
parent = models.ForeignKey("self", models.SET_NULL, null=True)

class Meta:
required_db_features = {"supports_partial_indexes"}
Expand Down
31 changes: 31 additions & 0 deletions tests/ordering/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,37 @@ def test_conditional_constraints(self):
self.assertIs(BarcodedArticle.objects.order_by("rank").totally_ordered, False)
self.assertIs(BarcodedArticle.objects.order_by("barcode").totally_ordered, True)

def test_alias(self):
self.assertIs(
Author.objects.alias(my_pk=F("pk")).order_by("my_pk").totally_ordered, True
)
self.assertIs(
Author.objects.alias(my_pk=F("name")).order_by("my_pk").totally_ordered,
False,
)

def test_alias_relation(self):
self.assertIs(
Author.objects.alias(my_editor=F("editor__pk"))
.order_by("my_editor")
.totally_ordered,
False,
)

def test_alias_self_relation(self):
self.assertIs(
BarcodedArticle.objects.alias(my_unique_rank=F("unique_rank"))
.order_by("my_unique_rank")
.totally_ordered,
True,
)
self.assertIs(
BarcodedArticle.objects.alias(parent_unique_rank=F("parent__unique_rank"))
.order_by("parent_unique_rank")
.totally_ordered,
False,
)

def test_totally_ordered_none(self):
qs = Author.objects.order_by().none()
self.assertIs(qs.totally_ordered, False)
Expand Down
Loading