//! # RuVector Delta Graph //! //! Delta operations for graph structures, supporting incremental updates //! to edges and node properties. //! //! ## Key Features //! //! - Edge delta operations (add, remove, update weight) //! - Node property deltas (including vector properties) //! - Delta-aware graph traversal //! - Efficient batch updates #![warn(missing_docs)] #![warn(clippy::all)] pub mod edge_delta; pub mod error; pub mod node_delta; pub mod traversal; use std::collections::{HashMap, HashSet}; use ruvector_delta_core::{Delta, DeltaStream, VectorDelta}; use smallvec::SmallVec; pub use edge_delta::{EdgeDelta, EdgeOp}; pub use error::{GraphDeltaError, Result}; pub use node_delta::{NodeDelta, PropertyDelta}; pub use traversal::{DeltaAwareTraversal, TraversalMode}; /// A node ID type pub type NodeId = String; /// An edge ID type pub type EdgeId = String; /// Property value types #[derive(Debug, Clone, PartialEq)] pub enum PropertyValue { /// Null/missing value Null, /// Boolean Bool(bool), /// Integer Int(i64), /// Float Float(f64), /// String String(String), /// Vector (for embeddings) Vector(Vec), /// List of values List(Vec), /// Map of values Map(HashMap), } impl Default for PropertyValue { fn default() -> Self { Self::Null } } /// A graph delta representing changes to graph structure #[derive(Debug, Clone)] pub struct GraphDelta { /// Node additions pub node_adds: Vec<(NodeId, HashMap)>, /// Node removals pub node_removes: Vec, /// Node property updates pub node_updates: Vec<(NodeId, NodeDelta)>, /// Edge additions pub edge_adds: Vec, /// Edge removals pub edge_removes: Vec, /// Edge updates pub edge_updates: Vec<(EdgeId, EdgeDelta)>, } /// An edge addition #[derive(Debug, Clone)] pub struct EdgeAddition { /// Edge ID pub id: EdgeId, /// Source node pub source: NodeId, /// Target node pub target: NodeId, /// Edge type/label pub edge_type: String, /// Edge properties pub properties: HashMap, } impl GraphDelta { /// Create an empty graph delta pub fn new() -> Self { Self { node_adds: Vec::new(), node_removes: Vec::new(), node_updates: Vec::new(), edge_adds: Vec::new(), edge_removes: Vec::new(), edge_updates: Vec::new(), } } /// Check if delta is empty pub fn is_empty(&self) -> bool { self.node_adds.is_empty() && self.node_removes.is_empty() && self.node_updates.is_empty() && self.edge_adds.is_empty() && self.edge_removes.is_empty() && self.edge_updates.is_empty() } /// Get total number of operations pub fn operation_count(&self) -> usize { self.node_adds.len() + self.node_removes.len() + self.node_updates.len() + self.edge_adds.len() + self.edge_removes.len() + self.edge_updates.len() } /// Add a node addition pub fn add_node(&mut self, id: NodeId, properties: HashMap) { self.node_adds.push((id, properties)); } /// Add a node removal pub fn remove_node(&mut self, id: NodeId) { self.node_removes.push(id); } /// Add a node update pub fn update_node(&mut self, id: NodeId, delta: NodeDelta) { self.node_updates.push((id, delta)); } /// Add an edge pub fn add_edge(&mut self, edge: EdgeAddition) { self.edge_adds.push(edge); } /// Remove an edge pub fn remove_edge(&mut self, id: EdgeId) { self.edge_removes.push(id); } /// Update an edge pub fn update_edge(&mut self, id: EdgeId, delta: EdgeDelta) { self.edge_updates.push((id, delta)); } /// Compose with another graph delta pub fn compose(mut self, other: GraphDelta) -> Self { // Handle conflicting operations let removed_nodes: HashSet<_> = other.node_removes.iter().collect(); let removed_edges: HashSet<_> = other.edge_removes.iter().collect(); // Filter out adds that are removed self.node_adds.retain(|(id, _)| !removed_nodes.contains(id)); self.edge_adds.retain(|e| !removed_edges.contains(&e.id)); // Merge adds self.node_adds.extend(other.node_adds); self.node_removes.extend(other.node_removes); self.edge_adds.extend(other.edge_adds); self.edge_removes.extend(other.edge_removes); // Compose node updates let mut node_update_map: HashMap = HashMap::new(); for (id, delta) in self.node_updates { node_update_map.insert(id, delta); } for (id, delta) in other.node_updates { node_update_map .entry(id) .and_modify(|existing| *existing = existing.clone().compose(delta.clone())) .or_insert(delta); } self.node_updates = node_update_map.into_iter().collect(); // Compose edge updates let mut edge_update_map: HashMap = HashMap::new(); for (id, delta) in self.edge_updates { edge_update_map.insert(id, delta); } for (id, delta) in other.edge_updates { edge_update_map .entry(id) .and_modify(|existing| *existing = existing.clone().compose(delta.clone())) .or_insert(delta); } self.edge_updates = edge_update_map.into_iter().collect(); self } /// Compute inverse delta pub fn inverse(&self) -> Self { // Note: Full inverse requires knowing original state // This is a partial inverse that works for simple cases Self { node_adds: Vec::new(), // Would need originals to undo removes node_removes: self.node_adds.iter().map(|(id, _)| id.clone()).collect(), node_updates: self .node_updates .iter() .map(|(id, d)| (id.clone(), d.inverse())) .collect(), edge_adds: Vec::new(), edge_removes: self.edge_adds.iter().map(|e| e.id.clone()).collect(), edge_updates: self .edge_updates .iter() .map(|(id, d)| (id.clone(), d.inverse())) .collect(), } } /// Get affected node IDs pub fn affected_nodes(&self) -> HashSet { let mut nodes = HashSet::new(); for (id, _) in &self.node_adds { nodes.insert(id.clone()); } for id in &self.node_removes { nodes.insert(id.clone()); } for (id, _) in &self.node_updates { nodes.insert(id.clone()); } for edge in &self.edge_adds { nodes.insert(edge.source.clone()); nodes.insert(edge.target.clone()); } nodes } /// Get affected edge IDs pub fn affected_edges(&self) -> HashSet { let mut edges = HashSet::new(); for edge in &self.edge_adds { edges.insert(edge.id.clone()); } for id in &self.edge_removes { edges.insert(id.clone()); } for (id, _) in &self.edge_updates { edges.insert(id.clone()); } edges } } impl Default for GraphDelta { fn default() -> Self { Self::new() } } /// Builder for graph deltas pub struct GraphDeltaBuilder { delta: GraphDelta, } impl GraphDeltaBuilder { /// Create a new builder pub fn new() -> Self { Self { delta: GraphDelta::new(), } } /// Add a node pub fn add_node(mut self, id: impl Into) -> Self { self.delta.add_node(id.into(), HashMap::new()); self } /// Add a node with properties pub fn add_node_with_props( mut self, id: impl Into, props: HashMap, ) -> Self { self.delta.add_node(id.into(), props); self } /// Remove a node pub fn remove_node(mut self, id: impl Into) -> Self { self.delta.remove_node(id.into()); self } /// Add an edge pub fn add_edge( mut self, id: impl Into, source: impl Into, target: impl Into, edge_type: impl Into, ) -> Self { self.delta.add_edge(EdgeAddition { id: id.into(), source: source.into(), target: target.into(), edge_type: edge_type.into(), properties: HashMap::new(), }); self } /// Remove an edge pub fn remove_edge(mut self, id: impl Into) -> Self { self.delta.remove_edge(id.into()); self } /// Build the delta pub fn build(self) -> GraphDelta { self.delta } } impl Default for GraphDeltaBuilder { fn default() -> Self { Self::new() } } /// Stream of graph deltas for event sourcing pub struct GraphDeltaStream { stream: DeltaStream, } // Wrapper to implement Delta trait #[derive(Clone)] struct WrappedGraphDelta(GraphDelta); impl Delta for WrappedGraphDelta { type Base = GraphState; type Error = GraphDeltaError; fn compute(_old: &Self::Base, _new: &Self::Base) -> Self { // Would diff two graph states WrappedGraphDelta(GraphDelta::new()) } fn apply(&self, base: &mut Self::Base) -> std::result::Result<(), Self::Error> { base.apply_delta(&self.0) } fn compose(self, other: Self) -> Self { WrappedGraphDelta(self.0.compose(other.0)) } fn inverse(&self) -> Self { WrappedGraphDelta(self.0.inverse()) } fn is_identity(&self) -> bool { self.0.is_empty() } fn byte_size(&self) -> usize { // Approximate size std::mem::size_of::() + self.0.operation_count() * 100 } } impl Default for WrappedGraphDelta { fn default() -> Self { Self(GraphDelta::new()) } } /// Simplified graph state for delta application #[derive(Debug, Clone, Default)] pub struct GraphState { /// Nodes with properties pub nodes: HashMap>, /// Edges pub edges: HashMap)>, } impl GraphState { /// Create empty state pub fn new() -> Self { Self::default() } /// Apply a graph delta pub fn apply_delta(&mut self, delta: &GraphDelta) -> Result<()> { // Remove nodes for id in &delta.node_removes { self.nodes.remove(id); // Also remove incident edges self.edges.retain(|_, (s, t, _, _)| s != id && t != id); } // Remove edges for id in &delta.edge_removes { self.edges.remove(id); } // Add nodes for (id, props) in &delta.node_adds { self.nodes.insert(id.clone(), props.clone()); } // Add edges for edge in &delta.edge_adds { self.edges.insert( edge.id.clone(), ( edge.source.clone(), edge.target.clone(), edge.edge_type.clone(), edge.properties.clone(), ), ); } // Update nodes for (id, node_delta) in &delta.node_updates { if let Some(props) = self.nodes.get_mut(id) { for prop_delta in &node_delta.property_deltas { match &prop_delta.operation { PropertyOp::Set(value) => { props.insert(prop_delta.key.clone(), value.clone()); } PropertyOp::Remove => { props.remove(&prop_delta.key); } PropertyOp::VectorDelta(vd) => { if let Some(PropertyValue::Vector(v)) = props.get_mut(&prop_delta.key) { vd.apply(v) .map_err(|e| GraphDeltaError::DeltaError(format!("{:?}", e)))?; } } } } } } // Update edges for (id, edge_delta) in &delta.edge_updates { if let Some((_, _, _, props)) = self.edges.get_mut(id) { for prop_delta in &edge_delta.property_deltas { match &prop_delta.operation { PropertyOp::Set(value) => { props.insert(prop_delta.key.clone(), value.clone()); } PropertyOp::Remove => { props.remove(&prop_delta.key); } PropertyOp::VectorDelta(_) => { // Edges typically don't have vector properties } } } } } Ok(()) } /// Get node count pub fn node_count(&self) -> usize { self.nodes.len() } /// Get edge count pub fn edge_count(&self) -> usize { self.edges.len() } } /// Property operation types #[derive(Debug, Clone)] pub enum PropertyOp { /// Set property value Set(PropertyValue), /// Remove property Remove, /// Apply vector delta to vector property VectorDelta(VectorDelta), } #[cfg(test)] mod tests { use super::*; #[test] fn test_graph_delta_builder() { let delta = GraphDeltaBuilder::new() .add_node("a") .add_node("b") .add_edge("e1", "a", "b", "KNOWS") .build(); assert_eq!(delta.node_adds.len(), 2); assert_eq!(delta.edge_adds.len(), 1); } #[test] fn test_graph_state_apply() { let mut state = GraphState::new(); let delta = GraphDeltaBuilder::new() .add_node("a") .add_node("b") .add_edge("e1", "a", "b", "KNOWS") .build(); state.apply_delta(&delta).unwrap(); assert_eq!(state.node_count(), 2); assert_eq!(state.edge_count(), 1); } #[test] fn test_delta_compose() { let d1 = GraphDeltaBuilder::new().add_node("a").add_node("b").build(); let d2 = GraphDeltaBuilder::new() .remove_node("b") .add_node("c") .build(); let composed = d1.compose(d2); // "a" added, "b" removed, "c" added assert_eq!(composed.node_adds.len(), 2); // a and c assert_eq!(composed.node_removes.len(), 1); // b } #[test] fn test_affected_nodes() { let delta = GraphDeltaBuilder::new() .add_node("a") .add_edge("e1", "b", "c", "RELATES") .build(); let affected = delta.affected_nodes(); assert!(affected.contains("a")); assert!(affected.contains("b")); assert!(affected.contains("c")); } }