Source code for runic.ogm.query.traversal

"""Traversal steps for the query builder.

A :class:`TraversalStep` is returned by :meth:`QueryBuilder.traverse` and
:meth:`QueryBuilder.repeat`.  It captures the intent for one hop (or a
variable-length path) along a declared :func:`~runic.ogm.core.descriptors.Relation`
field and provides a fluent ``.alias()`` method to register the traversal and
return to the builder chain.

Typical usage
-------------
.. code-block:: python

    results = (
        session.query(User)
        .alias("u")
        .where(User.id == user_id)
        .traverse(User.friends)  # → TraversalStep
        .alias("f")  # → back to QueryBuilder
        .where(User.age > 25, on="f")
        .all()
    )

    # With edge properties:
    rows = (
        session.query(User)
        .alias("u")
        .traverse(User.rated, edge_alias="r")  # → TraversalStep (edge captured)
        .alias("m")  # Movie is the target
        .where(Rated.score > 4.0, on="r")
        .return_nodes("u", "m")
        .return_edge("r")
        .all_with_edges()  # list[tuple[User, Rated, Movie]]
    )

    # Variable-length (e.g. org chart):
    ancestors = (
        session.query(Employee)
        .alias("e")
        .where(Employee.id == emp_id)
        .repeat(Employee.reports_to, min_hops=1, max_hops=5)
        .alias("anc")
        .all()
    )
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from runic.ogm.query.builder import QueryBuilder

log = logging.getLogger(__name__)


[docs] class TraversalStep: """Pending traversal hop; returned by :meth:`QueryBuilder.traverse` and :meth:`QueryBuilder.repeat`. Call :meth:`alias` to complete the step and resume the builder chain. Parameters ---------- builder: The owning :class:`QueryBuilder` instance. field_descriptor: The :class:`~runic.ogm.core.descriptors.FieldDescriptor` for the ``Relation`` field being traversed. source_alias: The Cypher variable name of the source node. optional: When ``True`` (the default), the traversal emits ``OPTIONAL MATCH`` (left-join: source nodes without the relationship are still returned). When ``False``, emits ``MATCH`` (inner-join: drops source nodes that have no such relationship). edge_alias: Optional Cypher variable name for the relationship itself. When set, the generated pattern is ``(src)-[edge_alias:TYPE]->(tgt)`` instead of the anonymous ``(src)-[:TYPE]->(tgt)``, enabling edge property filtering and retrieval. min_hops: Minimum number of hops for variable-length paths (default ``1``). Values > 1 only take effect when combined with *max_hops* to produce a ``*min..max`` quantifier. max_hops: Maximum number of hops. ``None`` means unbounded (``*min..``). A value of ``1`` with ``min_hops=1`` produces a fixed single-hop pattern. """ def __init__( self, builder: QueryBuilder[Any], field_descriptor: Any, source_alias: str, *, optional: bool = True, edge_alias: str | None = None, min_hops: int = 1, max_hops: int | None = 1, ) -> None: self._builder = builder self._fd = field_descriptor self._source_alias = source_alias self._optional = optional self._edge_alias = edge_alias self._min_hops = min_hops self._max_hops = max_hops # ------------------------------------------------------------------ # Fluent terminator # ------------------------------------------------------------------
[docs] def alias(self, name: str) -> QueryBuilder[Any]: """Register the target node alias and append the traversal to the builder. Calling this method: 1. Resolves the target Node class from the Relation field's ``target``. 2. Appends the appropriate ``(OPTIONAL) MATCH`` clause to the builder. 3. Registers ``name → target_cls`` in the builder's alias map. 4. Registers ``edge_alias → Edge class`` if an edge alias was given. 5. Sets the builder's *last alias* (used as the default ``RETURN`` target when no explicit ``return_target()`` is called). Parameters ---------- name: Cypher variable name for the target node (e.g. ``"f"``, ``"m"``). Returns ------- QueryBuilder The owning builder, ready for continued chaining. """ return self._builder.register_traversal( fd=self._fd, source_alias=self._source_alias, target_alias=name, optional=self._optional, edge_alias=self._edge_alias, min_hops=self._min_hops, max_hops=self._max_hops, )