exo_backend_classical/
graph.rs1use exo_core::{
4 EntityId, HyperedgeId, HyperedgeResult, Relation, SheafConsistencyResult, TopologicalQuery,
5};
6use ruvector_graph::{GraphDB, Hyperedge, Node};
7use std::str::FromStr;
8
9use exo_core::{Error as ExoError, Result as ExoResult};
10
11#[cfg(test)]
12use exo_core::RelationType;
13
14pub struct GraphWrapper {
16 db: GraphDB,
18}
19
20impl GraphWrapper {
21 pub fn new() -> Self {
23 Self { db: GraphDB::new() }
24 }
25
26 pub fn create_hyperedge(
28 &mut self,
29 entities: &[EntityId],
30 relation: &Relation,
31 ) -> ExoResult<HyperedgeId> {
32 for entity_id in entities {
34 let entity_id_str = entity_id.0.to_string();
35 if self.db.get_node(&entity_id_str).is_none() {
36 use ruvector_graph::types::{Label, Properties};
38 let node = Node::new(entity_id_str, vec![Label::new("Entity")], Properties::new());
39 self.db
40 .create_node(node)
41 .map_err(|e| ExoError::Backend(format!("Failed to create node: {}", e)))?;
42 }
43 }
44
45 let entity_strs: Vec<String> = entities.iter().map(|e| e.0.to_string()).collect();
47
48 let mut hyperedge = Hyperedge::new(entity_strs, relation.relation_type.0.clone());
49
50 if let Some(obj) = relation.properties.as_object() {
52 for (key, value) in obj {
53 if let Ok(prop_val) = serde_json::from_value(value.clone()) {
54 hyperedge.properties.insert(key.clone(), prop_val);
55 }
56 }
57 }
58
59 let hyperedge_id_str = hyperedge.id.clone();
60
61 self.db
62 .create_hyperedge(hyperedge)
63 .map_err(|e| ExoError::Backend(format!("Failed to create hyperedge: {}", e)))?;
64
65 let uuid = uuid::Uuid::from_str(&hyperedge_id_str).unwrap_or_else(|_| uuid::Uuid::new_v4());
67 Ok(HyperedgeId(uuid))
68 }
69
70 pub fn get_node(&self, id: &EntityId) -> Option<Node> {
72 self.db.get_node(&id.0.to_string())
73 }
74
75 pub fn get_hyperedge(&self, id: &HyperedgeId) -> Option<Hyperedge> {
77 self.db.get_hyperedge(&id.0.to_string())
78 }
79
80 pub fn query(&self, query: &TopologicalQuery) -> ExoResult<HyperedgeResult> {
82 match query {
83 TopologicalQuery::PersistentHomology {
84 dimension: _,
85 epsilon_range: _,
86 } => {
87 Ok(HyperedgeResult::NotSupported)
91 }
92 TopologicalQuery::BettiNumbers { max_dimension } => {
93 let betti_0 = self.approximate_connected_components();
100
101 let mut betti = vec![betti_0];
104 for _ in 1..=*max_dimension {
105 betti.push(0); }
107
108 Ok(HyperedgeResult::BettiNumbers(betti))
109 }
110 TopologicalQuery::SheafConsistency { local_sections: _ } => {
111 Ok(HyperedgeResult::SheafConsistency(
114 SheafConsistencyResult::Inconsistent(vec![
115 "Sheaf consistency not supported on classical backend".to_string(),
116 ]),
117 ))
118 }
119 }
120 }
121
122 fn approximate_connected_components(&self) -> usize {
124 1
128 }
129
130 pub fn hyperedges_containing(&self, node_id: &EntityId) -> Vec<Hyperedge> {
132 self.db.get_hyperedges_by_node(&node_id.0.to_string())
134 }
135}
136
137impl Default for GraphWrapper {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use std::collections::HashMap;
147
148 #[test]
149 fn test_graph_creation() {
150 let graph = GraphWrapper::new();
151 assert!(graph.db.get_node("nonexistent").is_none());
153 }
154
155 #[test]
156 fn test_create_hyperedge() {
157 let mut graph = GraphWrapper::new();
158
159 let entities = vec![EntityId::new(), EntityId::new(), EntityId::new()];
160 let relation = Relation {
161 relation_type: RelationType::new("related_to"),
162 properties: serde_json::json!({}),
163 };
164
165 let result = graph.create_hyperedge(&entities, &relation);
166 assert!(result.is_ok());
167 }
168
169 #[test]
170 fn test_topological_query() {
171 let graph = GraphWrapper::new();
172
173 let query = TopologicalQuery::BettiNumbers { max_dimension: 2 };
174 let result = graph.query(&query);
175 assert!(result.is_ok());
176
177 if let Ok(HyperedgeResult::BettiNumbers(betti)) = result {
178 assert_eq!(betti.len(), 3); }
180 }
181}