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
4 changes: 4 additions & 0 deletions .github/workflows/screenshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ jobs:
python-version: '3.14'
cache: 'pip'
cache-dependency-path: 'tests/requirements/py3.txt'
- name: Update apt repo
run: sudo apt update
- name: Install libmemcached-dev for pylibmc
run: sudo apt install -y libmemcached-dev
- name: Install and upgrade packaging tools
run: python -m pip install --upgrade pip wheel
- run: python -m pip install -r tests/requirements/py3.txt -e .
Expand Down
1 change: 1 addition & 0 deletions django/contrib/admin/static/admin/css/forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ form .aligned select + div.help {

form .aligned select option:checked {
background-color: var(--selected-row);
color: var(--body-fg);
}

form .aligned ul li {
Expand Down
69 changes: 3 additions & 66 deletions django/contrib/admin/views/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,72 +417,9 @@ def get_ordering(self, request, queryset):
# Add the given query's ordering fields, if any.
ordering.extend(queryset.query.order_by)

return self._get_deterministic_ordering(ordering)

def _get_deterministic_ordering(self, ordering):
"""
Ensure a deterministic order across all database backends. Search for a
single field or unique together set of fields providing a total
ordering. If these are missing, augment the ordering with a descendant
primary key.
"""
ordering = list(ordering)
ordering_fields = set()
total_ordering_fields = {"pk"} | {
field.attname
for field in self.lookup_opts.fields
if field.unique and not field.null
}
for part in ordering:
# Search for single field providing a total ordering.
field_name = None
if isinstance(part, str):
field_name = part.lstrip("-")
elif isinstance(part, F):
field_name = part.name
elif isinstance(part, OrderBy) and isinstance(part.expression, F):
field_name = part.expression.name
if field_name:
# Normalize attname references by using get_field().
try:
field = self.lookup_opts.get_field(field_name)
except FieldDoesNotExist:
# 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 total_ordering_fields:
break
ordering_fields.add(field.attname)
else:
# No single total ordering field, try unique_together and total
# unique constraints.
constraint_field_names = (
*self.lookup_opts.unique_together,
*(
constraint.fields
for constraint in self.lookup_opts.total_unique_constraints
),
)
for field_names in constraint_field_names:
# Normalize attname references by using get_field().
fields = [
self.lookup_opts.get_field(field_name) for field_name in field_names
]
# Composite unique constraints containing a nullable column
# cannot ensure total ordering.
if any(field.null for field in fields):
continue
if ordering_fields.issuperset(field.attname for field in fields):
break
else:
# If no set of unique fields is present in the ordering, rely
# on the primary key to provide total ordering.
ordering.append("-pk")
return ordering
if queryset.order_by(*ordering).totally_ordered:
return ordering
return ordering + ["-pk"]

def get_ordering_field_columns(self):
"""
Expand Down
76 changes: 75 additions & 1 deletion django/db/models/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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, Value, When
from django.db.models.expressions import Case, 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 FilteredRelation, Q
Expand Down Expand Up @@ -1974,6 +1974,80 @@ def ordered(self):
else:
return False

@property
def totally_ordered(self):
"""
Returns True if the QuerySet is ordered and the ordering is
deterministic. This requires that the ordering includes a field
(or set of fields) that is unique and non-nullable.

For queries involving a GROUP BY clause, the model's default
ordering is ignored. Ordering specified via .extra(order_by=...)
is also ignored.
"""
if not self.ordered:
return False
ordering = self.query.order_by
if not ordering and self.query.default_ordering:
ordering = self.query.get_meta().ordering
if not ordering:
return False
opts = self.model._meta
pk_fields = {f.attname for f in opts.pk_fields}
ordering_fields = set()
for part in ordering:
# Search for single field providing a total ordering.
field_name = None
if isinstance(part, str):
field_name = part.lstrip("-")
elif isinstance(part, F):
field_name = part.name
elif isinstance(part, OrderBy) and isinstance(part.expression, F):
field_name = part.expression.name
if field_name:
if field_name == "pk":
return True
# Normalize attname references by using get_field().
try:
field = opts.get_field(field_name)
except exceptions.FieldDoesNotExist:
# 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)

# Account for members of a CompositePrimaryKey.
if ordering_fields.issuperset(pk_fields):
return True
# No single total ordering field, try unique_together and total
# unique constraints.
constraint_field_names = (
*opts.unique_together,
*(constraint.fields for constraint in opts.total_unique_constraints),
)
for field_names in constraint_field_names:
# Normalize attname references by using get_field().
try:
fields = [opts.get_field(field_name) for field_name in field_names]
except exceptions.FieldDoesNotExist:
continue
# Composite unique constraints containing a nullable column
# cannot ensure total ordering.
if any(field.null for field in fields):
continue
if ordering_fields.issuperset(field.attname for field in fields):
return True

return False

@property
def db(self):
"""Return the database used if this query is executed now."""
Expand Down
12 changes: 12 additions & 0 deletions docs/ref/models/querysets.txt
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,18 @@ Here's the formal declaration of a ``QuerySet``:
:meth:`order_by` clause or a default ordering on the model.
``False`` otherwise.

.. attribute:: totally_ordered

.. versionadded:: 6.1

Returns ``True`` if the ``QuerySet`` is ordered and the ordering is
deterministic. This requires that the ordering includes a field
(or set of fields) that is unique and non-nullable.

For queries involving a ``GROUP BY`` clause, the model's default
ordering is ignored. Ordering specified via ``.extra(order_by=...)``
is also ignored.

.. attribute:: db

The database that will be used if this query is executed now.
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ Models
* :class:`~django.db.models.StringAgg` now supports ``distinct=True`` on SQLite
when using the default delimiter ``Value(",")`` only.

* The new :attr:`.QuerySet.totally_ordered` property returns ``True`` if the
:class:`~django.db.models.query.QuerySet` is ordered and the ordering is
deterministic.

Pagination
~~~~~~~~~~

Expand Down
Loading