# Traversal And Paths Traversal is split between SQL request normalization, engine dispatch, and low-level BFS/DFS execution over CSR arrays. ## Hot Loop Data `bfs::execute()` receives: | Input | Purpose | |---|---| | `NodeStore` | active bit and table lookup for candidate filtering | | `EdgeStore` | CSR neighbor slices | | `FilterIndex` | typed filter checks | | `BfsConfig` | seed, limits, edge filter, tenant bitmaps, sync overlays | `BfsConfig` owns resolved, hot-loop-friendly values. SQL strings and labels are resolved before entering the traversal loop. Traversal result metadata is adaptive. Dense parent/depth arrays are retained for normal high-coverage traversals, while low-visit-budget traversals on large graphs use sparse metadata to avoid full-graph scratch allocation. ## BFS Flow ```text allocate visited bitmap allocate dense or sparse depth metadata allocate dense or sparse parent metadata allocate dense or sparse parent_edge_type metadata seed frontier while frontier has nodes: pop current stop expanding at max_depth stream neighbors from CSR plus overlay inserts skip overlay-deleted edges candidate filters: already visited? edge type allowed? active node? tenant membership allowed? FilterIndex predicates pass? mark visited store depth, parent, parent edge type enforce max_nodes enqueue if depth allows enforce max_frontier ``` The BFS neighbor iterator streams base CSR edges and overlay inserts without materializing all neighbors into a vector. DFS pushes overlay and base neighbors in reverse stack order without materializing a full per-node neighbor vector. ## Circuit Breakers | Breaker | Enforced by | Result | |---|---|---| | `max_depth` | Current depth check | Node is included but not expanded at depth limit | | `max_nodes` | Visited counter | Traversal returns current partial result | | `max_frontier` | Frontier/stack length | Traversal returns current partial result | Circuit breakers stop traversal and return partial rows; they are not SQL errors. ## Candidate Filtering Order The hot loop filters in this order: 1. Already visited. 2. Edge type not allowed. 3. Node is inactive/tombstoned. 4. Tenant membership excludes node. 5. `FilterIndex` predicates fail. This order keeps cheap checks before typed filter lookups. ## Path Reconstruction BFS/DFS stores: | Vector | Meaning | |---|---| | `parent[node_idx]` | Node that discovered this node | | `parent_edge_type[node_idx]` | Edge label ID from parent to this node | | `depth[node_idx]` | Hop depth; `-1` for unvisited | After traversal, `to_traversal_results()` reconstructs each path by walking parent links backward to the seed, reversing them, and converting node indices to `(table_oid, primary_key)` coordinates. GQL path execution carries relationship identity separately from the public relationship JSON. Base CSR path steps retain the per-adjacency relationship ID when present, and wildcard path deduplication includes that ID so parallel source rows with equal endpoints remain distinct. Pending overlay and layered segment path steps carry the same generation dictionary identity when backed by a mapped source row. Relationship hydration, ACL/RLS visibility, and write rechecks fail closed if that identity is unavailable or belongs to a different mapping. ## Shortest Path `Engine::shortest_path()`: 1. Verifies engine built. 2. Resolves source and target coordinates. 3. Builds a clean CSR or overlay-aware neighbor source. 4. Calls `path_finder::shortest_path_with_neighbors()`. 5. Returns path steps with edge labels. The path finder can use bidirectional search when safe. When the graph contains any unidirectional edges, correctness requires a forward search path that does not assume reverse reachability represents the original directed graph. Shortest-path parent metadata is sparse: unweighted path search records only visited parent links, and weighted path search records parent edge metadata only for relaxed nodes. Bidirectional search tracks each visited node's hop distance from its own root (source for the forward side, target for the backward side) and fully scans every candidate meeting node discovered within the current BFS level before selecting a meeting point, keeping the one with the smallest combined distance. A single level can expose more than one candidate with different combined distances, since the opposite side's recorded distance for any given candidate reflects whenever that node was first visited, not necessarily the shallowest distance available. Selecting the first candidate found instead of the minimum-distance one within the level can return a path longer than the true shortest path; `path_finder::tests::bidirectional_bfs_matches_single_direction_bfs` differentially checks bidirectional results against the always-correct single-direction BFS across randomized graphs to guard against regressing this. ## Weighted Shortest Path Weighted paths use registered edge weights and Dijkstra-style cost expansion. The path layer accumulates costs as `u64` values with checked addition so valid large path costs do not collide with the unreachable sentinel. The SQL wrapper returns one row per path step and exposes `step_cost` and `total_cost` as `bigint`; costs outside the SQL `bigint` range fail instead of being capped. Weighted search still uses a dense distance vector for fast relaxation checks, but parent edge type and weight metadata are stored sparsely and only for nodes that receive a better distance. The SQL wrapper returns no rows when: | Condition | Reason | |---|---| | No path exists | Nothing to return | | No weight data exists | Weighted path cannot be evaluated | Missing per-edge weights in a weighted store default to cost `1`. When pending edge overlays exist, weighted path search is rejected with SQLSTATE `0A000` and diagnostic `PG018` because overlay mutations do not carry edge weights until maintenance merges them into the CSR weight store. ## Source-Table Search Search is intentionally not a graph-artifact lookup. `sql_search.rs` builds SQL over registered source tables and verifies matches against source rows. The Rust recheck prepares query match state once and reuses it across candidate rows so SQL/Rust predicate parity does not require per-row query normalization. Search modes: | Mode | Meaning | |---|---| | `contains` | substring match | | `exact` | whole-value match | | `prefix` | prefix match | | `token` | token match | This keeps search correctness tied to source rows and lets PostgreSQL indexes accelerate search predicates. ## Hydration Hydration happens after graph coordinates are known. It reads source rows by table OID and primary key and returns JSONB. Keep hydration out of internal traversal loops. ## Aggregation Internals `sql_aggregation.rs` parses strict traversal JSON. It rejects unexpected keys so callers cannot silently rely on misspelled or ignored options. The aggregation path can operate on: | Scope | Internal behavior | |---|---| | returned nodes | Traverse and aggregate reached coordinates | | chosen parent path | Expand traversal rows to selected parent path | | all possible paths | Enumerate paths with a hard count cap | Path enumeration must stay capped. Do not add an unbounded aggregate mode. All-path aggregation hydrates each unique coordinate once, then groups hydrated rows by table so repeated path occurrences can use borrowed node-id lookups. Chosen-parent-path aggregation keeps a borrowed cache over traversal rows and only clones hydrated JSON for rows it emits.