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

# core.paths

## Overview

Translation between the dotted paths VUEDA's public API accepts and the ``__``-joined paths Django's ORM accepts.

Sparse fields (``f``) and expands (``e``) have always been dotted, because DRF-flex-fields is dotted
natively. Ordering (``o``) and declared filters instead put a raw ``__``-joined ORM path on the wire,
which is the inconsistency this module exists to close: every public parameter takes a dotted path,
and every ``__``-joined path stays a server-internal detail of the Django declaration or query behind
it.

Each parameter grammar gets its own functions rather than one path converted generically, because the
grammars are not actually the same:

- Ordering needs both directions — a request's dotted term translated to the ORM path `order_by()`
  takes, and that path translated back for `model_ordering` — and carries an optional leading ``-``
  that a path conversion must not touch, and never carries a wildcard.
- A declared filter's public alias is derived once, from the filter's own declared identity, rather
  than parsed out of a per-request string the way an ordering term is — :mod:`vueda.core.filters`
  reaches for :func:`orm_filter_path_to_public` from ``FilterSet.__init__``, not from request
  handling, and there is no reverse direction: nothing here ever translates a client's request back
  to a filter's ORM path, because a filter's public name *is* the key django-filter binds the
  request to once ``PublicFilterAliasMixin`` has renamed it.
- Sparse fields and expands accept a trailing ``*`` wildcard segment; ordering and filters do not, and
  :func:`reject_wildcard` is what the two of them share to say so.

A function here converts one path at a time and raises nothing on its own: a path with no matching
name on the far side is for the caller's own validation (``VuedaOrderingFilter.get_ordering``, a
declared filter's alias lookup) to reject, not for a generic converter to guess about.

The module also resolves an ``__``-joined path to what it names on a model or a queryset annotation.
:func:`query_path_field`, :func:`query_path_multiplies_rows`, and :func:`annotation_path_output_field`
ask Django's own ``Query`` to resolve the path, so they accept exactly what ``order_by()`` and
``aggregate()`` accept, including a path that ends in transforms such as ``created__year``. Ordering
and column totals both resolve paths through them, so the two cannot disagree about which paths
resolve.

## `annotation_path_field` {#annotation_path_field}

The output field of a path that starts with a queryset annotation: the annotation's own output
field for a plain name such as ``launched``, or the output field of the last transform for a
path such as ``launched__year``.

``Query.resolve_ref`` resolves the path, the same way an ``F()`` expression or ``aggregate()``
resolves it. That includes an annotation that ``.alias()`` added. A plain string in
``order_by()`` can't apply a transform to an ``.alias()`` name, so ``VuedaOrderingFilter`` orders
by such a path through ``F()``.

A path that Django can't resolve raises ``FieldDoesNotExist`` with the message of the
``FieldError`` that Django raised. That covers a name after the annotation that is no transform,
and an annotation whose output type Django can't determine, such as a ``Coalesce`` over mixed
types.

:param query: The query whose annotations the path starts with, such as ``queryset.query``.
:type query: django.db.models.sql.Query
:param path: The ``__``-joined path to resolve.
:type path: str
:return: The output field of the annotation or of its last transform.
:rtype: django.db.models.Field
:raises django.core.exceptions.FieldDoesNotExist: If Django can't resolve the path on ``query``.

### Signature {#annotation_path_field-signature}

`annotation_path_field(query, path)`

#### Parameters {#annotation_path_field-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="annotation_path_field-param-query">query</span> |  | yes |  |  |
| <span id="annotation_path_field-param-path">path</span> |  | yes |  |  |

### Source {#annotation_path_field-source}

`server/vueda/core/paths.py:282`

## `annotation_path_output_field` {#annotation_path_output_field}

The output field of a path that starts with a queryset annotation, as
:func:`annotation_path_field` resolves it.

``None`` when the path doesn't start with an annotation. ``None`` too when Django can't resolve
the rest of the path as transforms, or can't determine the output field. A caller that needs
Django's reason calls :func:`annotation_path_field` instead.

:param query: The query whose annotations the path may start with, such as ``queryset.query``.
:type query: django.db.models.sql.Query
:param path: The ``__``-joined path to resolve.
:type path: str
:return: The output field of the annotation or of its last transform. None when the path doesn't
    resolve this way.
:rtype: Optional[django.db.models.Field]

### Signature {#annotation_path_output_field-signature}

`annotation_path_output_field(query, path)`

#### Parameters {#annotation_path_output_field-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="annotation_path_output_field-param-query">query</span> |  | yes |  |  |
| <span id="annotation_path_output_field-param-path">path</span> |  | yes |  |  |

### Source {#annotation_path_output_field-source}

`server/vueda/core/paths.py:317`

## `has_public_ordering_name` {#has_public_ordering_name}

Whether a ``__``-joined ordering path has a ``?o=`` name that reads back as the same path.

Most paths do. Two kinds of name can contain any character, though: a key on a ``JSONField``, and
an annotation name passed to ``annotate()`` through ``**kwargs``. A name that contains ``.``, such
as ``specs__v1.2``, has the dotted name ``specs.v1.2``, which reads back as ``specs__v1__2``, a
different path. A name that contains ``,`` can't be sent at all, because DRF splits ``?o=`` on
commas into separate terms. The client sends dotted names, so no client can request such a path.

:param path: An ORM ordering path, without a direction prefix.
:type path: str
:return: True when the path's public name reads back as the same path.
:rtype: bool

### Signature {#has_public_ordering_name-signature}

`has_public_ordering_name(path)`

#### Parameters {#has_public_ordering_name-parameters}

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

### Source {#has_public_ordering_name-source}

`server/vueda/core/paths.py:135`

## `join_ordering_direction` {#join_ordering_direction}

An ordering term built from a path and the direction it sorts in.

The inverse of :func:`split_ordering_direction`, so a path translated in between the two calls
carries the same direction out that it carried in, regardless of what the translation did to the
path itself.

:param descending: Whether the term should sort descending.
:type descending: bool
:param path: The path to sort by.
:type path: str
:return: The ordering term, ``-`` prefixed when descending.
:rtype: str

### Signature {#join_ordering_direction-signature}

`join_ordering_direction(descending, path)`

#### Parameters {#join_ordering_direction-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="join_ordering_direction-param-descending">descending</span> |  | yes |  |  |
| <span id="join_ordering_direction-param-path">path</span> |  | yes |  |  |

### Source {#join_ordering_direction-source}

`server/vueda/core/paths.py:92`

## `orm_filter_path_to_public` {#orm_filter_path_to_public}

A ``__``-joined filter path, translated to the dotted form the public wire uses for it.

The one caller is ``PublicFilterAliasMixin.__init__``, translating a filter's *declared* name (a
class attribute, or the name django-filter itself builds for a ``Meta.fields`` entry,
``LOOKUP_SEP.join([field_name, lookup_expr])``) to rename it. Nothing downstream of the rename --
``model_filtering`` included -- calls this again; it reads whatever name the mixin already
settled on.

Unlike an ordering term, there is no leading direction marker to preserve and no lookup
expression to split off first: a filter's lookup expression is a separate, fixed attribute
(``lookup_expr``), not a segment threaded through the name being translated here.

:param path: A ``__``-joined filter name or path.
:type path: str
:return: The path with ``__`` replaced by ``.``.
:rtype: str

### Signature {#orm_filter_path_to_public-signature}

`orm_filter_path_to_public(path)`

#### Parameters {#orm_filter_path_to_public-parameters}

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

### Source {#orm_filter_path_to_public-source}

`server/vueda/core/paths.py:153`

## `orm_ordering_path_to_public` {#orm_ordering_path_to_public}

A ``__``-joined ordering path translated to the dotted form ``model_ordering`` publishes.

:param path: An ORM ordering path, e.g. as reported by :func:`vueda.core.ordering.ordering_term_field_names`.
:type path: str
:return: The path with ``__`` replaced by ``.``.
:rtype: str

### Signature {#orm_ordering_path_to_public-signature}

`orm_ordering_path_to_public(path)`

#### Parameters {#orm_ordering_path_to_public-parameters}

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

### Source {#orm_ordering_path_to_public-source}

`server/vueda/core/paths.py:123`

## `path_annotation_name` {#path_annotation_name}

The name of the queryset annotation that a query path starts with.

Django resolves the first segment of a query path against the queryset's annotations before the
model's fields. So ``launched`` and ``launched__year`` both start with the annotation
``launched`` when the queryset carries one. The ordering backend, the system checks, and column
totals all decide this the same way through this function.

``None`` when the first segment of the path names no annotation.

:param annotations: The annotation names to look in, such as ``queryset.query.annotations``.
:type annotations: Container[str]
:param path: The ``__``-joined path to read.
:type path: str
:return: The annotation name. None when the path doesn't start with one.
:rtype: Optional[str]

### Signature {#path_annotation_name-signature}

`path_annotation_name(annotations, path)`

#### Parameters {#path_annotation_name-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="path_annotation_name-param-annotations">annotations</span> |  | yes |  |  |
| <span id="path_annotation_name-param-path">path</span> |  | yes |  |  |

### Source {#path_annotation_name-source}

`server/vueda/core/paths.py:191`

## `public_ordering_path_to_orm` {#public_ordering_path_to_orm}

A dotted ``?o=`` path translated to the ``__``-joined path ``order_by()`` accepts.

:param path: A dotted ordering path, with any leading ``-`` already removed by
    :func:`split_ordering_direction`.
:type path: str
:return: The path with ``.`` replaced by ``__``.
:rtype: str

### Signature {#public_ordering_path_to_orm-signature}

`public_ordering_path_to_orm(path)`

#### Parameters {#public_ordering_path_to_orm-parameters}

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

### Source {#public_ordering_path_to_orm-source}

`server/vueda/core/paths.py:110`

## `query_path_field` {#query_path_field}

The field that describes the value that a query path names on ``model``.

Django resolves the path itself, so this accepts every path that ``order_by()`` and
``aggregate()`` accept on the model. That includes a path through relations, the ``pk`` alias,
and a path that ends in one or more transforms, such as ``created__year``,
``created__date__year``, a key on a ``JSONField``, or a transform that a project registers.

``Query.names_to_path`` walks the fields and relations that the path names. When transforms follow
them, ``Query.resolve_ref`` applies them, and the result is the output field of the last one.
``created__year`` is an ``IntegerField``, because the database works with the year's number.

When nothing follows them, the result is the last field that the path names, as Django admin's
``get_fields_from_path`` reports it. ``customer`` is the ``customer`` foreign key, ``customer__id``
is the ``id`` field on Customer, and a many-to-many path is the ``ManyToManyField`` itself.
``names_to_path`` would report the through table's foreign key for that last one, which carries
neither the field's choices nor its type. Only the ``pk`` alias, which ``get_fields_from_path``
doesn't know, takes the field that ``names_to_path`` reached.

A path that Django can't resolve raises ``FieldDoesNotExist`` with the message of the
``FieldError`` that Django raised, such as ``Cannot resolve keyword 'yaer' into field. Join on
'released' not permitted.`` That is the error a query on the same path fails with. Any other error,
such as the ``TypeError`` of a transform class that can't be built from the one value a path
gives it, is Django's to raise and passes through unchanged.

:param model: The model the path starts from.
:type model: Type[django.db.models.Model]
:param path: The ``__``-joined path to resolve.
:type path: str
:return: The field that describes the value that the path names.
:rtype: django.db.models.Field
:raises django.core.exceptions.FieldDoesNotExist: If Django can't resolve the path on ``model``.

### Signature {#query_path_field-signature}

`query_path_field(model, path)`

#### Parameters {#query_path_field-parameters}

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

### Source {#query_path_field-source}

`server/vueda/core/paths.py:213`

## `query_path_multiplies_rows` {#query_path_multiplies_rows}

Whether a query path joins through a relation that can match more than one row.

Joining through a reverse foreign key, a many-to-many, or a ``GenericRelation`` produces a row
per related object. ``Query.names_to_path`` marks each such join, and this reads that mark, so a
path that ends in transforms is judged by the relations before them.

:param model: The model the path starts from.
:type model: Type[django.db.models.Model]
:param path: A ``__``-joined path that :func:`query_path_field` resolves.
:type path: str
:return: True when any relation the path joins through can match more than one row.
:rtype: bool

### Signature {#query_path_multiplies_rows-signature}

`query_path_multiplies_rows(model, path)`

#### Parameters {#query_path_multiplies_rows-parameters}

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

### Source {#query_path_multiplies_rows-source}

`server/vueda/core/paths.py:263`

## `reject_wildcard` {#reject_wildcard}

Raise when a path carries the ``*`` wildcard segment sparse fields and expands accept.

Ordering and filters have no "every field at this level" meaning the way ``f``/``e`` do, so a
wildcard reaching either one names nothing rather than naming everything, and is rejected here
rather than silently resolving to no field.

:param path: The path to check.
:type path: str
:raises ValueError: If ``path`` contains the wildcard segment.

### Signature {#reject_wildcard-signature}

`reject_wildcard(path)`

#### Parameters {#reject_wildcard-parameters}

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

### Source {#reject_wildcard-source}

`server/vueda/core/paths.py:175`

## `split_ordering_direction` {#split_ordering_direction}

An ordering term's direction and the path behind it, split apart.

The ``-`` marking descending order sits ahead of the path, not inside it, so it has to come off
before the path is translated and go back on after — a path conversion that ran across the whole
term unchanged would leave the marker exactly where it was, but only by accident, and a filter
grammar that changes the marker's position (there is none today) would need this to still hold.

:param term: An ordering term as it arrives on ``?o=``, e.g. ``"-employee.name"``.
:type term: str
:return: Whether the term is descending, and the path with any ``-`` removed.
:rtype: Tuple[bool, str]

### Signature {#split_ordering_direction-signature}

`split_ordering_direction(term)`

#### Parameters {#split_ordering_direction-parameters}

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

### Source {#split_ordering_direction-source}

`server/vueda/core/paths.py:74`

## Source

`server/vueda/core/paths.py`
