Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Query

Traverse (walking a graph)

<Entity>.Traverse(from: seed, follow: e => e.Parent, depth: 10, where: e => p, match: e => e.Col, bidirectional: true)

Walk a relation recursively — an org chart up to its root, a category tree down to its leaves, a bill of materials, a reply thread — and get back every row on the way. It is one recursive SQL query with cycle detection and a depth bound, which is the thing you cannot write with a loop of ordinary queries without paying a round trip per level.

stable3 examples compiled by CIquerydatarelationsgraph

Summary#

Traverse follows a relation over and over, and returns everything it reaches:

entity Employee {
  [Required, MaxLength(120)] string Name;
  Employee Manager;                                     // the relation to walk
  [ForeignKey(Manager)] Employee[] Reports;
  bool Active;
}

Employee[] EveryoneUnder(Employee boss) {
  return Employee.Traverse(
    from:   boss,
    follow: e => e.Reports,          // walk DOWN the collection
    depth:  10,
    where:  e => e.Active);          // …skipping anyone inactive (and everyone below them)
}

That is one query — a recursive one — not a loop that fires a query per level. It detects cycles and it is bounded by depth, so a self-referencing row cannot hang it.

Signature#

<Entity>.Traverse(
  from:          <seed> | [<seed>, <seed>],   // REQUIRED — one row, or several
  follow:        e => e.<Relation>,           // REQUIRED — an entity reference (up) or a collection (down)
  depth:         <positive int literal>,      // optional — default 10
  where:         e => <predicate>,            // optional — pruned at each hop
  match:         e => e.<Column>,             // optional — walk an edge table by VALUE (see below)
  bidirectional: true                         // optional — requires `match:`
)

Named arguments only — a positional argument is a compile error, because six positional arguments would be unreadable and easy to get subtly wrong. depth must be an integer literal, not a variable.

Description#

Up or down — the direction is the relation you follow#

follow: names a relation on the entity, and its kind decides which way you walk:

  • An entity reference (e => e.Manager) walks up — from a row to the one it points at. Seed with an employee and you get their management chain to the root.
  • A collection (e => e.Reports) walks down — from a row to the rows that point at it. Seed with a manager and you get their whole subtree.

Same verb, same query shape, opposite direction. It is the relation that says which.

Employee[] ChainOfCommand(Employee e) {
  return Employee.Traverse(from: e, follow: x => x.Manager);   // an entity REF → walks up
}

where: prunes, it does not filter#

This is the distinction that matters, and it is not the one people expect.

A where: predicate is applied at each hop, so a row that fails it is not just left out of the result — it is not walked through. Everything beneath it is unreachable too. That is usually exactly what you want (an inactive manager's whole branch is out of scope), and occasionally a surprise (you wanted the branch, minus that one row).

If you want to filter the result rather than prune the walk, traverse without a where: and filter what comes back.

depth: is a fuse, not a target#

depth: bounds how far the walk goes; it defaults to 10. It is not a promise that the graph is that deep — it is the thing that stops a walk running away. Combined with cycle detection (a row is never visited twice), a self-referencing hierarchy fails safe rather than hanging.

Raise it when your hierarchy is genuinely deeper; do not remove it, because there is no removing it.

Edges held by value: match: and bidirectional:#

Sometimes the graph is not a declared relation at all — it is an edge table holding two references (a "related product", a "duplicate of", a follower graph). match: walks by column value rather than by relation:

  • follow: names the column to leave by, match: names the column to arrive at.
  • bidirectional: true walks the edge in both directions — the shape a symmetric relationship ("is related to") actually has, where an edge recorded one way should be found from either end.

What it gives back#

A collection of the rows it reached — the same entity type you started from. It is not a chain: you cannot Where or OrderBy a Traverse. Materialise it and work on the result.

Entity-only. There is nothing to traverse on a local list.

Examples#

A category tree, and why a hand-rolled loop is not the same thing:

entity Category {
  [Required, MaxLength(80)] string Name;
  Category Parent;
  [ForeignKey(Parent)] Category[] Children;
}

Category[] Subtree(Category root) {
  return Category.Traverse(from: root, follow: c => c.Children, depth: 6);
}

Category[] Roots(Category a, Category b) {
  return Category.Traverse(from: [a, b], follow: c => c.Parent);   // several seeds at once
}

Written by hand, the first one is a queue, a visited-set, a cycle check, and one query per level — and it is a query per level that makes it slow on a deep tree, which is precisely the thing a recursive query avoids.

See also#

Related

Querying data

How you read data in Osy#. You write C# LINQ; it becomes one SQL statement. The rules that follow from that are the…

Child collections (navigating a relation)

A parent's child collection — `order.Lines` — is not a loaded array. It is a QUERY, correlated to that parent, and…

relations

One entity points at another by declaring it as a member — that is the foreign key. The parent reads its children back…

Include (pre-loading relations)

Pre-load the related rows a query's results are about to navigate to. `Include(o => o.Lines)` does not change what…