Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
[package]
name = "ruvector-delta-graph"
version = "0.1.0"
edition = "2021"
description = "Delta operations for graph structures - edge and node changes"
license = "MIT OR Apache-2.0"
repository = "https://github.com/ruvnet/ruvector"
keywords = ["graph", "delta", "incremental", "streaming"]
categories = ["data-structures", "algorithms"]
[features]
default = []
parallel = ["rayon"]
serde = ["dep:serde", "dep:serde_json"]
[dependencies]
# Core delta library
ruvector-delta-core = { path = "../ruvector-delta-core" }
# Error handling
thiserror = "2.0"
# Data structures
parking_lot = "0.12"
dashmap = "6.0"
smallvec = { version = "1.13", features = ["union"] }
# Optional parallelism
rayon = { version = "1.10", optional = true }
# Optional serialization
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
[dev-dependencies]
criterion = "0.6"
proptest = "1.4"
# Benchmarks will be added later
# [[bench]]
# name = "graph_delta"
# harness = false

View File

@@ -0,0 +1,235 @@
//! Edge delta operations
//!
//! Represents changes to edges in a graph.
use std::collections::HashMap;
use ruvector_delta_core::VectorDelta;
use crate::{EdgeId, NodeId, PropertyOp, PropertyValue};
/// An operation on an edge
#[derive(Debug, Clone)]
pub enum EdgeOp {
/// Add a new edge
Add {
source: NodeId,
target: NodeId,
edge_type: String,
properties: HashMap<String, PropertyValue>,
},
/// Remove an edge
Remove,
/// Update edge properties
Update(Vec<PropertyDelta>),
/// Change edge type
Retype { new_type: String },
}
/// A property delta for edges
#[derive(Debug, Clone)]
pub struct PropertyDelta {
/// Property key
pub key: String,
/// Operation
pub operation: PropertyOp,
}
impl PropertyDelta {
/// Create a set operation
pub fn set(key: impl Into<String>, value: PropertyValue) -> Self {
Self {
key: key.into(),
operation: PropertyOp::Set(value),
}
}
/// Create a remove operation
pub fn remove(key: impl Into<String>) -> Self {
Self {
key: key.into(),
operation: PropertyOp::Remove,
}
}
}
/// Delta for an edge
#[derive(Debug, Clone)]
pub struct EdgeDelta {
/// Property deltas
pub property_deltas: Vec<PropertyDelta>,
/// Weight change (if applicable)
pub weight_delta: Option<f64>,
/// Type change
pub type_change: Option<String>,
}
impl EdgeDelta {
/// Create empty delta
pub fn new() -> Self {
Self {
property_deltas: Vec::new(),
weight_delta: None,
type_change: None,
}
}
/// Check if empty
pub fn is_empty(&self) -> bool {
self.property_deltas.is_empty() && self.weight_delta.is_none() && self.type_change.is_none()
}
/// Add a property set
pub fn set_property(mut self, key: impl Into<String>, value: PropertyValue) -> Self {
self.property_deltas.push(PropertyDelta::set(key, value));
self
}
/// Add a property removal
pub fn remove_property(mut self, key: impl Into<String>) -> Self {
self.property_deltas.push(PropertyDelta::remove(key));
self
}
/// Set weight delta
pub fn with_weight_delta(mut self, delta: f64) -> Self {
self.weight_delta = Some(delta);
self
}
/// Set type change
pub fn with_type_change(mut self, new_type: impl Into<String>) -> Self {
self.type_change = Some(new_type.into());
self
}
/// Compose with another delta
pub fn compose(mut self, other: EdgeDelta) -> Self {
// Merge property deltas
let mut prop_map: HashMap<String, PropertyDelta> = HashMap::new();
for pd in self.property_deltas {
prop_map.insert(pd.key.clone(), pd);
}
for pd in other.property_deltas {
prop_map.insert(pd.key.clone(), pd);
}
self.property_deltas = prop_map.into_values().collect();
// Compose weight delta
if let Some(od) = other.weight_delta {
self.weight_delta = Some(self.weight_delta.unwrap_or(0.0) + od);
}
// Take latest type change
if other.type_change.is_some() {
self.type_change = other.type_change;
}
self
}
/// Compute inverse
pub fn inverse(&self) -> Self {
Self {
property_deltas: Vec::new(), // Can't invert without originals
weight_delta: self.weight_delta.map(|d| -d),
type_change: None, // Can't invert without original type
}
}
}
impl Default for EdgeDelta {
fn default() -> Self {
Self::new()
}
}
/// Builder for edge operations
pub struct EdgeDeltaBuilder {
delta: EdgeDelta,
}
impl EdgeDeltaBuilder {
/// Create new builder
pub fn new() -> Self {
Self {
delta: EdgeDelta::new(),
}
}
/// Set a property
pub fn set(mut self, key: impl Into<String>, value: PropertyValue) -> Self {
self.delta
.property_deltas
.push(PropertyDelta::set(key, value));
self
}
/// Remove a property
pub fn remove(mut self, key: impl Into<String>) -> Self {
self.delta.property_deltas.push(PropertyDelta::remove(key));
self
}
/// Set weight delta
pub fn weight(mut self, delta: f64) -> Self {
self.delta.weight_delta = Some(delta);
self
}
/// Set type change
pub fn retype(mut self, new_type: impl Into<String>) -> Self {
self.delta.type_change = Some(new_type.into());
self
}
/// Build the delta
pub fn build(self) -> EdgeDelta {
self.delta
}
}
impl Default for EdgeDeltaBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_edge_delta_builder() {
let delta = EdgeDeltaBuilder::new()
.set("weight", PropertyValue::Float(1.5))
.set("label", PropertyValue::String("test".into()))
.build();
assert_eq!(delta.property_deltas.len(), 2);
}
#[test]
fn test_edge_delta_compose() {
let d1 = EdgeDelta::new()
.set_property("a", PropertyValue::Int(1))
.with_weight_delta(1.0);
let d2 = EdgeDelta::new()
.set_property("b", PropertyValue::Int(2))
.with_weight_delta(0.5);
let composed = d1.compose(d2);
assert_eq!(composed.property_deltas.len(), 2);
assert!((composed.weight_delta.unwrap() - 1.5).abs() < 1e-6);
}
#[test]
fn test_edge_delta_inverse() {
let delta = EdgeDelta::new().with_weight_delta(2.0);
let inverse = delta.inverse();
assert!((inverse.weight_delta.unwrap() - (-2.0)).abs() < 1e-6);
}
}

View File

@@ -0,0 +1,38 @@
//! Error types for graph delta operations
use std::fmt;
/// Result type for graph delta operations
pub type Result<T> = std::result::Result<T, GraphDeltaError>;
/// Errors that can occur during graph delta operations
#[derive(Debug, Clone)]
pub enum GraphDeltaError {
/// Node not found
NodeNotFound(String),
/// Edge not found
EdgeNotFound(String),
/// Invalid operation
InvalidOperation(String),
/// Delta error
DeltaError(String),
/// Cycle detected
CycleDetected,
/// Constraint violation
ConstraintViolation(String),
}
impl fmt::Display for GraphDeltaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NodeNotFound(id) => write!(f, "Node not found: {}", id),
Self::EdgeNotFound(id) => write!(f, "Edge not found: {}", id),
Self::InvalidOperation(msg) => write!(f, "Invalid operation: {}", msg),
Self::DeltaError(msg) => write!(f, "Delta error: {}", msg),
Self::CycleDetected => write!(f, "Cycle detected in graph"),
Self::ConstraintViolation(msg) => write!(f, "Constraint violation: {}", msg),
}
}
}
impl std::error::Error for GraphDeltaError {}

View File

@@ -0,0 +1,562 @@
//! # 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<f32>),
/// List of values
List(Vec<PropertyValue>),
/// Map of values
Map(HashMap<String, PropertyValue>),
}
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<String, PropertyValue>)>,
/// Node removals
pub node_removes: Vec<NodeId>,
/// Node property updates
pub node_updates: Vec<(NodeId, NodeDelta)>,
/// Edge additions
pub edge_adds: Vec<EdgeAddition>,
/// Edge removals
pub edge_removes: Vec<EdgeId>,
/// 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<String, PropertyValue>,
}
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<String, PropertyValue>) {
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<NodeId, NodeDelta> = 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<EdgeId, EdgeDelta> = 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<NodeId> {
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<EdgeId> {
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<NodeId>) -> 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<NodeId>,
props: HashMap<String, PropertyValue>,
) -> Self {
self.delta.add_node(id.into(), props);
self
}
/// Remove a node
pub fn remove_node(mut self, id: impl Into<NodeId>) -> Self {
self.delta.remove_node(id.into());
self
}
/// Add an edge
pub fn add_edge(
mut self,
id: impl Into<EdgeId>,
source: impl Into<NodeId>,
target: impl Into<NodeId>,
edge_type: impl Into<String>,
) -> 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<EdgeId>) -> 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<WrappedGraphDelta>,
}
// 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::<GraphDelta>() + 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<NodeId, HashMap<String, PropertyValue>>,
/// Edges
pub edges: HashMap<EdgeId, (NodeId, NodeId, String, HashMap<String, PropertyValue>)>,
}
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"));
}
}

View File

@@ -0,0 +1,280 @@
//! Node delta operations
//!
//! Represents changes to nodes in a graph.
use std::collections::HashMap;
use ruvector_delta_core::{Delta, VectorDelta};
use crate::{NodeId, PropertyOp, PropertyValue};
/// A property delta for nodes
#[derive(Debug, Clone)]
pub struct PropertyDelta {
/// Property key
pub key: String,
/// Operation
pub operation: PropertyOp,
}
impl PropertyDelta {
/// Create a set operation
pub fn set(key: impl Into<String>, value: PropertyValue) -> Self {
Self {
key: key.into(),
operation: PropertyOp::Set(value),
}
}
/// Create a remove operation
pub fn remove(key: impl Into<String>) -> Self {
Self {
key: key.into(),
operation: PropertyOp::Remove,
}
}
/// Create a vector delta operation
pub fn vector_delta(key: impl Into<String>, delta: VectorDelta) -> Self {
Self {
key: key.into(),
operation: PropertyOp::VectorDelta(delta),
}
}
}
/// Delta for a node
#[derive(Debug, Clone)]
pub struct NodeDelta {
/// Property deltas
pub property_deltas: Vec<PropertyDelta>,
/// Label changes (add/remove)
pub label_adds: Vec<String>,
/// Labels to remove
pub label_removes: Vec<String>,
}
impl NodeDelta {
/// Create empty delta
pub fn new() -> Self {
Self {
property_deltas: Vec::new(),
label_adds: Vec::new(),
label_removes: Vec::new(),
}
}
/// Check if empty
pub fn is_empty(&self) -> bool {
self.property_deltas.is_empty()
&& self.label_adds.is_empty()
&& self.label_removes.is_empty()
}
/// Add a property set
pub fn set_property(mut self, key: impl Into<String>, value: PropertyValue) -> Self {
self.property_deltas.push(PropertyDelta::set(key, value));
self
}
/// Add a property removal
pub fn remove_property(mut self, key: impl Into<String>) -> Self {
self.property_deltas.push(PropertyDelta::remove(key));
self
}
/// Add a vector delta for an embedding property
pub fn vector_delta(mut self, key: impl Into<String>, delta: VectorDelta) -> Self {
self.property_deltas
.push(PropertyDelta::vector_delta(key, delta));
self
}
/// Add a label
pub fn add_label(mut self, label: impl Into<String>) -> Self {
self.label_adds.push(label.into());
self
}
/// Remove a label
pub fn remove_label(mut self, label: impl Into<String>) -> Self {
self.label_removes.push(label.into());
self
}
/// Compose with another delta
pub fn compose(mut self, other: NodeDelta) -> Self {
// Merge property deltas (later overrides earlier)
let mut prop_map: HashMap<String, PropertyDelta> = HashMap::new();
for pd in self.property_deltas {
prop_map.insert(pd.key.clone(), pd);
}
for pd in other.property_deltas {
// For vector deltas, compose them
if let PropertyOp::VectorDelta(vd2) = &pd.operation {
if let Some(existing) = prop_map.get(&pd.key) {
if let PropertyOp::VectorDelta(vd1) = &existing.operation {
let composed = vd1.clone().compose(vd2.clone());
prop_map.insert(
pd.key.clone(),
PropertyDelta::vector_delta(pd.key.clone(), composed),
);
continue;
}
}
}
prop_map.insert(pd.key.clone(), pd);
}
self.property_deltas = prop_map.into_values().collect();
// Merge label changes
let mut adds: std::collections::HashSet<String> = self.label_adds.into_iter().collect();
let mut removes: std::collections::HashSet<String> =
self.label_removes.into_iter().collect();
for label in other.label_adds {
removes.remove(&label);
adds.insert(label);
}
for label in other.label_removes {
adds.remove(&label);
removes.insert(label);
}
self.label_adds = adds.into_iter().collect();
self.label_removes = removes.into_iter().collect();
self
}
/// Compute inverse
pub fn inverse(&self) -> Self {
// Swap adds and removes for labels
// Property deltas can't be fully inverted without originals
Self {
property_deltas: Vec::new(),
label_adds: self.label_removes.clone(),
label_removes: self.label_adds.clone(),
}
}
}
impl Default for NodeDelta {
fn default() -> Self {
Self::new()
}
}
/// Builder for node deltas
pub struct NodeDeltaBuilder {
delta: NodeDelta,
}
impl NodeDeltaBuilder {
/// Create new builder
pub fn new() -> Self {
Self {
delta: NodeDelta::new(),
}
}
/// Set a property
pub fn set(mut self, key: impl Into<String>, value: PropertyValue) -> Self {
self.delta
.property_deltas
.push(PropertyDelta::set(key, value));
self
}
/// Remove a property
pub fn remove(mut self, key: impl Into<String>) -> Self {
self.delta.property_deltas.push(PropertyDelta::remove(key));
self
}
/// Apply vector delta
pub fn vector(mut self, key: impl Into<String>, delta: VectorDelta) -> Self {
self.delta
.property_deltas
.push(PropertyDelta::vector_delta(key, delta));
self
}
/// Add a label
pub fn add_label(mut self, label: impl Into<String>) -> Self {
self.delta.label_adds.push(label.into());
self
}
/// Remove a label
pub fn remove_label(mut self, label: impl Into<String>) -> Self {
self.delta.label_removes.push(label.into());
self
}
/// Build the delta
pub fn build(self) -> NodeDelta {
self.delta
}
}
impl Default for NodeDeltaBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_delta_builder() {
let delta = NodeDeltaBuilder::new()
.set("name", PropertyValue::String("Alice".into()))
.set("age", PropertyValue::Int(30))
.add_label("Person")
.build();
assert_eq!(delta.property_deltas.len(), 2);
assert_eq!(delta.label_adds.len(), 1);
}
#[test]
fn test_node_delta_compose() {
let d1 = NodeDelta::new()
.set_property("a", PropertyValue::Int(1))
.add_label("Label1");
let d2 = NodeDelta::new()
.set_property("b", PropertyValue::Int(2))
.remove_label("Label1")
.add_label("Label2");
let composed = d1.compose(d2);
assert_eq!(composed.property_deltas.len(), 2);
assert!(composed.label_adds.contains(&"Label2".to_string()));
assert!(!composed.label_adds.contains(&"Label1".to_string()));
}
#[test]
fn test_vector_delta_compose() {
let vd1 = VectorDelta::from_dense(vec![1.0, 0.0, 0.0]);
let vd2 = VectorDelta::from_dense(vec![0.0, 1.0, 0.0]);
let d1 = NodeDelta::new().vector_delta("embedding", vd1);
let d2 = NodeDelta::new().vector_delta("embedding", vd2);
let composed = d1.compose(d2);
assert_eq!(composed.property_deltas.len(), 1);
if let PropertyOp::VectorDelta(vd) = &composed.property_deltas[0].operation {
// Should be composed delta
assert!(!vd.is_identity());
} else {
panic!("Expected vector delta");
}
}
}

View File

@@ -0,0 +1,452 @@
//! 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);
}
}