1use std::collections::HashMap;
6use std::sync::Arc;
7
8#[derive(Debug, Clone, uniffi::Record)]
10pub struct MobileGraphNode {
11 pub id: u64,
13 pub label: String,
15 pub properties_json: Option<String>,
17 pub vector: Option<Vec<f32>>,
19}
20
21#[derive(Debug, Clone, uniffi::Record)]
23pub struct MobileGraphEdge {
24 pub id: u64,
26 pub source: u64,
28 pub target: u64,
30 pub label: String,
32 pub properties_json: Option<String>,
34}
35
36#[derive(Debug, Clone, uniffi::Record)]
38pub struct TraversalResult {
39 pub node_id: u64,
41 pub depth: u32,
43}
44
45#[derive(uniffi::Object)]
47pub struct MobileGraphStore {
48 nodes: std::sync::RwLock<HashMap<u64, MobileGraphNode>>,
49 edges: std::sync::RwLock<HashMap<u64, MobileGraphEdge>>,
50 outgoing: std::sync::RwLock<HashMap<u64, Vec<u64>>>,
51 incoming: std::sync::RwLock<HashMap<u64, Vec<u64>>>,
52}
53
54#[uniffi::export]
55impl MobileGraphStore {
56 #[uniffi::constructor]
58 pub fn new() -> Arc<Self> {
59 Arc::new(Self {
60 nodes: std::sync::RwLock::new(HashMap::new()),
61 edges: std::sync::RwLock::new(HashMap::new()),
62 outgoing: std::sync::RwLock::new(HashMap::new()),
63 incoming: std::sync::RwLock::new(HashMap::new()),
64 })
65 }
66
67 pub fn add_node(&self, node: MobileGraphNode) {
69 let mut nodes = self.nodes.write().unwrap();
70 nodes.insert(node.id, node);
71 }
72
73 pub fn add_edge(&self, edge: MobileGraphEdge) -> Result<(), crate::VelesError> {
81 let mut edges = self.edges.write().unwrap();
85 let mut outgoing = self.outgoing.write().unwrap();
86 let mut incoming = self.incoming.write().unwrap();
87
88 if edges.contains_key(&edge.id) {
89 return Err(crate::VelesError::Database {
90 message: format!("Edge with ID {} already exists", edge.id),
91 });
92 }
93
94 let source = edge.source;
95 let target = edge.target;
96 let id = edge.id;
97
98 edges.insert(id, edge);
100 outgoing.entry(source).or_default().push(id);
101 incoming.entry(target).or_default().push(id);
102
103 Ok(())
105 }
106
107 pub fn get_node(&self, id: u64) -> Option<MobileGraphNode> {
109 let nodes = self.nodes.read().unwrap();
110 nodes.get(&id).cloned()
111 }
112
113 pub fn get_edge(&self, id: u64) -> Option<MobileGraphEdge> {
115 let edges = self.edges.read().unwrap();
116 edges.get(&id).cloned()
117 }
118
119 pub fn node_count(&self) -> u64 {
121 let nodes = self.nodes.read().unwrap();
122 nodes.len() as u64
123 }
124
125 pub fn edge_count(&self) -> u64 {
127 let edges = self.edges.read().unwrap();
128 edges.len() as u64
129 }
130
131 pub fn get_outgoing(&self, node_id: u64) -> Vec<MobileGraphEdge> {
138 let edges = self.edges.read().unwrap();
140 let outgoing = self.outgoing.read().unwrap();
141
142 outgoing
143 .get(&node_id)
144 .map(|ids| ids.iter().filter_map(|id| edges.get(id).cloned()).collect())
145 .unwrap_or_default()
146 }
147
148 pub fn get_incoming(&self, node_id: u64) -> Vec<MobileGraphEdge> {
155 let edges = self.edges.read().unwrap();
157 let incoming = self.incoming.read().unwrap();
158
159 incoming
160 .get(&node_id)
161 .map(|ids| ids.iter().filter_map(|id| edges.get(id).cloned()).collect())
162 .unwrap_or_default()
163 }
164
165 pub fn get_outgoing_by_label(&self, node_id: u64, label: String) -> Vec<MobileGraphEdge> {
167 self.get_outgoing(node_id)
168 .into_iter()
169 .filter(|e| e.label == label)
170 .collect()
171 }
172
173 pub fn get_neighbors(&self, node_id: u64) -> Vec<u64> {
175 self.get_outgoing(node_id)
176 .into_iter()
177 .map(|e| e.target)
178 .collect()
179 }
180
181 pub fn bfs_traverse(&self, source_id: u64, max_depth: u32, limit: u32) -> Vec<TraversalResult> {
189 use std::collections::{HashSet, VecDeque};
190
191 let mut results: Vec<TraversalResult> = Vec::new();
192 let mut visited: HashSet<u64> = HashSet::new();
193 let mut queue: VecDeque<(u64, u32)> = VecDeque::new();
194
195 queue.push_back((source_id, 0));
196 visited.insert(source_id);
197
198 while let Some((node_id, depth)) = queue.pop_front() {
199 if results.len() >= limit as usize {
200 break;
201 }
202
203 if depth > 0 {
204 results.push(TraversalResult { node_id, depth });
205 }
206
207 if depth < max_depth {
208 for edge in self.get_outgoing(node_id) {
209 if !visited.contains(&edge.target) {
210 visited.insert(edge.target);
211 queue.push_back((edge.target, depth + 1));
212 }
213 }
214 }
215 }
216
217 results
218 }
219
220 pub fn remove_node(&self, node_id: u64) {
227 let mut edges = self.edges.write().unwrap();
230 let mut outgoing = self.outgoing.write().unwrap();
231 let mut incoming = self.incoming.write().unwrap();
232 let mut nodes = self.nodes.write().unwrap();
233
234 nodes.remove(&node_id);
235
236 let outgoing_ids: Vec<u64> = outgoing.remove(&node_id).unwrap_or_default();
237 for edge_id in outgoing_ids {
238 if let Some(edge) = edges.remove(&edge_id) {
239 if let Some(ids) = incoming.get_mut(&edge.target) {
240 ids.retain(|&id| id != edge_id);
241 }
242 }
243 }
244
245 let incoming_ids: Vec<u64> = incoming.remove(&node_id).unwrap_or_default();
246 for edge_id in incoming_ids {
247 if let Some(edge) = edges.remove(&edge_id) {
248 if let Some(ids) = outgoing.get_mut(&edge.source) {
249 ids.retain(|&id| id != edge_id);
250 }
251 }
252 }
253 }
254
255 pub fn remove_edge(&self, edge_id: u64) {
262 let mut edges = self.edges.write().unwrap();
264 let mut outgoing = self.outgoing.write().unwrap();
265 let mut incoming = self.incoming.write().unwrap();
266
267 if let Some(edge) = edges.remove(&edge_id) {
268 if let Some(ids) = outgoing.get_mut(&edge.source) {
269 ids.retain(|&id| id != edge_id);
270 }
271 if let Some(ids) = incoming.get_mut(&edge.target) {
272 ids.retain(|&id| id != edge_id);
273 }
274 }
275 }
277
278 pub fn clear(&self) {
284 let mut edges = self.edges.write().unwrap();
286 let mut outgoing = self.outgoing.write().unwrap();
287 let mut incoming = self.incoming.write().unwrap();
288 let mut nodes = self.nodes.write().unwrap();
289
290 edges.clear();
291 outgoing.clear();
292 incoming.clear();
293 nodes.clear();
294 }
295
296 pub fn dfs_traverse(&self, source_id: u64, max_depth: u32, limit: u32) -> Vec<TraversalResult> {
304 use std::collections::HashSet;
305
306 let mut results: Vec<TraversalResult> = Vec::new();
307 let mut visited: HashSet<u64> = HashSet::new();
308 let mut stack: Vec<(u64, u32)> = vec![(source_id, 0)];
309
310 while let Some((node_id, depth)) = stack.pop() {
311 if results.len() >= limit as usize {
312 break;
313 }
314
315 if visited.contains(&node_id) {
316 continue;
317 }
318 visited.insert(node_id);
319
320 if depth > 0 {
321 results.push(TraversalResult { node_id, depth });
322 }
323
324 if depth < max_depth {
325 let neighbors: Vec<_> = self
326 .get_outgoing(node_id)
327 .into_iter()
328 .filter(|e| !visited.contains(&e.target))
329 .collect();
330
331 for edge in neighbors.into_iter().rev() {
332 stack.push((edge.target, depth + 1));
333 }
334 }
335 }
336
337 results
338 }
339
340 pub fn has_node(&self, id: u64) -> bool {
342 let nodes = self.nodes.read().unwrap();
343 nodes.contains_key(&id)
344 }
345
346 pub fn has_edge(&self, id: u64) -> bool {
348 let edges = self.edges.read().unwrap();
349 edges.contains_key(&id)
350 }
351
352 #[allow(clippy::cast_possible_truncation)]
354 pub fn out_degree(&self, node_id: u64) -> u32 {
355 let outgoing = self.outgoing.read().unwrap();
356 outgoing.get(&node_id).map_or(0, |v| v.len() as u32)
358 }
359
360 #[allow(clippy::cast_possible_truncation)]
362 pub fn in_degree(&self, node_id: u64) -> u32 {
363 let incoming = self.incoming.read().unwrap();
364 incoming.get(&node_id).map_or(0, |v| v.len() as u32)
366 }
367
368 pub fn get_nodes_by_label(&self, label: String) -> Vec<MobileGraphNode> {
370 let nodes = self.nodes.read().unwrap();
371 nodes
372 .values()
373 .filter(|n| n.label == label)
374 .cloned()
375 .collect()
376 }
377
378 pub fn get_edges_by_label(&self, label: String) -> Vec<MobileGraphEdge> {
380 let edges = self.edges.read().unwrap();
381 edges
382 .values()
383 .filter(|e| e.label == label)
384 .cloned()
385 .collect()
386 }
387}
388
389impl Default for MobileGraphStore {
390 fn default() -> Self {
391 Self {
392 nodes: std::sync::RwLock::new(HashMap::new()),
393 edges: std::sync::RwLock::new(HashMap::new()),
394 outgoing: std::sync::RwLock::new(HashMap::new()),
395 incoming: std::sync::RwLock::new(HashMap::new()),
396 }
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn test_mobile_graph_node_creation() {
406 let node = MobileGraphNode {
407 id: 1,
408 label: "Person".to_string(),
409 properties_json: Some(r#"{"name": "John"}"#.to_string()),
410 vector: None,
411 };
412 assert_eq!(node.id, 1);
413 assert_eq!(node.label, "Person");
414 }
415
416 #[test]
417 fn test_mobile_graph_edge_creation() {
418 let edge = MobileGraphEdge {
419 id: 100,
420 source: 1,
421 target: 2,
422 label: "KNOWS".to_string(),
423 properties_json: None,
424 };
425 assert_eq!(edge.id, 100);
426 assert_eq!(edge.source, 1);
427 assert_eq!(edge.target, 2);
428 }
429
430 #[test]
431 fn test_mobile_graph_store_add_nodes() {
432 let store = MobileGraphStore::new();
433 store.add_node(MobileGraphNode {
434 id: 1,
435 label: "Person".to_string(),
436 properties_json: None,
437 vector: None,
438 });
439 assert_eq!(store.node_count(), 1);
440 }
441
442 #[test]
443 fn test_mobile_graph_store_add_edges() {
444 let store = MobileGraphStore::new();
445 store.add_node(MobileGraphNode {
446 id: 1,
447 label: "Person".to_string(),
448 properties_json: None,
449 vector: None,
450 });
451 store.add_node(MobileGraphNode {
452 id: 2,
453 label: "Person".to_string(),
454 properties_json: None,
455 vector: None,
456 });
457
458 let result = store.add_edge(MobileGraphEdge {
459 id: 100,
460 source: 1,
461 target: 2,
462 label: "KNOWS".to_string(),
463 properties_json: None,
464 });
465
466 assert!(result.is_ok());
467 assert_eq!(store.edge_count(), 1);
468 }
469
470 #[test]
471 fn test_mobile_graph_store_duplicate_edge_error() {
472 let store = MobileGraphStore::new();
473 store.add_node(MobileGraphNode {
474 id: 1,
475 label: "Person".to_string(),
476 properties_json: None,
477 vector: None,
478 });
479 store.add_node(MobileGraphNode {
480 id: 2,
481 label: "Person".to_string(),
482 properties_json: None,
483 vector: None,
484 });
485
486 let _ = store.add_edge(MobileGraphEdge {
487 id: 100,
488 source: 1,
489 target: 2,
490 label: "KNOWS".to_string(),
491 properties_json: None,
492 });
493
494 let result = store.add_edge(MobileGraphEdge {
496 id: 100,
497 source: 1,
498 target: 2,
499 label: "KNOWS".to_string(),
500 properties_json: None,
501 });
502
503 assert!(result.is_err());
504 }
505
506 #[test]
507 fn test_mobile_graph_store_get_outgoing() {
508 let store = MobileGraphStore::new();
509 store.add_node(MobileGraphNode {
510 id: 1,
511 label: "Person".to_string(),
512 properties_json: None,
513 vector: None,
514 });
515 store.add_node(MobileGraphNode {
516 id: 2,
517 label: "Person".to_string(),
518 properties_json: None,
519 vector: None,
520 });
521 store.add_node(MobileGraphNode {
522 id: 3,
523 label: "Person".to_string(),
524 properties_json: None,
525 vector: None,
526 });
527
528 let _ = store.add_edge(MobileGraphEdge {
529 id: 100,
530 source: 1,
531 target: 2,
532 label: "KNOWS".to_string(),
533 properties_json: None,
534 });
535 let _ = store.add_edge(MobileGraphEdge {
536 id: 101,
537 source: 1,
538 target: 3,
539 label: "KNOWS".to_string(),
540 properties_json: None,
541 });
542
543 let outgoing = store.get_outgoing(1);
544 assert_eq!(outgoing.len(), 2);
545 }
546
547 #[test]
548 fn test_mobile_graph_store_bfs_traverse() {
549 let store = MobileGraphStore::new();
550
551 for i in 1..=4 {
553 store.add_node(MobileGraphNode {
554 id: i,
555 label: "Person".to_string(),
556 properties_json: None,
557 vector: None,
558 });
559 }
560
561 let _ = store.add_edge(MobileGraphEdge {
563 id: 100,
564 source: 1,
565 target: 2,
566 label: "KNOWS".to_string(),
567 properties_json: None,
568 });
569 let _ = store.add_edge(MobileGraphEdge {
570 id: 101,
571 source: 2,
572 target: 3,
573 label: "KNOWS".to_string(),
574 properties_json: None,
575 });
576 let _ = store.add_edge(MobileGraphEdge {
577 id: 102,
578 source: 3,
579 target: 4,
580 label: "KNOWS".to_string(),
581 properties_json: None,
582 });
583
584 let results = store.bfs_traverse(1, 3, 100);
585
586 assert_eq!(results.len(), 3);
588 assert!(results.iter().any(|r| r.node_id == 2 && r.depth == 1));
589 assert!(results.iter().any(|r| r.node_id == 3 && r.depth == 2));
590 assert!(results.iter().any(|r| r.node_id == 4 && r.depth == 3));
591 }
592
593 #[test]
594 fn test_mobile_graph_store_remove_node() {
595 let store = MobileGraphStore::new();
596
597 store.add_node(MobileGraphNode {
598 id: 1,
599 label: "Person".to_string(),
600 properties_json: None,
601 vector: None,
602 });
603 store.add_node(MobileGraphNode {
604 id: 2,
605 label: "Person".to_string(),
606 properties_json: None,
607 vector: None,
608 });
609
610 let _ = store.add_edge(MobileGraphEdge {
611 id: 100,
612 source: 1,
613 target: 2,
614 label: "KNOWS".to_string(),
615 properties_json: None,
616 });
617
618 assert_eq!(store.node_count(), 2);
619 assert_eq!(store.edge_count(), 1);
620
621 store.remove_node(1);
622
623 assert_eq!(store.node_count(), 1);
624 assert_eq!(store.edge_count(), 0); }
626
627 #[test]
628 fn test_mobile_graph_store_remove_edge() {
629 let store = MobileGraphStore::new();
630
631 store.add_node(MobileGraphNode {
632 id: 1,
633 label: "Person".to_string(),
634 properties_json: None,
635 vector: None,
636 });
637 store.add_node(MobileGraphNode {
638 id: 2,
639 label: "Person".to_string(),
640 properties_json: None,
641 vector: None,
642 });
643
644 let _ = store.add_edge(MobileGraphEdge {
645 id: 100,
646 source: 1,
647 target: 2,
648 label: "KNOWS".to_string(),
649 properties_json: None,
650 });
651
652 assert_eq!(store.edge_count(), 1);
653
654 store.remove_edge(100);
655
656 assert_eq!(store.edge_count(), 0);
657 assert!(store.get_outgoing(1).is_empty());
658 assert!(store.get_incoming(2).is_empty());
659 }
660
661 #[test]
662 fn test_mobile_graph_store_clear() {
663 let store = MobileGraphStore::new();
664
665 store.add_node(MobileGraphNode {
666 id: 1,
667 label: "Person".to_string(),
668 properties_json: None,
669 vector: None,
670 });
671 store.add_node(MobileGraphNode {
672 id: 2,
673 label: "Person".to_string(),
674 properties_json: None,
675 vector: None,
676 });
677
678 let _ = store.add_edge(MobileGraphEdge {
679 id: 100,
680 source: 1,
681 target: 2,
682 label: "KNOWS".to_string(),
683 properties_json: None,
684 });
685
686 store.clear();
687
688 assert_eq!(store.node_count(), 0);
689 assert_eq!(store.edge_count(), 0);
690 }
691}