1use std::collections::HashMap;
6use std::sync::Arc;
7
8use parking_lot::RwLock;
9
10#[derive(Debug, Clone, uniffi::Record, serde::Serialize, serde::Deserialize)]
12pub struct MobileGraphNode {
13 pub id: u64,
15 pub label: String,
17 pub properties_json: Option<String>,
19 pub vector: Option<Vec<f32>>,
21}
22
23#[derive(Debug, Clone, uniffi::Record, serde::Serialize, serde::Deserialize)]
25pub struct MobileGraphEdge {
26 pub id: u64,
28 pub source: u64,
30 pub target: u64,
32 pub label: String,
34 pub properties_json: Option<String>,
36}
37
38#[derive(Debug, Clone, uniffi::Record)]
46pub struct TraversalResult {
47 pub node_id: u64,
49 pub path: Vec<u64>,
51 pub depth: u32,
53}
54
55fn properties_to_json(
61 properties: &std::collections::HashMap<String, serde_json::Value>,
62) -> Option<String> {
63 if properties.is_empty() {
64 return None;
65 }
66 serde_json::to_string(properties).ok()
67}
68
69impl From<velesdb_core::GraphNode> for MobileGraphNode {
70 fn from(node: velesdb_core::GraphNode) -> Self {
71 Self {
72 id: node.id(),
73 label: node.label().to_string(),
74 properties_json: properties_to_json(node.properties()),
75 vector: node.vector().cloned(),
76 }
77 }
78}
79
80impl From<velesdb_core::GraphEdge> for MobileGraphEdge {
81 fn from(edge: velesdb_core::GraphEdge) -> Self {
82 Self {
83 id: edge.id(),
84 source: edge.source(),
85 target: edge.target(),
86 label: edge.label().to_string(),
87 properties_json: properties_to_json(edge.properties()),
88 }
89 }
90}
91
92impl From<velesdb_core::TraversalResult> for TraversalResult {
93 fn from(result: velesdb_core::TraversalResult) -> Self {
94 Self {
95 node_id: result.target_id,
96 path: result.path,
97 depth: result.depth,
98 }
99 }
100}
101
102#[derive(uniffi::Object)]
108pub struct MobileGraphStore {
109 nodes: RwLock<HashMap<u64, MobileGraphNode>>,
110 edges: RwLock<HashMap<u64, MobileGraphEdge>>,
111 outgoing: RwLock<HashMap<u64, Vec<u64>>>,
112 incoming: RwLock<HashMap<u64, Vec<u64>>>,
113}
114
115#[derive(serde::Serialize, serde::Deserialize)]
118struct GraphSnapshot {
119 nodes: Vec<MobileGraphNode>,
120 edges: Vec<MobileGraphEdge>,
121}
122
123#[uniffi::export]
124impl MobileGraphStore {
125 #[uniffi::constructor]
127 pub fn new() -> Arc<Self> {
128 Arc::new(Self {
129 nodes: RwLock::new(HashMap::new()),
130 edges: RwLock::new(HashMap::new()),
131 outgoing: RwLock::new(HashMap::new()),
132 incoming: RwLock::new(HashMap::new()),
133 })
134 }
135
136 pub fn save(&self, path: String) -> Result<(), crate::VelesError> {
151 let edges: Vec<MobileGraphEdge> = self.edges.read().values().cloned().collect();
152 let nodes: Vec<MobileGraphNode> = self.nodes.read().values().cloned().collect();
153 let snapshot = GraphSnapshot { nodes, edges };
154 let bytes = serde_json::to_vec(&snapshot)
155 .map_err(|e| crate::VelesError::database(format!("Graph serialize failed: {e}")))?;
156 std::fs::write(&path, bytes)
157 .map_err(|e| crate::VelesError::database(format!("Graph save to '{path}' failed: {e}")))
158 }
159
160 #[uniffi::constructor]
163 pub fn load(path: String) -> Result<Arc<Self>, crate::VelesError> {
164 let bytes = std::fs::read(&path).map_err(|e| {
165 crate::VelesError::database(format!("Graph load from '{path}' failed: {e}"))
166 })?;
167 let snapshot: GraphSnapshot = serde_json::from_slice(&bytes)
168 .map_err(|e| crate::VelesError::database(format!("Graph deserialize failed: {e}")))?;
169 let store = Self::new();
170 for node in snapshot.nodes {
171 store.add_node(node);
172 }
173 for edge in snapshot.edges {
174 store.add_edge(edge)?;
175 }
176 Ok(store)
177 }
178
179 pub fn add_node(&self, node: MobileGraphNode) {
181 let mut nodes = self.nodes.write();
182 nodes.insert(node.id, node);
183 }
184
185 pub fn add_edge(&self, edge: MobileGraphEdge) -> Result<(), crate::VelesError> {
193 let mut edges = self.edges.write();
197 let mut outgoing = self.outgoing.write();
198 let mut incoming = self.incoming.write();
199
200 if edges.contains_key(&edge.id) {
201 return Err(crate::VelesError::database(format!(
202 "Edge with ID {} already exists",
203 edge.id
204 )));
205 }
206
207 let source = edge.source;
208 let target = edge.target;
209 let id = edge.id;
210
211 edges.insert(id, edge);
213 outgoing.entry(source).or_default().push(id);
214 incoming.entry(target).or_default().push(id);
215
216 Ok(())
218 }
219
220 pub fn get_node(&self, id: u64) -> Option<MobileGraphNode> {
222 let nodes = self.nodes.read();
223 nodes.get(&id).cloned()
224 }
225
226 pub fn get_edge(&self, id: u64) -> Option<MobileGraphEdge> {
228 let edges = self.edges.read();
229 edges.get(&id).cloned()
230 }
231
232 pub fn node_count(&self) -> u64 {
234 let nodes = self.nodes.read();
235 nodes.len() as u64
236 }
237
238 pub fn edge_count(&self) -> u64 {
240 let edges = self.edges.read();
241 edges.len() as u64
242 }
243
244 pub fn get_outgoing(&self, node_id: u64) -> Vec<MobileGraphEdge> {
251 self.get_edges_from_index(node_id, &self.outgoing)
252 }
253
254 pub fn get_incoming(&self, node_id: u64) -> Vec<MobileGraphEdge> {
261 self.get_edges_from_index(node_id, &self.incoming)
262 }
263
264 pub fn get_outgoing_by_label(&self, node_id: u64, label: String) -> Vec<MobileGraphEdge> {
266 self.get_outgoing(node_id)
267 .into_iter()
268 .filter(|e| e.label == label)
269 .collect()
270 }
271
272 pub fn get_neighbors(&self, node_id: u64) -> Vec<u64> {
274 self.get_outgoing(node_id)
275 .into_iter()
276 .map(|e| e.target)
277 .collect()
278 }
279
280 pub fn bfs_traverse(&self, source_id: u64, max_depth: u32, limit: u32) -> Vec<TraversalResult> {
288 self.bfs_traverse_parallel(vec![source_id], max_depth, limit)
289 }
290
291 pub fn bfs_traverse_parallel(
302 &self,
303 source_ids: Vec<u64>,
304 max_depth: u32,
305 limit: u32,
306 ) -> Vec<TraversalResult> {
307 use std::collections::{HashSet, VecDeque};
308
309 let mut results: Vec<TraversalResult> = Vec::new();
310 let mut visited: HashSet<u64> = HashSet::new();
311 let mut queue: VecDeque<(u64, u32, Vec<u64>)> = VecDeque::new();
312
313 for &source_id in &source_ids {
314 if visited.insert(source_id) {
315 queue.push_back((source_id, 0, Vec::new()));
316 }
317 }
318
319 while let Some((node_id, depth, path)) = queue.pop_front() {
320 if results.len() >= limit as usize {
321 break;
322 }
323
324 if depth > 0 {
325 results.push(TraversalResult {
326 node_id,
327 path: path.clone(),
328 depth,
329 });
330 }
331
332 self.enqueue_neighbors(node_id, depth, max_depth, &path, &mut visited, &mut queue);
333 }
334
335 results
336 }
337
338 pub fn remove_node(&self, node_id: u64) {
345 let mut edges = self.edges.write();
348 let mut outgoing = self.outgoing.write();
349 let mut incoming = self.incoming.write();
350 let mut nodes = self.nodes.write();
351
352 nodes.remove(&node_id);
353
354 let outgoing_ids: Vec<u64> = outgoing.remove(&node_id).unwrap_or_default();
355 for edge_id in outgoing_ids {
356 if let Some(edge) = edges.remove(&edge_id) {
357 if let Some(ids) = incoming.get_mut(&edge.target) {
358 ids.retain(|&id| id != edge_id);
359 }
360 }
361 }
362
363 let incoming_ids: Vec<u64> = incoming.remove(&node_id).unwrap_or_default();
364 for edge_id in incoming_ids {
365 if let Some(edge) = edges.remove(&edge_id) {
366 if let Some(ids) = outgoing.get_mut(&edge.source) {
367 ids.retain(|&id| id != edge_id);
368 }
369 }
370 }
371 }
372
373 pub fn remove_edge(&self, edge_id: u64) {
380 let mut edges = self.edges.write();
382 let mut outgoing = self.outgoing.write();
383 let mut incoming = self.incoming.write();
384
385 if let Some(edge) = edges.remove(&edge_id) {
386 if let Some(ids) = outgoing.get_mut(&edge.source) {
387 ids.retain(|&id| id != edge_id);
388 }
389 if let Some(ids) = incoming.get_mut(&edge.target) {
390 ids.retain(|&id| id != edge_id);
391 }
392 }
393 }
395
396 pub fn clear(&self) {
402 let mut edges = self.edges.write();
404 let mut outgoing = self.outgoing.write();
405 let mut incoming = self.incoming.write();
406 let mut nodes = self.nodes.write();
407
408 edges.clear();
409 outgoing.clear();
410 incoming.clear();
411 nodes.clear();
412 }
413
414 pub fn dfs_traverse(&self, source_id: u64, max_depth: u32, limit: u32) -> Vec<TraversalResult> {
422 use std::collections::HashSet;
423
424 let mut results: Vec<TraversalResult> = Vec::new();
425 let mut visited: HashSet<u64> = HashSet::new();
426 let mut stack: Vec<(u64, u32, Vec<u64>)> = vec![(source_id, 0, Vec::new())];
427
428 while let Some((node_id, depth, path)) = stack.pop() {
429 if results.len() >= limit as usize {
430 break;
431 }
432
433 if visited.contains(&node_id) {
434 continue;
435 }
436 visited.insert(node_id);
437
438 if depth > 0 {
439 results.push(TraversalResult {
440 node_id,
441 path: path.clone(),
442 depth,
443 });
444 }
445
446 if depth < max_depth {
447 let neighbors: Vec<_> = self
448 .get_outgoing(node_id)
449 .into_iter()
450 .filter(|e| !visited.contains(&e.target))
451 .collect();
452
453 for edge in neighbors.into_iter().rev() {
454 let mut next_path = path.clone();
455 next_path.push(edge.id);
456 stack.push((edge.target, depth + 1, next_path));
457 }
458 }
459 }
460
461 results
462 }
463
464 pub fn has_node(&self, id: u64) -> bool {
466 let nodes = self.nodes.read();
467 nodes.contains_key(&id)
468 }
469
470 pub fn has_edge(&self, id: u64) -> bool {
472 let edges = self.edges.read();
473 edges.contains_key(&id)
474 }
475
476 #[allow(clippy::cast_possible_truncation)]
478 pub fn out_degree(&self, node_id: u64) -> u32 {
479 let outgoing = self.outgoing.read();
480 outgoing.get(&node_id).map_or(0, |v| v.len() as u32)
482 }
483
484 #[allow(clippy::cast_possible_truncation)]
486 pub fn in_degree(&self, node_id: u64) -> u32 {
487 let incoming = self.incoming.read();
488 incoming.get(&node_id).map_or(0, |v| v.len() as u32)
490 }
491
492 pub fn get_nodes_by_label(&self, label: String) -> Vec<MobileGraphNode> {
494 let nodes = self.nodes.read();
495 nodes
496 .values()
497 .filter(|n| n.label == label)
498 .cloned()
499 .collect()
500 }
501
502 pub fn get_edges_by_label(&self, label: String) -> Vec<MobileGraphEdge> {
504 let edges = self.edges.read();
505 edges
506 .values()
507 .filter(|e| e.label == label)
508 .cloned()
509 .collect()
510 }
511}
512
513impl MobileGraphStore {
515 fn get_edges_from_index(
522 &self,
523 node_id: u64,
524 index: &RwLock<HashMap<u64, Vec<u64>>>,
525 ) -> Vec<MobileGraphEdge> {
526 let edges = self.edges.read();
527 let idx = index.read();
528 idx.get(&node_id)
529 .map(|ids| ids.iter().filter_map(|id| edges.get(id).cloned()).collect())
530 .unwrap_or_default()
531 }
532
533 fn enqueue_neighbors(
540 &self,
541 node_id: u64,
542 depth: u32,
543 max_depth: u32,
544 path: &[u64],
545 visited: &mut std::collections::HashSet<u64>,
546 queue: &mut std::collections::VecDeque<(u64, u32, Vec<u64>)>,
547 ) {
548 if depth >= max_depth {
549 return;
550 }
551 for edge in self.get_outgoing(node_id) {
552 if visited.insert(edge.target) {
553 let mut next_path = path.to_vec();
554 next_path.push(edge.id);
555 queue.push_back((edge.target, depth + 1, next_path));
556 }
557 }
558 }
559}
560
561impl Default for MobileGraphStore {
562 fn default() -> Self {
563 Self {
564 nodes: RwLock::new(HashMap::new()),
565 edges: RwLock::new(HashMap::new()),
566 outgoing: RwLock::new(HashMap::new()),
567 incoming: RwLock::new(HashMap::new()),
568 }
569 }
570}
571
572#[cfg(test)]
573#[path = "graph_tests.rs"]
574mod tests;