manifoldb_graph/traversal/mod.rs
1//! Graph traversal algorithms.
2//!
3//! This module provides traversal primitives for graph exploration, including
4//! single-hop expansion, multi-hop traversal, shortest path finding, and
5//! path pattern matching.
6//!
7//! # Overview
8//!
9//! Graph traversal is fundamental to querying graph databases. This module
10//! provides the building blocks used by the query engine:
11//!
12//! - [`Expand`] - Single-hop traversal from a node to its neighbors
13//! - [`ExpandAll`] - Multi-hop traversal with depth control
14//! - [`ShortestPath`] - BFS-based unweighted shortest path finding
15//! - [`Dijkstra`] - Weighted shortest path using Dijkstra's algorithm
16//! - [`AStar`] - Goal-directed weighted shortest path with heuristics
17//! - [`PathPattern`] - Pattern matching for paths
18//! - [`TraversalIterator`] - Lazy iteration over traversal results
19//!
20//! # Direction
21//!
22//! All traversal operations support three directions:
23//!
24//! - [`Direction::Outgoing`] - Follow edges from source to target
25//! - [`Direction::Incoming`] - Follow edges from target to source
26//! - [`Direction::Both`] - Follow edges in both directions
27//!
28//! # Weighted vs Unweighted Paths
29//!
30//! For unweighted graphs where all edges have equal cost, use [`ShortestPath`]
31//! which implements BFS for O(V+E) performance.
32//!
33//! For weighted graphs, use [`Dijkstra`] or [`AStar`]:
34//! - [`Dijkstra`] finds the shortest weighted path without any heuristic
35//! - [`AStar`] uses a heuristic to guide the search toward the goal
36//!
37//! Both detect negative edge weights and return an error, as they do not
38//! support negative weights (use Bellman-Ford for that, not yet implemented).
39//!
40//! # Memory Efficiency
41//!
42//! Traversal operations are designed for memory efficiency on large graphs:
43//!
44//! - Use iterators for lazy evaluation
45//! - Support early termination with limits
46//! - Allow filtering during traversal to prune the search space
47//!
48//! # Example
49//!
50//! ```ignore
51//! use manifoldb_graph::traversal::{
52//! Expand, ExpandAll, Direction, TraversalConfig,
53//! ShortestPath, Dijkstra, AStar, EuclideanHeuristic,
54//! };
55//!
56//! // Single-hop: find all friends of a user
57//! let friends = Expand::neighbors(&tx, user_id, Direction::Outgoing)?
58//! .with_edge_type("FRIEND")
59//! .collect()?;
60//!
61//! // Multi-hop: find all users within 3 hops
62//! let nearby = ExpandAll::new(&tx, user_id, Direction::Outgoing)
63//! .with_max_depth(3)
64//! .collect_nodes()?;
65//!
66//! // Unweighted shortest path between two users
67//! let path = ShortestPath::find(&tx, user_a, user_b, Direction::Both)?;
68//!
69//! // Weighted shortest path using edge costs
70//! let path = Dijkstra::new(city_a, city_b, Direction::Outgoing)
71//! .with_weight_property("distance")
72//! .find(&tx)?;
73//!
74//! // A* with Euclidean heuristic for spatial graphs
75//! let path = AStar::new(start, goal, Direction::Outgoing)
76//! .with_heuristic(EuclideanHeuristic::xy())
77//! .with_weight_property("cost")
78//! .find(&tx)?;
79//! ```
80
81mod astar;
82mod dijkstra;
83mod expand;
84mod iterator;
85mod pattern;
86mod shortest_path;
87
88pub use astar::{
89 AStar, ConstantHeuristic, EuclideanHeuristic, Heuristic, ManhattanHeuristic, ZeroHeuristic,
90};
91pub use dijkstra::{Dijkstra, SingleSourceDijkstra, WeightConfig, WeightedPathResult};
92pub use expand::{Expand, ExpandAll, ExpandResult};
93pub use iterator::{
94 NeighborIterator, NeighborIteratorAdapter, TraversalConfig, TraversalIterator,
95 TraversalIteratorAdapter,
96};
97pub use pattern::{PathPattern, PathStep, PatternBuilder, PatternMatch, StepFilter};
98pub use shortest_path::{AllShortestPaths, PathResult, ShortestPath};
99
100use manifoldb_core::{EdgeType, EntityId};
101
102/// Direction for graph traversal.
103///
104/// Specifies which edges to follow when traversing from a node.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
106pub enum Direction {
107 /// Follow outgoing edges (source -> target).
108 ///
109 /// When traversing from node A, find nodes B where an edge A -> B exists.
110 #[default]
111 Outgoing,
112
113 /// Follow incoming edges (target <- source).
114 ///
115 /// When traversing from node A, find nodes B where an edge B -> A exists.
116 Incoming,
117
118 /// Follow edges in both directions.
119 ///
120 /// When traversing from node A, find nodes B where either A -> B or B -> A exists.
121 Both,
122}
123
124impl Direction {
125 /// Returns true if this direction includes outgoing edges.
126 #[inline]
127 pub const fn includes_outgoing(self) -> bool {
128 matches!(self, Self::Outgoing | Self::Both)
129 }
130
131 /// Returns true if this direction includes incoming edges.
132 #[inline]
133 pub const fn includes_incoming(self) -> bool {
134 matches!(self, Self::Incoming | Self::Both)
135 }
136}
137
138/// Filter for traversal operations.
139///
140/// Used to limit which edges and nodes are considered during traversal.
141#[derive(Debug, Clone, Default)]
142pub struct TraversalFilter {
143 /// Only traverse edges of these types.
144 pub edge_types: Option<Vec<EdgeType>>,
145 /// Exclude these nodes from traversal.
146 pub exclude_nodes: Option<Vec<EntityId>>,
147 /// Maximum number of results to return.
148 pub limit: Option<usize>,
149}
150
151impl TraversalFilter {
152 /// Create a new empty filter.
153 #[inline]
154 pub fn new() -> Self {
155 Self::default()
156 }
157
158 /// Filter to only traverse edges of the specified type.
159 pub fn with_edge_type(mut self, edge_type: impl Into<EdgeType>) -> Self {
160 let types = self.edge_types.get_or_insert_with(Vec::new);
161 types.push(edge_type.into());
162 self
163 }
164
165 /// Filter to only traverse edges of the specified types.
166 pub fn with_edge_types(mut self, edge_types: impl IntoIterator<Item = EdgeType>) -> Self {
167 let types = self.edge_types.get_or_insert_with(Vec::new);
168 types.extend(edge_types);
169 self
170 }
171
172 /// Exclude the specified node from traversal results.
173 pub fn exclude_node(mut self, node: EntityId) -> Self {
174 let nodes = self.exclude_nodes.get_or_insert_with(Vec::new);
175 nodes.push(node);
176 self
177 }
178
179 /// Exclude the specified nodes from traversal results.
180 pub fn exclude_nodes(mut self, nodes: impl IntoIterator<Item = EntityId>) -> Self {
181 let exclude = self.exclude_nodes.get_or_insert_with(Vec::new);
182 exclude.extend(nodes);
183 self
184 }
185
186 /// Limit the number of results.
187 pub const fn with_limit(mut self, limit: usize) -> Self {
188 self.limit = Some(limit);
189 self
190 }
191
192 /// Check if a node should be included based on exclusion list.
193 #[inline]
194 pub fn should_include_node(&self, node: EntityId) -> bool {
195 match &self.exclude_nodes {
196 Some(excluded) => !excluded.contains(&node),
197 None => true,
198 }
199 }
200
201 /// Check if an edge type should be included.
202 #[inline]
203 pub fn should_include_edge_type(&self, edge_type: &EdgeType) -> bool {
204 match &self.edge_types {
205 Some(types) => types.contains(edge_type),
206 None => true,
207 }
208 }
209
210 /// Check if we've hit the result limit.
211 #[inline]
212 pub fn is_at_limit(&self, count: usize) -> bool {
213 self.limit.is_some_and(|l| count >= l)
214 }
215}
216
217/// A node with traversal metadata.
218///
219/// Returned by traversal operations to provide context about how a node
220/// was reached during traversal.
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct TraversalNode {
223 /// The entity ID of the node.
224 pub id: EntityId,
225 /// The depth at which this node was discovered (0 = starting node).
226 pub depth: usize,
227}
228
229impl TraversalNode {
230 /// Create a new traversal node.
231 #[inline]
232 pub const fn new(id: EntityId, depth: usize) -> Self {
233 Self { id, depth }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn direction_includes_outgoing() {
243 assert!(Direction::Outgoing.includes_outgoing());
244 assert!(!Direction::Incoming.includes_outgoing());
245 assert!(Direction::Both.includes_outgoing());
246 }
247
248 #[test]
249 fn direction_includes_incoming() {
250 assert!(!Direction::Outgoing.includes_incoming());
251 assert!(Direction::Incoming.includes_incoming());
252 assert!(Direction::Both.includes_incoming());
253 }
254
255 #[test]
256 fn direction_default() {
257 assert_eq!(Direction::default(), Direction::Outgoing);
258 }
259
260 #[test]
261 fn filter_edge_types() {
262 let filter = TraversalFilter::new().with_edge_type("FRIEND").with_edge_type("FOLLOWS");
263
264 assert!(filter.should_include_edge_type(&EdgeType::new("FRIEND")));
265 assert!(filter.should_include_edge_type(&EdgeType::new("FOLLOWS")));
266 assert!(!filter.should_include_edge_type(&EdgeType::new("BLOCKS")));
267 }
268
269 #[test]
270 fn filter_exclude_nodes() {
271 let node1 = EntityId::new(1);
272 let node2 = EntityId::new(2);
273 let node3 = EntityId::new(3);
274
275 let filter = TraversalFilter::new().exclude_node(node1).exclude_node(node2);
276
277 assert!(!filter.should_include_node(node1));
278 assert!(!filter.should_include_node(node2));
279 assert!(filter.should_include_node(node3));
280 }
281
282 #[test]
283 fn filter_limit() {
284 let filter = TraversalFilter::new().with_limit(10);
285
286 assert!(!filter.is_at_limit(0));
287 assert!(!filter.is_at_limit(9));
288 assert!(filter.is_at_limit(10));
289 assert!(filter.is_at_limit(11));
290 }
291
292 #[test]
293 fn empty_filter_allows_all() {
294 let filter = TraversalFilter::new();
295
296 assert!(filter.should_include_node(EntityId::new(1)));
297 assert!(filter.should_include_edge_type(&EdgeType::new("ANY")));
298 assert!(!filter.is_at_limit(1000));
299 }
300
301 #[test]
302 fn traversal_node_creation() {
303 let node = TraversalNode::new(EntityId::new(42), 3);
304 assert_eq!(node.id, EntityId::new(42));
305 assert_eq!(node.depth, 3);
306 }
307}