diff options
Diffstat (limited to 'django/db/models/query.py')
| -rw-r--r-- | django/db/models/query.py | 76 |
1 files changed, 75 insertions, 1 deletions
diff --git a/django/db/models/query.py b/django/db/models/query.py index 76d0f449a6..cbe77caea9 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -26,7 +26,7 @@ from django.db import ( 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 @@ -1975,6 +1975,80 @@ class QuerySet(AltersData): 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.""" if self._for_write: |
