Skip to main content

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}