453 lines
12 KiB
Rust
453 lines
12 KiB
Rust
//! Delta-aware graph traversal
|
|
//!
|
|
//! Provides traversal algorithms that account for pending deltas.
|
|
|
|
use std::collections::{HashMap, HashSet, VecDeque};
|
|
|
|
use crate::{EdgeId, GraphDelta, GraphState, NodeId, Result};
|
|
|
|
/// Traversal mode
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum TraversalMode {
|
|
/// Breadth-first search
|
|
Bfs,
|
|
/// Depth-first search
|
|
Dfs,
|
|
/// Dijkstra's shortest path
|
|
Dijkstra,
|
|
/// Best-first (with heuristic)
|
|
BestFirst,
|
|
}
|
|
|
|
/// Configuration for delta-aware traversal
|
|
#[derive(Debug, Clone)]
|
|
pub struct TraversalConfig {
|
|
/// Maximum depth
|
|
pub max_depth: usize,
|
|
/// Maximum nodes to visit
|
|
pub max_nodes: usize,
|
|
/// Edge types to follow (empty = all)
|
|
pub edge_types: Vec<String>,
|
|
/// Direction
|
|
pub direction: TraversalDirection,
|
|
}
|
|
|
|
impl Default for TraversalConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
max_depth: usize::MAX,
|
|
max_nodes: usize::MAX,
|
|
edge_types: Vec::new(),
|
|
direction: TraversalDirection::Outgoing,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Traversal direction
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum TraversalDirection {
|
|
/// Follow outgoing edges
|
|
Outgoing,
|
|
/// Follow incoming edges
|
|
Incoming,
|
|
/// Follow both directions
|
|
Both,
|
|
}
|
|
|
|
/// Delta-aware graph traversal
|
|
pub struct DeltaAwareTraversal<'a> {
|
|
state: &'a GraphState,
|
|
pending_delta: Option<&'a GraphDelta>,
|
|
config: TraversalConfig,
|
|
}
|
|
|
|
impl<'a> DeltaAwareTraversal<'a> {
|
|
/// Create new traversal
|
|
pub fn new(state: &'a GraphState) -> Self {
|
|
Self {
|
|
state,
|
|
pending_delta: None,
|
|
config: TraversalConfig::default(),
|
|
}
|
|
}
|
|
|
|
/// Set pending delta to consider
|
|
pub fn with_delta(mut self, delta: &'a GraphDelta) -> Self {
|
|
self.pending_delta = Some(delta);
|
|
self
|
|
}
|
|
|
|
/// Set configuration
|
|
pub fn with_config(mut self, config: TraversalConfig) -> Self {
|
|
self.config = config;
|
|
self
|
|
}
|
|
|
|
/// BFS from start node
|
|
pub fn bfs(&self, start: &NodeId) -> Vec<NodeId> {
|
|
let mut visited = HashSet::new();
|
|
let mut queue = VecDeque::new();
|
|
let mut result = Vec::new();
|
|
|
|
if !self.node_exists(start) {
|
|
return result;
|
|
}
|
|
|
|
queue.push_back((start.clone(), 0usize));
|
|
visited.insert(start.clone());
|
|
|
|
while let Some((node, depth)) = queue.pop_front() {
|
|
if depth > self.config.max_depth {
|
|
continue;
|
|
}
|
|
if result.len() >= self.config.max_nodes {
|
|
break;
|
|
}
|
|
|
|
result.push(node.clone());
|
|
|
|
for neighbor in self.neighbors(&node) {
|
|
if !visited.contains(&neighbor) {
|
|
visited.insert(neighbor.clone());
|
|
queue.push_back((neighbor, depth + 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// DFS from start node
|
|
pub fn dfs(&self, start: &NodeId) -> Vec<NodeId> {
|
|
let mut visited = HashSet::new();
|
|
let mut stack = Vec::new();
|
|
let mut result = Vec::new();
|
|
|
|
if !self.node_exists(start) {
|
|
return result;
|
|
}
|
|
|
|
stack.push((start.clone(), 0usize));
|
|
|
|
while let Some((node, depth)) = stack.pop() {
|
|
if visited.contains(&node) {
|
|
continue;
|
|
}
|
|
if depth > self.config.max_depth {
|
|
continue;
|
|
}
|
|
if result.len() >= self.config.max_nodes {
|
|
break;
|
|
}
|
|
|
|
visited.insert(node.clone());
|
|
result.push(node.clone());
|
|
|
|
for neighbor in self.neighbors(&node) {
|
|
if !visited.contains(&neighbor) {
|
|
stack.push((neighbor, depth + 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Find shortest path
|
|
pub fn shortest_path(&self, from: &NodeId, to: &NodeId) -> Option<Vec<NodeId>> {
|
|
let mut visited = HashSet::new();
|
|
let mut queue = VecDeque::new();
|
|
let mut parent: HashMap<NodeId, NodeId> = HashMap::new();
|
|
|
|
if !self.node_exists(from) || !self.node_exists(to) {
|
|
return None;
|
|
}
|
|
|
|
queue.push_back(from.clone());
|
|
visited.insert(from.clone());
|
|
|
|
while let Some(node) = queue.pop_front() {
|
|
if &node == to {
|
|
// Reconstruct path
|
|
let mut path = vec![node.clone()];
|
|
let mut current = &node;
|
|
while let Some(p) = parent.get(current) {
|
|
path.push(p.clone());
|
|
current = p;
|
|
}
|
|
path.reverse();
|
|
return Some(path);
|
|
}
|
|
|
|
for neighbor in self.neighbors(&node) {
|
|
if !visited.contains(&neighbor) {
|
|
visited.insert(neighbor.clone());
|
|
parent.insert(neighbor.clone(), node.clone());
|
|
queue.push_back(neighbor);
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Count connected components
|
|
pub fn connected_components(&self) -> Vec<Vec<NodeId>> {
|
|
let mut visited = HashSet::new();
|
|
let mut components = Vec::new();
|
|
|
|
let all_nodes = self.all_nodes();
|
|
|
|
for node in all_nodes {
|
|
if !visited.contains(&node) {
|
|
// Use BFS to find component
|
|
let mut component = Vec::new();
|
|
let mut queue = VecDeque::new();
|
|
queue.push_back(node.clone());
|
|
visited.insert(node.clone());
|
|
|
|
while let Some(n) = queue.pop_front() {
|
|
component.push(n.clone());
|
|
|
|
for neighbor in self.neighbors_both_directions(&n) {
|
|
if !visited.contains(&neighbor) {
|
|
visited.insert(neighbor.clone());
|
|
queue.push_back(neighbor);
|
|
}
|
|
}
|
|
}
|
|
|
|
components.push(component);
|
|
}
|
|
}
|
|
|
|
components
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
fn node_exists(&self, id: &NodeId) -> bool {
|
|
// Check base state
|
|
if self.state.nodes.contains_key(id) {
|
|
// Check if removed by delta
|
|
if let Some(delta) = self.pending_delta {
|
|
if delta.node_removes.contains(id) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Check if added by delta
|
|
if let Some(delta) = self.pending_delta {
|
|
if delta.node_adds.iter().any(|(nid, _)| nid == id) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
fn neighbors(&self, id: &NodeId) -> Vec<NodeId> {
|
|
let mut neighbors = Vec::new();
|
|
|
|
// Get neighbors from base state
|
|
for (edge_id, (source, target, edge_type, _)) in &self.state.edges {
|
|
// Skip if removed by delta
|
|
if let Some(delta) = self.pending_delta {
|
|
if delta.edge_removes.contains(edge_id) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Filter by edge type
|
|
if !self.config.edge_types.is_empty() && !self.config.edge_types.contains(edge_type) {
|
|
continue;
|
|
}
|
|
|
|
match self.config.direction {
|
|
TraversalDirection::Outgoing => {
|
|
if source == id {
|
|
neighbors.push(target.clone());
|
|
}
|
|
}
|
|
TraversalDirection::Incoming => {
|
|
if target == id {
|
|
neighbors.push(source.clone());
|
|
}
|
|
}
|
|
TraversalDirection::Both => {
|
|
if source == id {
|
|
neighbors.push(target.clone());
|
|
} else if target == id {
|
|
neighbors.push(source.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add neighbors from delta edges
|
|
if let Some(delta) = self.pending_delta {
|
|
for edge in &delta.edge_adds {
|
|
if !self.config.edge_types.is_empty()
|
|
&& !self.config.edge_types.contains(&edge.edge_type)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
match self.config.direction {
|
|
TraversalDirection::Outgoing => {
|
|
if &edge.source == id {
|
|
neighbors.push(edge.target.clone());
|
|
}
|
|
}
|
|
TraversalDirection::Incoming => {
|
|
if &edge.target == id {
|
|
neighbors.push(edge.source.clone());
|
|
}
|
|
}
|
|
TraversalDirection::Both => {
|
|
if &edge.source == id {
|
|
neighbors.push(edge.target.clone());
|
|
} else if &edge.target == id {
|
|
neighbors.push(edge.source.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
neighbors
|
|
}
|
|
|
|
fn neighbors_both_directions(&self, id: &NodeId) -> Vec<NodeId> {
|
|
let config = TraversalConfig {
|
|
direction: TraversalDirection::Both,
|
|
..self.config.clone()
|
|
};
|
|
|
|
let traversal = DeltaAwareTraversal {
|
|
state: self.state,
|
|
pending_delta: self.pending_delta,
|
|
config,
|
|
};
|
|
|
|
traversal.neighbors(id)
|
|
}
|
|
|
|
fn all_nodes(&self) -> Vec<NodeId> {
|
|
let mut nodes: HashSet<NodeId> = self.state.nodes.keys().cloned().collect();
|
|
|
|
if let Some(delta) = self.pending_delta {
|
|
// Remove deleted nodes
|
|
for id in &delta.node_removes {
|
|
nodes.remove(id);
|
|
}
|
|
|
|
// Add new nodes
|
|
for (id, _) in &delta.node_adds {
|
|
nodes.insert(id.clone());
|
|
}
|
|
}
|
|
|
|
nodes.into_iter().collect()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::GraphDeltaBuilder;
|
|
|
|
fn create_test_state() -> GraphState {
|
|
let mut state = GraphState::new();
|
|
|
|
let delta = GraphDeltaBuilder::new()
|
|
.add_node("a")
|
|
.add_node("b")
|
|
.add_node("c")
|
|
.add_node("d")
|
|
.add_edge("e1", "a", "b", "KNOWS")
|
|
.add_edge("e2", "b", "c", "KNOWS")
|
|
.add_edge("e3", "c", "d", "KNOWS")
|
|
.build();
|
|
|
|
state.apply_delta(&delta).unwrap();
|
|
state
|
|
}
|
|
|
|
#[test]
|
|
fn test_bfs() {
|
|
let state = create_test_state();
|
|
let traversal = DeltaAwareTraversal::new(&state);
|
|
|
|
let result = traversal.bfs(&"a".to_string());
|
|
|
|
assert_eq!(result.len(), 4);
|
|
assert_eq!(result[0], "a");
|
|
}
|
|
|
|
#[test]
|
|
fn test_dfs() {
|
|
let state = create_test_state();
|
|
let traversal = DeltaAwareTraversal::new(&state);
|
|
|
|
let result = traversal.dfs(&"a".to_string());
|
|
|
|
assert_eq!(result.len(), 4);
|
|
assert_eq!(result[0], "a");
|
|
}
|
|
|
|
#[test]
|
|
fn test_shortest_path() {
|
|
let state = create_test_state();
|
|
let traversal = DeltaAwareTraversal::new(&state);
|
|
|
|
let path = traversal
|
|
.shortest_path(&"a".to_string(), &"d".to_string())
|
|
.unwrap();
|
|
|
|
assert_eq!(path.len(), 4);
|
|
assert_eq!(path[0], "a");
|
|
assert_eq!(path[3], "d");
|
|
}
|
|
|
|
#[test]
|
|
fn test_with_pending_delta() {
|
|
let state = create_test_state();
|
|
|
|
// Add new edge directly from a to d
|
|
let delta = GraphDeltaBuilder::new()
|
|
.add_edge("e4", "a", "d", "SHORTCUT")
|
|
.build();
|
|
|
|
let traversal = DeltaAwareTraversal::new(&state).with_delta(&delta);
|
|
|
|
let path = traversal
|
|
.shortest_path(&"a".to_string(), &"d".to_string())
|
|
.unwrap();
|
|
|
|
// Should find shorter path via new edge
|
|
assert_eq!(path.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_connected_components() {
|
|
let mut state = create_test_state();
|
|
|
|
// Add isolated nodes
|
|
let delta = GraphDeltaBuilder::new()
|
|
.add_node("x")
|
|
.add_node("y")
|
|
.add_edge("ex", "x", "y", "RELATED")
|
|
.build();
|
|
|
|
state.apply_delta(&delta).unwrap();
|
|
|
|
let traversal = DeltaAwareTraversal::new(&state);
|
|
let components = traversal.connected_components();
|
|
|
|
assert_eq!(components.len(), 2);
|
|
}
|
|
}
|