13. WITH RECURSIVE
← SQL examples Recursive CTEs (WITH RECURSIVE ...) execute over /sql. There is no prefix gate on WITH - a statement that starts with it routes through the same path a SELECT takes, and plain non-recursive CTEs run there too. The recursive form is evaluated to a fixed point and returns real rows. Bounds worth knowing before you use it are below, and for wide hierarchy walks the graph endpoints are still the better tool.
Walk an org chart down from one root. This runs as written:
WITH RECURSIVE descendants AS (
SELECT id, manager_id FROM hr.employees WHERE id = 'ceo'
UNION ALL
SELECT e.id, e.manager_id
FROM hr.employees e
INNER JOIN descendants d ON e.manager_id = d.id
)
SELECT * FROM descendants The response carries the whole transitive closure from the seed row, and nothing outside it. Access control covers the real tables named in both arms, so a seed or step term you cannot read is refused rather than silently skipped.
UNION ALLbetween the seed and step arms.UNION(distinct) is refused - deduplicating would mean hashing every intermediate row.- Exactly one CTE in a statement that uses
RECURSIVE; chaining a second binding off the recursive one is refused. - No per-CTE column list (
WITH RECURSIVE t(a, b) AS ...). - No aggregate over the recursive relation in the outer projection -
SELECT COUNT(*) FROM tis refused, so count client-side. (Over a plain CTE,COUNT(*)does fold - the asymmetry is deliberate.) - Two caps bound the walk: a depth cap of 100 iterations, and a separate cap of 1,000,000 accumulated rows. Cyclic or wide-branching data hits one of them and comes back as a
400naming the cap it hit - it will not spin or exhaust the engine. Add a visited-set predicate to make such a walk terminate with rows instead. - Filtering and projecting the recursive relation in the outer query works:
SELECT id FROM descendants WHERE id <> 'ceo'.
For a walk that branches widely, or one you want to run repeatedly, declare the parent column as a relation on the schema and walk the edge with depth-bounded BFS instead. This is not a workaround for a missing feature - it is the traversal-shaped tool for a traversal-shaped job.
# A hierarchy walk via the graph endpoint instead of WITH RECURSIVE.
# Assumes the schema declares manager_id as a [[relations]] block:
#
# [[relations]]
# name = "reports_to"
# from_col = "manager_id"
# bidirectional = true
#
# [relations.target]
# namespace = "hr"
# table = "employees"
# pk = "id"
curl "https://$OC_HOST/v1/tenants/$OC_TENANT/graph/hr.employees/bfs?rel=reports_to&pk=ceo&max_depth=10" \
-H "Authorization: Bearer $OC_TOKEN" See Graph reference for BFS, shortest path, k-shortest, and the other 16 graph algorithms. Schema reference → relations covers how to declare the relation.
- BFS takes
max_depthas an explicit per-request argument.WITH RECURSIVEis capped too, but by fixed engine limits you tune the query around rather than pass in. - Forward + reverse traversal are both indexed (bidirectional relations) - "who reports to X" and "who does Y report to" are equally fast.
- For weighted paths or shortest-path, you can swap BFS for Dijkstra or k-shortest without rewriting the schema.