---
url: "https://vueda.dev/v3/reference/api/py/vueda.core.ordering.md"
title: "core.ordering"
---

# core.ordering

## Overview

Inspection and rewriting of the ordering terms Django's ``order_by()`` accepts.

## `expand_ordering_pk` {#expand_ordering_pk}

Expand an ordering term of ``"pk"`` into the concrete field name(s) it stands for.

``"pk"`` is an alias Django's own query machinery resolves (``Query.names_to_path`` swaps it for
the primary key), so it is a legitimate thing to find in a model's ``Meta.ordering``, a viewset's
``ordering``, or a viewset's ``ordering_fields``. It isn't a real field name though, so it
resolves to no field, and a client has no way to tell which column it refers to. Reporting the
field name(s) behind it keeps the alias a server-side detail: the client orders by "id" (or by
each field of a composite primary key) without having to know what ``"pk"`` means for this model.

One term in can be several terms out, so this always returns a list, holding terms of the same
kind that came in.

:param model: The model the term is declared against.
:type model: Type[django.db.models.Model]
:param term: The ordering term to expand.
:type term: Union[str, django.db.models.F, django.db.models.expressions.BaseExpression]
:return: The expanded terms, or ``[term]`` when there is no alias to expand.
:rtype: List[Union[str, django.db.models.F, django.db.models.expressions.BaseExpression]]

### Signature {#expand_ordering_pk-signature}

`expand_ordering_pk(model, term)`

#### Parameters {#expand_ordering_pk-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="expand_ordering_pk-param-model">model</span> |  | yes |  |  |
| <span id="expand_ordering_pk-param-term">term</span> |  | yes |  |  |

### Source {#expand_ordering_pk-source}

`server/vueda/core/ordering.py:352`

## `ordering_field_key` {#ordering_field_key}

A key that two ordering paths share when they sort by the same field.

A ``"pk"`` alias and the field behind it are two spellings of one field: ``"customer__pk"`` and
``"customer__id"`` share a key, and so do ``"pk"`` and every column of a ``CompositePrimaryKey``
taken together. Any other path is its own key. ``VuedaOrderingFilter`` matches a ``nulls_ordering``
key to a request term by this key, and the ``vueda_info.E007`` system check compares names the
same way, so the two cannot disagree about which spellings match.

A path that resolves to nothing is its own key too, rather than raising. Comparing names is not
where a broken path gets reported.

:param model: The model the path starts from.
:type model: Type[django.db.models.Model]
:param field_name: The ordering path, without a direction prefix.
:type field_name: str
:return: The concrete field path(s) behind the path, as a tuple so that the key is hashable.
:rtype: Tuple[str, ...]

### Signature {#ordering_field_key-signature}

`ordering_field_key(model, field_name)`

#### Parameters {#ordering_field_key-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="ordering_field_key-param-model">model</span> |  | yes |  |  |
| <span id="ordering_field_key-param-field_name">field_name</span> |  | yes |  |  |

### Source {#ordering_field_key-source}

`server/vueda/core/ordering.py:326`

## `ordering_fields_entry_name` {#ordering_fields_entry_name}

The field name an ``ordering_fields`` entry offers to ``?o=``, or ``None`` when it has none.

DRF's ``OrderingFilter`` lets an ``ordering_fields`` entry be either a plain field name or a
``(field_name, label)`` pair, and normalizes the two the same way — a string becomes
``(item, item)`` and anything else is passed through as it stands (``get_valid_fields``), after
which only the first element is compared against ``?o=`` (``remove_invalid_fields``). So the two
forms offer exactly the same field; the label is used only to caption DRF's own browsable-API
ordering control, and never reaches a VUEDA client.

An entry is read positionally rather than unpacked, the way DRF reads it, so an entry carrying
more than a name and a label is tolerated here as it is there. One that holds no first element at
all — an empty pair, a number — resolves to ``None`` rather than raising: this reports what a
declaration offers, and a declaration DRF itself can't read a name from offers nothing.

:param entry: The ``ordering_fields`` entry to read.
:type entry: Union[str, Sequence]
:return: The field name the entry offers, or None.
:rtype: Optional[str]

### Signature {#ordering_fields_entry_name-signature}

`ordering_fields_entry_name(entry)`

#### Parameters {#ordering_fields_entry_name-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="ordering_fields_entry_name-param-entry">entry</span> |  | yes |  |  |

### Source {#ordering_fields_entry_name-source}

`server/vueda/core/ordering.py:43`

## `ordering_path_field` {#ordering_path_field}

The field that describes the value that an ordering path sorts by, following
``formatted_name_lookup_expression`` for a ``formatted_name`` that has no column of its own.

Django resolves the path itself (see ``vueda.core.paths.query_path_field``), so every path that
``order_by()`` accepts on the model resolves here. That includes a path that ends in one or more
transforms, such as ``created__year``, ``created__date__year``, or a key on a ``JSONField``. Such a
path is typed by the output of its last transform, which is the value the database sorts:
``created__year`` is typed as a number, not as a date.

A transform may also follow ``formatted_name``, as in ``customer__formatted_name__unaccent``. The
part up to ``formatted_name`` is rewritten to the lookup expression behind it, and the transforms
are applied to that path. See ``resolve_formatted_name_transform_path``.

``formatted_name`` is a display name every VUEDA model exposes, but it reaches the database three
different ways: as its own ``GeneratedField`` column; as the path named by
``formatted_name_lookup_expression``; or as a ``get_formatted_name()`` method, which computes it
in Python. The first two are orderable and both resolve here, so a client can order by
``formatted_name`` either way without knowing which. A method-backed one has no path to resolve,
so it raises ``FieldDoesNotExist`` like any other unorderable path — sorting by it would mean
loading every row into Python — and the ``vueda_info.E005`` system check reports an ordering
declared on one.

``resolve_formatted_name_transform_path`` is what the ordering backend puts into the query, so
metadata describing a field and the query sorting by it agree by construction. A path that this
function types is a path that the backend can run. A path that it can't type is one that the
backend leaves alone.

:param model: The model the path starts from.
:type model: Type[django.db.models.Model]
:param field_name: The ordering path to resolve.
:type field_name: str
:return: The field that describes the value ordered by: the last field that the path names, or the
    output field of the transform that the path ends in.
:rtype: django.db.models.Field
:raises django.core.exceptions.FieldDoesNotExist: If Django can't resolve the path on ``model``.

### Signature {#ordering_path_field-signature}

`ordering_path_field(model, field_name)`

#### Parameters {#ordering_path_field-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="ordering_path_field-param-model">model</span> |  | yes |  |  |
| <span id="ordering_path_field-param-field_name">field_name</span> |  | yes |  |  |

### Source {#ordering_path_field-source}

`server/vueda/core/ordering.py:393`

## `ordering_pk_field_names` {#ordering_pk_field_names}

The concrete field name(s) an ordering path resolves to, with a trailing ``"pk"`` replaced by the
primary key field(s) it stands for. Any other path is returned unchanged.

A ``CompositePrimaryKey`` expands to every field it is built from, matching how Django expands
ordering by such a key into one term per column (``OrderBy.as_sql``), so a single name in can be
several names out.

:param model: The model the path starts from.
:type model: Type[django.db.models.Model]
:param field_name: The ordering path to resolve, e.g. ``"pk"`` or ``"order__pk"``.
:type field_name: str
:return: The concrete field paths, or ``[field_name]`` when there is no alias to expand.
:rtype: List[str]

### Signature {#ordering_pk_field_names-signature}

`ordering_pk_field_names(model, field_name)`

#### Parameters {#ordering_pk_field_names-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="ordering_pk_field_names-param-model">model</span> |  | yes |  |  |
| <span id="ordering_pk_field_names-param-field_name">field_name</span> |  | yes |  |  |

### Source {#ordering_pk_field_names-source}

`server/vueda/core/ordering.py:289`

## `ordering_term_field_names` {#ordering_term_field_names}

The model field paths an ordering term references, in the order they appear in it.

``order_by()`` takes a field name string (optionally ``-`` prefixed, or ``"?"`` for random
ordering) or any query expression, so an ordering term — a viewset's ``ordering``, a model's
``Meta.ordering``, a client's ``?o=`` — can reference any number of fields:

- one, for a plain name, an ``F("name").asc()``, or a scalar function over a single column such
  as ``Lower("name")`` or ``Coalesce("nickname", Value(""))``;
- several, for a scalar function over more than one column, such as
  ``Concat("first_name", "last_name")`` or ``Greatest("updated", "created")``;
- none, for ``"?"`` or an expression built on no column at all, such as ``Now()``.

Field names are taken from the ``F`` references in the expression tree. Django builds those for
itself: ``Func`` turns every string argument into an ``F``, so ``Lower("name")`` holds
``F("name")`` whether or not it was written that way. Duplicates are dropped, so a term naming
the same field twice reports it once.

Nothing here recognizes a particular function. ``flatten`` is ``BaseExpression``'s own tree walk,
which every ``Func`` subclass inherits, and ``F`` is the only node that carries a field path — so
every scalar function in ``django.db.models.functions`` is handled, along with any ``Func``
subclass that an application writes and any function that a future Django version adds. Window
functions (``Rank``, ``RowNumber``, and the rest) are the exception: Django accepts them in
``order_by()`` only inside a ``Window(...)``.

A field named only inside a boolean condition — the ``Q`` in ``Case(When(active=True, ...))`` —
is not reported. The condition decides which value is sorted, not which column the sort reads, and
Django's own expression walk doesn't surface the left-hand side of a ``Q`` as an ``F``.

:param term: The ordering term to inspect.
:type term: Union[str, django.db.models.F, django.db.models.expressions.BaseExpression]
:return: The field paths the term references, without direction prefixes.
:rtype: List[str]

### Signature {#ordering_term_field_names-signature}

`ordering_term_field_names(term)`

#### Parameters {#ordering_term_field_names-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="ordering_term_field_names-param-term">term</span> |  | yes |  |  |

### Source {#ordering_term_field_names-source}

`server/vueda/core/ordering.py:75`

## `ordering_term_is_ascending` {#ordering_term_is_ascending}

Whether an ordering term sorts ascending.

A bare expression carries no direction of its own: ``order_by(Lower("name"))`` is ascending,
because the SQL compiler wraps anything that isn't already an ``OrderBy`` in ``.asc()``.

:param term: The ordering term to inspect.
:type term: Union[str, django.db.models.F, django.db.models.expressions.BaseExpression]
:return: True when the term sorts ascending.
:rtype: bool

### Signature {#ordering_term_is_ascending-signature}

`ordering_term_is_ascending(term)`

#### Parameters {#ordering_term_is_ascending-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="ordering_term_is_ascending-param-term">term</span> |  | yes |  |  |

### Source {#ordering_term_is_ascending-source}

`server/vueda/core/ordering.py:220`

## `queryset_explicit_ordering` {#queryset_explicit_ordering}

The ordering terms that a queryset applies in its own right, with any ``.reverse()`` applied.

Each term is the one that ``order_by()`` received, unless ``.reverse()`` flipped it. A
``.reverse()`` with no ``order_by()`` gives the model's ``Meta.ordering``, flipped. On a grouped
query, such as one that annotates an aggregate, a bare ``.reverse()`` gives nothing, because Django
leaves ``Meta.ordering`` out of a query with ``GROUP BY``. The details are below.

Django keeps an explicit ordering on the query itself. ``QuerySet.order_by`` clears whatever
ordering was there and appends its terms to ``Query.order_by`` (``Query.add_ordering``), and the
SQL compiler prefers those terms over the model's ``Meta.ordering``, which it reaches only for a
query that carries none of its own (``SQLCompiler._order_by_pairs``). So a non-empty result here
is an ordering that overrides the model's default, and an empty one leaves that default to apply.

Those two attributes are Django internals, which is the reason this lives in one function: the
compiler's precedence and the attributes behind it are verified against the Django versions VUEDA
supports in ``tests/unit/filtering/test_queryset_explicit_ordering.py``, rather than assumed at
each call site.

This reads a queryset, so it describes a declaration and not the ordering any particular list
request ends up with. A filter backend replaces the ordering it finds — DRF's ``OrderingFilter``
does exactly that whenever a view declares ``ordering`` or a client sends ``?o=`` — and a
``get_queryset`` can order differently per request. The ``vueda_info.E010`` system check compares
what this reads off a viewset's list queryset against what that viewset declares as its default
ordering, and ``vueda_info.E015`` reads it off a model's default manager. Neither they nor this
claims to know a request's final order.

The terms are the same however the ordering was applied: by ``order_by()`` directly, by a custom
QuerySet method that calls it, or by a manager's ``get_queryset()``.

An ordering set by ``extra(order_by=...)`` reports nothing. That is raw SQL rather than ordering
terms, so it has no field paths to read, and reporting the terms it overrides would describe an
ordering the query doesn't use.

An ordering *removed* by a bare ``order_by()`` reports nothing either, which is a gap rather than
a judgement: that call clears the model's default ordering along with any explicit one, so the
rows arrive unordered while the metadata still reports the model's declaration. Nothing here
distinguishes a queryset that never ordered from one that deliberately stopped ordering.

A ``.reverse()`` is applied to the terms. Django records it as a flag on the query
(``Query.standard_ordering``) and flips every term when it compiles the query, so each term here
comes back flipped the same way. A ``.reverse()`` with no ``order_by()`` flips the model's
``Meta.ordering``, so that reversed ordering is reported. It is an ordering that the queryset
applies in its own right, and it differs from the model's default.

:param queryset: The queryset to inspect.
:type queryset: django.db.models.QuerySet
:return: The ordering terms that the queryset applies, with any ``.reverse()`` applied. The list is
    empty when the queryset applies none.
:rtype: List[Union[str, django.db.models.F, django.db.models.expressions.BaseExpression]]

### Signature {#queryset_explicit_ordering-signature}

`queryset_explicit_ordering(queryset)`

#### Parameters {#queryset_explicit_ordering-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="queryset_explicit_ordering-param-queryset">queryset</span> |  | yes |  |  |

### Source {#queryset_explicit_ordering-source}

`server/vueda/core/ordering.py:130`

## `rewrite_ordering_term_field_names` {#rewrite_ordering_term_field_names}

The ordering term with every field path it references passed through ``rewrite``.

``rewrite`` is called once per field path the term references and returns the path to use in its
place, or ``None`` to leave that one alone. The term is returned unchanged when nothing is
rewritten, and otherwise copied: a term keeps its direction and its nulls placement, and the
caller's own declaration — a viewset's ``ordering``, a model's ``Meta.ordering`` — is never
mutated.

Rewriting reaches every ``F`` in the expression tree, so the path behind a name is substituted
wherever it appears: ``Lower("customer__formatted_name")`` becomes
``Lower("customer__data__formatted_name")``, and the ``Lower`` still applies.

:param term: The ordering term to rewrite.
:type term: Union[str, django.db.models.F, django.db.models.expressions.BaseExpression]
:param rewrite: Called with a field path, returning its replacement or ``None``.
:type rewrite: Callable[[str], Optional[str]]
:return: The rewritten term, or ``term`` itself when nothing changed.
:rtype: Union[str, django.db.models.F, django.db.models.expressions.BaseExpression]

### Signature {#rewrite_ordering_term_field_names-signature}

`rewrite_ordering_term_field_names(term, rewrite)`

#### Parameters {#rewrite_ordering_term_field_names-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="rewrite_ordering_term_field_names-param-term">term</span> |  | yes |  |  |
| <span id="rewrite_ordering_term_field_names-param-rewrite">rewrite</span> |  | yes |  |  |

### Source {#rewrite_ordering_term_field_names-source}

`server/vueda/core/ordering.py:238`

## Source

`server/vueda/core/ordering.py`
