1use crate::error::AgentRuntimeError;
17use crate::util::recover_lock;
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
21use std::sync::{Arc, Mutex};
22
23#[derive(Debug, Clone, Copy, PartialEq)]
28struct OrdF32(f32);
29
30impl Eq for OrdF32 {}
31
32impl PartialOrd for OrdF32 {
33 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
34 Some(self.cmp(other))
35 }
36}
37
38impl Ord for OrdF32 {
39 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
40 self.0
41 .partial_cmp(&other.0)
42 .unwrap_or(std::cmp::Ordering::Greater)
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
50pub struct EntityId(pub String);
51
52impl EntityId {
53 pub fn new(id: impl Into<String>) -> Self {
61 let id = id.into();
62 if id.is_empty() {
63 debug_assert!(false, "EntityId must not be empty");
64 tracing::warn!("EntityId::new called with an empty string — entity IDs should be non-empty to avoid lookup ambiguity");
65 }
66 Self(id)
67 }
68
69 pub fn try_new(id: impl Into<String>) -> Result<Self, AgentRuntimeError> {
71 let id = id.into();
72 if id.is_empty() {
73 return Err(AgentRuntimeError::Graph(
74 "EntityId must not be empty".into(),
75 ));
76 }
77 Ok(Self(id))
78 }
79
80 pub fn as_str(&self) -> &str {
82 &self.0
83 }
84
85 pub fn is_empty(&self) -> bool {
89 self.0.is_empty()
90 }
91
92 pub fn starts_with(&self, prefix: &str) -> bool {
94 self.0.starts_with(prefix)
95 }
96}
97
98impl AsRef<str> for EntityId {
99 fn as_ref(&self) -> &str {
100 &self.0
101 }
102}
103
104impl std::fmt::Display for EntityId {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 write!(f, "{}", self.0)
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Entity {
115 pub id: EntityId,
117 pub label: String,
119 pub properties: HashMap<String, Value>,
121}
122
123impl Entity {
124 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
126 Self {
127 id: EntityId::new(id),
128 label: label.into(),
129 properties: HashMap::new(),
130 }
131 }
132
133 pub fn with_properties(
135 id: impl Into<String>,
136 label: impl Into<String>,
137 properties: HashMap<String, Value>,
138 ) -> Self {
139 Self {
140 id: EntityId::new(id),
141 label: label.into(),
142 properties,
143 }
144 }
145
146 pub fn with_property(mut self, key: impl Into<String>, value: Value) -> Self {
155 self.properties.insert(key.into(), value);
156 self
157 }
158
159 pub fn has_property(&self, key: &str) -> bool {
161 self.properties.contains_key(key)
162 }
163
164 pub fn property_value(&self, key: &str) -> Option<&serde_json::Value> {
166 self.properties.get(key)
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct Relationship {
175 pub from: EntityId,
177 pub to: EntityId,
179 pub kind: String,
181 pub weight: f32,
183}
184
185impl Relationship {
186 pub fn new(
188 from: impl Into<String>,
189 to: impl Into<String>,
190 kind: impl Into<String>,
191 weight: f32,
192 ) -> Self {
193 Self {
194 from: EntityId::new(from),
195 to: EntityId::new(to),
196 kind: kind.into(),
197 weight,
198 }
199 }
200
201 pub fn is_self_loop(&self) -> bool {
203 self.from == self.to
204 }
205
206 pub fn reversed(&self) -> Self {
210 Self {
211 from: self.to.clone(),
212 to: self.from.clone(),
213 kind: self.kind.clone(),
214 weight: self.weight,
215 }
216 }
217}
218
219#[derive(Debug, thiserror::Error)]
223pub enum MemGraphError {
224 #[error("Entity '{0}' not found")]
226 EntityNotFound(String),
227
228 #[error("Relationship '{kind}' from '{from}' to '{to}' already exists")]
230 DuplicateRelationship {
231 from: String,
233 to: String,
235 kind: String,
237 },
238
239 #[error("Graph internal error: {0}")]
241 Internal(String),
242}
243
244impl From<MemGraphError> for AgentRuntimeError {
245 fn from(e: MemGraphError) -> Self {
246 AgentRuntimeError::Graph(e.to_string())
247 }
248}
249
250#[derive(Debug, Clone)]
261pub struct GraphStore {
262 inner: Arc<Mutex<GraphInner>>,
263}
264
265#[derive(Debug)]
266struct GraphInner {
267 entities: HashMap<EntityId, Entity>,
268 relationships: Vec<Relationship>,
270 adjacency: HashMap<EntityId, Vec<Relationship>>,
274 reverse_adjacency: HashMap<EntityId, Vec<EntityId>>,
278 cycle_cache: Option<bool>,
280}
281
282impl GraphStore {
283 pub fn new() -> Self {
285 Self {
286 inner: Arc::new(Mutex::new(GraphInner {
287 entities: HashMap::new(),
288 relationships: Vec::new(),
289 adjacency: HashMap::new(),
290 reverse_adjacency: HashMap::new(),
291 cycle_cache: None,
292 })),
293 }
294 }
295
296 pub fn add_entity(&self, entity: Entity) -> Result<(), AgentRuntimeError> {
300 let mut inner = recover_lock(self.inner.lock(), "add_entity");
301 inner.cycle_cache = None;
302 inner.adjacency.entry(entity.id.clone()).or_default();
304 inner.entities.insert(entity.id.clone(), entity);
305 Ok(())
306 }
307
308 pub fn get_entity(&self, id: &EntityId) -> Result<Entity, AgentRuntimeError> {
310 let inner = recover_lock(self.inner.lock(), "get_entity");
311 inner
312 .entities
313 .get(id)
314 .cloned()
315 .ok_or_else(|| AgentRuntimeError::Graph(format!("entity '{}' not found", id.0)))
316 }
317
318 pub fn has_entity(&self, id: &EntityId) -> Result<bool, AgentRuntimeError> {
320 let inner = recover_lock(self.inner.lock(), "GraphStore::has_entity");
321 Ok(inner.entities.contains_key(id))
322 }
323
324 pub fn has_any_entities(&self) -> Result<bool, AgentRuntimeError> {
326 let inner = recover_lock(self.inner.lock(), "GraphStore::has_any_entities");
327 Ok(!inner.entities.is_empty())
328 }
329
330 pub fn entity_type_count(&self) -> Result<usize, AgentRuntimeError> {
332 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_type_count");
333 let types: std::collections::HashSet<&str> =
334 inner.entities.values().map(|e| e.label.as_str()).collect();
335 Ok(types.len())
336 }
337
338 pub fn labels(&self) -> Result<Vec<String>, AgentRuntimeError> {
340 let inner = recover_lock(self.inner.lock(), "GraphStore::labels");
341 let mut labels: Vec<String> = inner
342 .entities
343 .values()
344 .map(|e| e.label.clone())
345 .collect::<std::collections::HashSet<_>>()
346 .into_iter()
347 .collect();
348 labels.sort();
349 Ok(labels)
350 }
351
352 pub fn incoming_count_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
356 let inner = recover_lock(self.inner.lock(), "GraphStore::incoming_count_for");
357 Ok(inner.reverse_adjacency.get(id).map_or(0, |v| v.len()))
358 }
359
360 pub fn outgoing_count_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
364 let inner = recover_lock(self.inner.lock(), "GraphStore::outgoing_count_for");
365 Ok(inner.adjacency.get(id).map_or(0, |v| v.len()))
366 }
367
368 pub fn sink_count(&self) -> Result<usize, AgentRuntimeError> {
370 let inner = recover_lock(self.inner.lock(), "GraphStore::sink_count");
371 let count = inner
372 .entities
373 .keys()
374 .filter(|id| inner.adjacency.get(*id).map_or(true, |v| v.is_empty()))
375 .count();
376 Ok(count)
377 }
378
379 pub fn bidirectional_count(&self) -> Result<usize, AgentRuntimeError> {
383 let inner = recover_lock(self.inner.lock(), "GraphStore::bidirectional_count");
384 let mut count = 0usize;
385 for (from, rels) in &inner.adjacency {
386 for rel in rels {
387 let to = &rel.to;
388 if to > from {
389 if inner.adjacency.get(to).map_or(false, |v| v.iter().any(|r| &r.to == from)) {
391 count += 1;
392 }
393 }
394 }
395 }
396 Ok(count)
397 }
398
399 pub fn has_self_loops(&self) -> Result<bool, AgentRuntimeError> {
401 let inner = recover_lock(self.inner.lock(), "GraphStore::has_self_loops");
402 let found = inner.adjacency.iter().any(|(from, rels)| {
403 rels.iter().any(|r| &r.to == from)
404 });
405 Ok(found)
406 }
407
408 pub fn source_count(&self) -> Result<usize, AgentRuntimeError> {
410 let inner = recover_lock(self.inner.lock(), "GraphStore::source_count");
411 let count = inner
412 .entities
413 .keys()
414 .filter(|id| inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty()))
415 .count();
416 Ok(count)
417 }
418
419 pub fn orphan_count(&self) -> Result<usize, AgentRuntimeError> {
421 let inner = recover_lock(self.inner.lock(), "GraphStore::orphan_count");
422 let count = inner
423 .entities
424 .keys()
425 .filter(|id| inner.adjacency.get(*id).map_or(true, |v| v.is_empty()))
426 .count();
427 Ok(count)
428 }
429
430 pub fn isolated_entity_count(&self) -> Result<usize, AgentRuntimeError> {
432 let inner = recover_lock(self.inner.lock(), "GraphStore::isolated_entity_count");
433 let count = inner.entities.keys().filter(|id| {
434 inner.adjacency.get(*id).map_or(true, |v| v.is_empty())
435 && inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty())
436 }).count();
437 Ok(count)
438 }
439
440 pub fn avg_relationship_weight(&self) -> Result<f64, AgentRuntimeError> {
444 let inner = recover_lock(self.inner.lock(), "GraphStore::avg_relationship_weight");
445 if inner.relationships.is_empty() {
446 return Ok(0.0);
447 }
448 let total: f32 = inner.relationships.iter().map(|r| r.weight).sum();
449 Ok(total as f64 / inner.relationships.len() as f64)
450 }
451
452 pub fn total_in_degree(&self) -> Result<usize, AgentRuntimeError> {
457 let inner = recover_lock(self.inner.lock(), "GraphStore::total_in_degree");
458 Ok(inner.relationships.len())
459 }
460
461 pub fn relationship_count_between(
465 &self,
466 from: &EntityId,
467 to: &EntityId,
468 ) -> Result<usize, AgentRuntimeError> {
469 let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_count_between");
470 Ok(inner
471 .adjacency
472 .get(from)
473 .map_or(0, |rels| rels.iter().filter(|r| &r.to == to).count()))
474 }
475
476 pub fn edges_from(&self, id: &EntityId) -> Result<Vec<Relationship>, AgentRuntimeError> {
480 let inner = recover_lock(self.inner.lock(), "GraphStore::edges_from");
481 Ok(inner
482 .adjacency
483 .get(id)
484 .cloned()
485 .unwrap_or_default())
486 }
487
488 pub fn neighbors_of(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
492 let inner = recover_lock(self.inner.lock(), "GraphStore::neighbors_of");
493 let mut targets: Vec<EntityId> = inner
494 .adjacency
495 .get(id)
496 .map_or_else(Vec::new, |rels| rels.iter().map(|r| r.to.clone()).collect());
497 targets.sort_unstable();
498 targets.dedup();
499 Ok(targets)
500 }
501
502 pub fn add_relationship(&self, rel: Relationship) -> Result<(), AgentRuntimeError> {
506 let mut inner = recover_lock(self.inner.lock(), "add_relationship");
507
508 if !inner.entities.contains_key(&rel.from) {
509 return Err(AgentRuntimeError::Graph(format!(
510 "source entity '{}' not found",
511 rel.from.0
512 )));
513 }
514 if !inner.entities.contains_key(&rel.to) {
515 return Err(AgentRuntimeError::Graph(format!(
516 "target entity '{}' not found",
517 rel.to.0
518 )));
519 }
520
521 let duplicate = inner
525 .relationships
526 .iter()
527 .any(|r| r.from == rel.from && r.to == rel.to && r.kind == rel.kind);
528 if duplicate {
529 return Err(AgentRuntimeError::Graph(
530 MemGraphError::DuplicateRelationship {
531 from: rel.from.0.clone(),
532 to: rel.to.0.clone(),
533 kind: rel.kind.clone(),
534 }
535 .to_string(),
536 ));
537 }
538
539 inner.cycle_cache = None;
540 inner
542 .adjacency
543 .entry(rel.from.clone())
544 .or_default()
545 .push(rel.clone());
546 inner
548 .reverse_adjacency
549 .entry(rel.to.clone())
550 .or_default()
551 .push(rel.from.clone());
552 inner.relationships.push(rel);
553 Ok(())
554 }
555
556 pub fn remove_relationship(
560 &self,
561 from: &EntityId,
562 to: &EntityId,
563 kind: &str,
564 ) -> Result<(), AgentRuntimeError> {
565 let mut inner = recover_lock(self.inner.lock(), "remove_relationship");
566
567 let before = inner.relationships.len();
568 inner
569 .relationships
570 .retain(|r| !(&r.from == from && &r.to == to && r.kind == kind));
571 if inner.relationships.len() == before {
572 return Err(AgentRuntimeError::Graph(format!(
573 "relationship '{kind}' from '{}' to '{}' not found",
574 from.0, to.0
575 )));
576 }
577
578 if let Some(adj) = inner.adjacency.get_mut(from) {
580 adj.retain(|r| !(&r.to == to && r.kind == kind));
581 }
582 if let Some(rev) = inner.reverse_adjacency.get_mut(to) {
584 rev.retain(|src| src != from);
585 }
586
587 inner.cycle_cache = None;
588 Ok(())
589 }
590
591 pub fn remove_entity(&self, id: &EntityId) -> Result<(), AgentRuntimeError> {
593 let mut inner = recover_lock(self.inner.lock(), "remove_entity");
594
595 if inner.entities.remove(id).is_none() {
596 return Err(AgentRuntimeError::Graph(format!(
597 "entity '{}' not found",
598 id.0
599 )));
600 }
601 inner.cycle_cache = None;
602 inner.relationships.retain(|r| &r.from != id && &r.to != id);
603 inner.adjacency.remove(id);
605 for adj in inner.adjacency.values_mut() {
606 adj.retain(|r| &r.to != id);
607 }
608 inner.reverse_adjacency.remove(id);
610 for rev in inner.reverse_adjacency.values_mut() {
611 rev.retain(|src| src != id);
612 }
613 Ok(())
614 }
615
616 fn neighbours(adjacency: &HashMap<EntityId, Vec<Relationship>>, id: &EntityId) -> Vec<EntityId> {
620 adjacency
621 .get(id)
622 .map(|rels| rels.iter().map(|r| r.to.clone()).collect())
623 .unwrap_or_default()
624 }
625
626 #[tracing::instrument(skip(self))]
630 pub fn bfs(&self, start: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
631 let inner = recover_lock(self.inner.lock(), "bfs");
632
633 if !inner.entities.contains_key(start) {
634 return Err(AgentRuntimeError::Graph(format!(
635 "start entity '{}' not found",
636 start.0
637 )));
638 }
639
640 let mut visited: HashSet<EntityId> = HashSet::new();
641 let mut queue: VecDeque<EntityId> = VecDeque::new();
642 let mut result: Vec<EntityId> = Vec::new();
643
644 visited.insert(start.clone());
645 queue.push_back(start.clone());
646
647 while let Some(current) = queue.pop_front() {
648 let neighbours: Vec<EntityId> = Self::neighbours(&inner.adjacency, ¤t);
649 for neighbour in neighbours {
650 if visited.insert(neighbour.clone()) {
651 result.push(neighbour.clone());
652 queue.push_back(neighbour);
653 }
654 }
655 }
656
657 tracing::debug!("BFS visited {} nodes", result.len());
658 Ok(result)
659 }
660
661 #[tracing::instrument(skip(self))]
665 pub fn dfs(&self, start: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
666 let inner = recover_lock(self.inner.lock(), "dfs");
667
668 if !inner.entities.contains_key(start) {
669 return Err(AgentRuntimeError::Graph(format!(
670 "start entity '{}' not found",
671 start.0
672 )));
673 }
674
675 let mut visited: HashSet<EntityId> = HashSet::new();
676 let mut stack: Vec<EntityId> = Vec::new();
677 let mut result: Vec<EntityId> = Vec::new();
678
679 visited.insert(start.clone());
680 stack.push(start.clone());
681
682 while let Some(current) = stack.pop() {
683 let neighbours: Vec<EntityId> = Self::neighbours(&inner.adjacency, ¤t);
684 for neighbour in neighbours {
685 if visited.insert(neighbour.clone()) {
686 result.push(neighbour.clone());
687 stack.push(neighbour);
688 }
689 }
690 }
691
692 tracing::debug!("DFS visited {} nodes", result.len());
693 Ok(result)
694 }
695
696 #[tracing::instrument(skip(self))]
702 pub fn shortest_path(
703 &self,
704 from: &EntityId,
705 to: &EntityId,
706 ) -> Result<Option<Vec<EntityId>>, AgentRuntimeError> {
707 let inner = recover_lock(self.inner.lock(), "shortest_path");
708
709 if !inner.entities.contains_key(from) {
710 return Err(AgentRuntimeError::Graph(format!(
711 "source entity '{}' not found",
712 from.0
713 )));
714 }
715 if !inner.entities.contains_key(to) {
716 return Err(AgentRuntimeError::Graph(format!(
717 "target entity '{}' not found",
718 to.0
719 )));
720 }
721
722 if from == to {
723 return Ok(Some(vec![from.clone()]));
724 }
725
726 let mut visited: HashSet<EntityId> = HashSet::new();
728 let mut prev: HashMap<EntityId, EntityId> = HashMap::new();
729 let mut queue: VecDeque<EntityId> = VecDeque::new();
730
731 visited.insert(from.clone());
732 queue.push_back(from.clone());
733
734 while let Some(current) = queue.pop_front() {
735 for neighbour in Self::neighbours(&inner.adjacency, ¤t) {
736 if &neighbour == to {
737 let mut path = vec![neighbour, current.clone()];
739 let mut node = current;
740 while let Some(p) = prev.get(&node) {
741 path.push(p.clone());
742 node = p.clone();
743 }
744 path.reverse();
745 return Ok(Some(path));
746 }
747 if visited.insert(neighbour.clone()) {
748 prev.insert(neighbour.clone(), current.clone());
749 queue.push_back(neighbour);
750 }
751 }
752 }
753
754 Ok(None)
755 }
756
757 pub fn shortest_path_weighted(
767 &self,
768 from: &EntityId,
769 to: &EntityId,
770 ) -> Result<Option<(Vec<EntityId>, f32)>, AgentRuntimeError> {
771 let inner = recover_lock(self.inner.lock(), "shortest_path_weighted");
772
773 if !inner.entities.contains_key(from) {
774 return Err(AgentRuntimeError::Graph(format!(
775 "source entity '{}' not found",
776 from.0
777 )));
778 }
779 if !inner.entities.contains_key(to) {
780 return Err(AgentRuntimeError::Graph(format!(
781 "target entity '{}' not found",
782 to.0
783 )));
784 }
785
786 for rel in &inner.relationships {
788 if rel.weight < 0.0 {
789 return Err(AgentRuntimeError::Graph(format!(
790 "negative weight {:.4} on edge '{}' -> '{}'",
791 rel.weight, rel.from.0, rel.to.0
792 )));
793 }
794 }
795
796 if from == to {
797 return Ok(Some((vec![from.clone()], 0.0)));
798 }
799
800 let mut dist: HashMap<EntityId, f32> = HashMap::new();
803 let mut prev: HashMap<EntityId, EntityId> = HashMap::new();
804 let mut heap: BinaryHeap<(OrdF32, EntityId)> = BinaryHeap::new();
806
807 dist.insert(from.clone(), 0.0);
808 heap.push((OrdF32(-0.0), from.clone()));
809
810 while let Some((OrdF32(neg_cost), current)) = heap.pop() {
811 let cost = -neg_cost;
812
813 if let Some(&best) = dist.get(¤t) {
815 if cost > best {
816 continue;
817 }
818 }
819
820 if ¤t == to {
821 let mut path = vec![to.clone()];
823 let mut node = to.clone();
824 while let Some(p) = prev.get(&node) {
825 path.push(p.clone());
826 node = p.clone();
827 }
828 path.reverse();
829 return Ok(Some((path, cost)));
830 }
831
832 if let Some(rels) = inner.adjacency.get(¤t) {
835 for rel in rels {
836 let next_cost = cost + rel.weight;
837 let entry = dist.entry(rel.to.clone()).or_insert(f32::INFINITY);
838 if next_cost < *entry {
839 *entry = next_cost;
840 prev.insert(rel.to.clone(), current.clone());
841 heap.push((OrdF32(-next_cost), rel.to.clone()));
842 }
843 }
844 }
845 }
846
847 Ok(None)
848 }
849
850 fn bfs_into_set(inner: &GraphInner, start: &EntityId) -> HashSet<EntityId> {
855 let mut visited: HashSet<EntityId> = HashSet::new();
856 let mut queue: VecDeque<EntityId> = VecDeque::new();
857 visited.insert(start.clone());
858 queue.push_back(start.clone());
859 while let Some(current) = queue.pop_front() {
860 for neighbour in Self::neighbours(&inner.adjacency, ¤t) {
861 if visited.insert(neighbour.clone()) {
862 queue.push_back(neighbour);
863 }
864 }
865 }
866 visited
867 }
868
869 pub fn transitive_closure(
878 &self,
879 start: &EntityId,
880 ) -> Result<HashSet<EntityId>, AgentRuntimeError> {
881 let inner = recover_lock(self.inner.lock(), "transitive_closure");
882 if !inner.entities.contains_key(start) {
883 return Err(AgentRuntimeError::Graph(format!(
884 "start entity '{}' not found",
885 start.0
886 )));
887 }
888 Ok(Self::bfs_into_set(&inner, start))
889 }
890
891 pub fn entity_count(&self) -> Result<usize, AgentRuntimeError> {
893 let inner = recover_lock(self.inner.lock(), "entity_count");
894 Ok(inner.entities.len())
895 }
896
897 pub fn node_count(&self) -> Result<usize, AgentRuntimeError> {
903 self.entity_count()
904 }
905
906 pub fn relationship_count(&self) -> Result<usize, AgentRuntimeError> {
908 let inner = recover_lock(self.inner.lock(), "relationship_count");
909 Ok(inner.relationships.len())
910 }
911
912 pub fn average_out_degree(&self) -> Result<f64, AgentRuntimeError> {
917 let inner = recover_lock(self.inner.lock(), "average_out_degree");
918 let n = inner.entities.len();
919 if n == 0 {
920 return Ok(0.0);
921 }
922 Ok(inner.relationships.len() as f64 / n as f64)
923 }
924
925 pub fn in_degree_for(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
929 let inner = recover_lock(self.inner.lock(), "in_degree_for");
930 Ok(inner
931 .reverse_adjacency
932 .get(entity_id)
933 .map(|v| v.len())
934 .unwrap_or(0))
935 }
936
937 pub fn out_degree_for(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
941 let inner = recover_lock(self.inner.lock(), "out_degree_for");
942 Ok(inner
943 .adjacency
944 .get(entity_id)
945 .map(|v| v.len())
946 .unwrap_or(0))
947 }
948
949 pub fn predecessors(&self, entity_id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
955 let inner = recover_lock(self.inner.lock(), "predecessors");
956 let ids = inner
957 .reverse_adjacency
958 .get(entity_id)
959 .cloned()
960 .unwrap_or_default();
961 Ok(ids
962 .iter()
963 .filter_map(|id| inner.entities.get(id).cloned())
964 .collect())
965 }
966
967 pub fn is_source(&self, entity_id: &EntityId) -> Result<bool, AgentRuntimeError> {
972 Ok(self.in_degree_for(entity_id)? == 0)
973 }
974
975 pub fn successors(&self, entity_id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
980 let inner = recover_lock(self.inner.lock(), "successors");
981 let rels = inner.adjacency.get(entity_id).cloned().unwrap_or_default();
982 Ok(rels
983 .iter()
984 .filter_map(|r| inner.entities.get(&r.to).cloned())
985 .collect())
986 }
987
988 pub fn is_sink(&self, entity_id: &EntityId) -> Result<bool, AgentRuntimeError> {
993 Ok(self.out_degree_for(entity_id)? == 0)
994 }
995
996 pub fn reachable_from(
1001 &self,
1002 start: &EntityId,
1003 ) -> Result<std::collections::HashSet<EntityId>, AgentRuntimeError> {
1004 let inner = recover_lock(self.inner.lock(), "reachable_from");
1005 let mut visited = std::collections::HashSet::new();
1006 let mut queue = std::collections::VecDeque::new();
1007 if let Some(rels) = inner.adjacency.get(start) {
1008 for r in rels {
1009 if visited.insert(r.to.clone()) {
1010 queue.push_back(r.to.clone());
1011 }
1012 }
1013 }
1014 while let Some(current) = queue.pop_front() {
1015 if let Some(rels) = inner.adjacency.get(¤t) {
1016 for r in rels {
1017 if visited.insert(r.to.clone()) {
1018 queue.push_back(r.to.clone());
1019 }
1020 }
1021 }
1022 }
1023 Ok(visited)
1024 }
1025
1026 pub fn contains_cycle(&self) -> Result<bool, AgentRuntimeError> {
1031 let inner = recover_lock(self.inner.lock(), "contains_cycle");
1032 let mut color: HashMap<&EntityId, u8> = HashMap::new();
1033
1034 for start in inner.entities.keys() {
1035 if color.get(start).copied().unwrap_or(0) != 0 {
1036 continue;
1037 }
1038 let mut stack: Vec<(&EntityId, usize)> = vec![(start, 0)];
1039 color.insert(start, 1);
1040 while let Some((node, idx)) = stack.last_mut() {
1041 let neighbors = inner.adjacency.get(node).map(|v| v.as_slice()).unwrap_or(&[]);
1042 if *idx < neighbors.len() {
1043 let neighbor = &neighbors[*idx].to;
1044 *idx += 1;
1045 match color.get(neighbor).copied().unwrap_or(0) {
1046 1 => return Ok(true),
1047 0 => {
1048 color.insert(neighbor, 1);
1049 stack.push((neighbor, 0));
1050 }
1051 _ => {}
1052 }
1053 } else {
1054 color.insert(*node, 2);
1055 stack.pop();
1056 }
1057 }
1058 }
1059 Ok(false)
1060 }
1061
1062 pub fn edge_count(&self) -> Result<usize, AgentRuntimeError> {
1068 self.relationship_count()
1069 }
1070
1071 pub fn is_acyclic(&self) -> Result<bool, AgentRuntimeError> {
1075 Ok(!self.contains_cycle()?)
1076 }
1077
1078 pub fn avg_out_degree(&self) -> Result<f64, AgentRuntimeError> {
1082 let inner = recover_lock(self.inner.lock(), "GraphStore::avg_out_degree");
1083 let n = inner.entities.len();
1084 if n == 0 {
1085 return Ok(0.0);
1086 }
1087 let total: usize = inner.entities.keys().map(|id| {
1088 inner.adjacency.get(id).map_or(0, |v| v.len())
1089 }).sum();
1090 Ok(total as f64 / n as f64)
1091 }
1092
1093 pub fn avg_in_degree(&self) -> Result<f64, AgentRuntimeError> {
1097 let inner = recover_lock(self.inner.lock(), "GraphStore::avg_in_degree");
1098 let n = inner.entities.len();
1099 if n == 0 {
1100 return Ok(0.0);
1101 }
1102 let total: usize = inner.entities.keys().map(|id| {
1103 inner.reverse_adjacency.get(id).map_or(0, |v| v.len())
1104 }).sum();
1105 Ok(total as f64 / n as f64)
1106 }
1107
1108 pub fn max_out_degree(&self) -> Result<usize, AgentRuntimeError> {
1110 let inner = recover_lock(self.inner.lock(), "max_out_degree");
1111 Ok(inner
1112 .adjacency
1113 .values()
1114 .map(|v| v.len())
1115 .max()
1116 .unwrap_or(0))
1117 }
1118
1119 pub fn max_in_degree(&self) -> Result<usize, AgentRuntimeError> {
1121 let inner = recover_lock(self.inner.lock(), "max_in_degree");
1122 Ok(inner
1123 .reverse_adjacency
1124 .values()
1125 .map(|v| v.len())
1126 .max()
1127 .unwrap_or(0))
1128 }
1129
1130 pub fn weight_stats(&self) -> Result<Option<(f64, f64, f64)>, AgentRuntimeError> {
1134 let inner = recover_lock(self.inner.lock(), "weight_stats");
1135 let weights: Vec<f64> = inner
1136 .adjacency
1137 .values()
1138 .flat_map(|rels| rels.iter())
1139 .map(|r| r.weight as f64)
1140 .collect();
1141 if weights.is_empty() {
1142 return Ok(None);
1143 }
1144 let min = weights.iter().cloned().fold(f64::INFINITY, f64::min);
1145 let max = weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1146 let mean = weights.iter().sum::<f64>() / weights.len() as f64;
1147 Ok(Some((min, max, mean)))
1148 }
1149
1150 pub fn isolated_nodes(&self) -> Result<HashSet<EntityId>, AgentRuntimeError> {
1155 let inner = recover_lock(self.inner.lock(), "GraphStore::isolated_nodes");
1156 let mut result = HashSet::new();
1157 for id in inner.entities.keys() {
1158 let has_outbound = inner
1159 .adjacency
1160 .get(id)
1161 .map_or(false, |v| !v.is_empty());
1162 let has_inbound = inner
1163 .reverse_adjacency
1164 .get(id)
1165 .map_or(false, |v| !v.is_empty());
1166 if !has_outbound && !has_inbound {
1167 result.insert(id.clone());
1168 }
1169 }
1170 Ok(result)
1171 }
1172
1173 pub fn sum_edge_weights(&self) -> Result<f64, AgentRuntimeError> {
1177 let inner = recover_lock(self.inner.lock(), "sum_edge_weights");
1178 Ok(inner
1179 .adjacency
1180 .values()
1181 .flat_map(|rels| rels.iter())
1182 .map(|r| r.weight as f64)
1183 .sum())
1184 }
1185
1186 pub fn entity_ids(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
1192 let inner = recover_lock(self.inner.lock(), "entity_ids");
1193 Ok(inner.entities.keys().cloned().collect())
1194 }
1195
1196 pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
1198 Ok(self.entity_count()? == 0)
1199 }
1200
1201 pub fn clear(&self) -> Result<(), AgentRuntimeError> {
1205 let mut inner = recover_lock(self.inner.lock(), "GraphStore::clear");
1206 inner.entities.clear();
1207 inner.relationships.clear();
1208 inner.adjacency.clear();
1209 inner.reverse_adjacency.clear();
1210 inner.cycle_cache = None;
1211 Ok(())
1212 }
1213
1214 pub fn entity_count_by_label(&self, label: &str) -> Result<usize, AgentRuntimeError> {
1216 let inner = recover_lock(self.inner.lock(), "entity_count_by_label");
1217 Ok(inner.entities.values().filter(|e| e.label == label).count())
1218 }
1219
1220 pub fn graph_density(&self) -> Result<f64, AgentRuntimeError> {
1225 let inner = recover_lock(self.inner.lock(), "graph_density");
1226 let v = inner.entities.len();
1227 if v < 2 {
1228 return Ok(0.0);
1229 }
1230 let e = inner.relationships.len() as f64;
1231 Ok(e / (v as f64 * (v - 1) as f64))
1232 }
1233
1234 pub fn entity_labels(&self) -> Result<Vec<String>, AgentRuntimeError> {
1236 let inner = recover_lock(self.inner.lock(), "entity_labels");
1237 let mut labels: Vec<String> = inner
1238 .entities
1239 .values()
1240 .map(|e| e.label.clone())
1241 .collect::<std::collections::HashSet<_>>()
1242 .into_iter()
1243 .collect();
1244 labels.sort_unstable();
1245 Ok(labels)
1246 }
1247
1248 pub fn relationship_kinds(&self) -> Result<Vec<String>, AgentRuntimeError> {
1250 let inner = recover_lock(self.inner.lock(), "relationship_kinds");
1251 let mut kinds: Vec<String> = inner
1252 .relationships
1253 .iter()
1254 .map(|r| r.kind.clone())
1255 .collect::<std::collections::HashSet<_>>()
1256 .into_iter()
1257 .collect();
1258 kinds.sort_unstable();
1259 Ok(kinds)
1260 }
1261
1262 pub fn relationship_kind_count(&self) -> Result<usize, AgentRuntimeError> {
1264 let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_kind_count");
1265 let count = inner
1266 .relationships
1267 .iter()
1268 .map(|r| r.kind.as_str())
1269 .collect::<std::collections::HashSet<_>>()
1270 .len();
1271 Ok(count)
1272 }
1273
1274 pub fn entities_with_self_loops(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
1278 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_self_loops");
1279 let mut ids: Vec<EntityId> = inner
1280 .adjacency
1281 .iter()
1282 .filter(|(from, rels)| rels.iter().any(|r| &r.to == *from))
1283 .map(|(id, _)| id.clone())
1284 .collect();
1285 ids.sort_unstable();
1286 Ok(ids)
1287 }
1288
1289 pub fn update_entity_label(
1293 &self,
1294 id: &EntityId,
1295 new_label: impl Into<String>,
1296 ) -> Result<bool, AgentRuntimeError> {
1297 let mut inner = recover_lock(self.inner.lock(), "update_entity_label");
1298 if let Some(entity) = inner.entities.get_mut(id) {
1299 entity.label = new_label.into();
1300 inner.cycle_cache = None;
1301 Ok(true)
1302 } else {
1303 Ok(false)
1304 }
1305 }
1306
1307 pub fn degree_centrality(&self) -> Result<HashMap<EntityId, f32>, AgentRuntimeError> {
1310 let inner = recover_lock(self.inner.lock(), "degree_centrality");
1311 let n = inner.entities.len();
1312
1313 let denom = if n <= 1 { 1.0 } else { (n - 1) as f32 };
1317 let mut result = HashMap::new();
1318 for id in inner.entities.keys() {
1319 let od = inner.adjacency.get(id).map_or(0, |v| v.len());
1320 let id_ = inner.reverse_adjacency.get(id).map_or(0, |v| v.len());
1321 let centrality = if n <= 1 {
1322 0.0
1323 } else {
1324 (od + id_) as f32 / denom
1325 };
1326 result.insert(id.clone(), centrality);
1327 }
1328
1329 Ok(result)
1330 }
1331
1332 pub fn betweenness_centrality(&self) -> Result<HashMap<EntityId, f32>, AgentRuntimeError> {
1338 let inner = recover_lock(self.inner.lock(), "betweenness_centrality");
1339 let n = inner.entities.len();
1340 let nodes: Vec<EntityId> = inner.entities.keys().cloned().collect();
1341
1342 let mut centrality: HashMap<EntityId, f32> =
1343 nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
1344
1345 let mut stack: Vec<EntityId> = Vec::with_capacity(n);
1348 let mut predecessors: HashMap<EntityId, Vec<EntityId>> =
1349 nodes.iter().map(|id| (id.clone(), vec![])).collect();
1350 let mut sigma: HashMap<EntityId, f32> =
1351 nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
1352 let mut dist: HashMap<EntityId, i64> =
1353 nodes.iter().map(|id| (id.clone(), -1i64)).collect();
1354 let mut delta: HashMap<EntityId, f32> =
1355 nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
1356 let mut queue: VecDeque<EntityId> = VecDeque::with_capacity(n);
1357
1358 for source in &nodes {
1359 stack.clear();
1362 for v in predecessors.values_mut() {
1363 v.clear();
1364 }
1365 for v in sigma.values_mut() {
1366 *v = 0.0;
1367 }
1368 for v in dist.values_mut() {
1369 *v = -1;
1370 }
1371 for v in delta.values_mut() {
1372 *v = 0.0;
1373 }
1374 queue.clear();
1375
1376 *sigma.entry(source.clone()).or_insert(0.0) = 1.0;
1377 *dist.entry(source.clone()).or_insert(-1) = 0;
1378 queue.push_back(source.clone());
1379
1380 while let Some(v) = queue.pop_front() {
1381 stack.push(v.clone());
1382 let d_v = *dist.get(&v).unwrap_or(&0);
1383 let sigma_v = *sigma.get(&v).unwrap_or(&0.0);
1384 if let Some(rels) = inner.adjacency.get(&v) {
1386 for rel in rels {
1387 let w = &rel.to;
1388 let d_w = dist.get(w).copied().unwrap_or(-1);
1389 if d_w < 0 {
1390 queue.push_back(w.clone());
1391 *dist.entry(w.clone()).or_insert(-1) = d_v + 1;
1392 }
1393 if dist.get(w).copied().unwrap_or(-1) == d_v + 1 {
1394 *sigma.entry(w.clone()).or_insert(0.0) += sigma_v;
1395 predecessors.entry(w.clone()).or_default().push(v.clone());
1396 }
1397 }
1398 }
1399 }
1400
1401 while let Some(w) = stack.pop() {
1404 let delta_w = *delta.get(&w).unwrap_or(&0.0);
1405 let sigma_w = *sigma.get(&w).unwrap_or(&1.0);
1406 for v in predecessors
1408 .get(&w)
1409 .map(|ps| ps.as_slice())
1410 .unwrap_or_default()
1411 {
1412 let sigma_v = *sigma.get(v).unwrap_or(&1.0);
1413 let contribution = (sigma_v / sigma_w) * (1.0 + delta_w);
1414 *delta.entry(v.clone()).or_insert(0.0) += contribution;
1415 }
1416 if &w != source {
1417 *centrality.entry(w.clone()).or_insert(0.0) += delta_w;
1418 }
1419 }
1420 }
1421
1422 if n > 2 {
1424 let norm = 2.0 / (((n - 1) * (n - 2)) as f32);
1425 for v in centrality.values_mut() {
1426 *v *= norm;
1427 }
1428 } else {
1429 for v in centrality.values_mut() {
1430 *v = 0.0;
1431 }
1432 }
1433
1434 Ok(centrality)
1435 }
1436
1437 pub fn label_propagation_communities(
1442 &self,
1443 max_iterations: usize,
1444 ) -> Result<HashMap<EntityId, usize>, AgentRuntimeError> {
1445 let inner = recover_lock(self.inner.lock(), "label_propagation_communities");
1446 let nodes: Vec<EntityId> = inner.entities.keys().cloned().collect();
1447
1448 let mut reverse_adj: HashMap<EntityId, Vec<EntityId>> =
1452 nodes.iter().map(|id| (id.clone(), vec![])).collect();
1453 for rel in &inner.relationships {
1454 reverse_adj
1455 .entry(rel.to.clone())
1456 .or_default()
1457 .push(rel.from.clone());
1458 }
1459
1460 let mut labels: HashMap<EntityId, usize> = nodes
1462 .iter()
1463 .enumerate()
1464 .map(|(i, id)| (id.clone(), i))
1465 .collect();
1466
1467 let mut freq: HashMap<usize, usize> = HashMap::new();
1470
1471 for _ in 0..max_iterations {
1472 let mut changed = false;
1473 for node in &nodes {
1475 let out_labels = inner
1477 .adjacency
1478 .get(node)
1479 .map(|rels| {
1480 rels.iter()
1481 .map(|r| labels.get(&r.to).copied().unwrap_or(0))
1482 })
1483 .into_iter()
1484 .flatten();
1485 let in_labels = reverse_adj
1486 .get(node)
1487 .map(|froms| froms.iter().map(|f| labels.get(f).copied().unwrap_or(0)))
1488 .into_iter()
1489 .flatten();
1490
1491 freq.clear();
1492 for lbl in out_labels.chain(in_labels) {
1493 *freq.entry(lbl).or_insert(0) += 1;
1494 }
1495
1496 if freq.is_empty() {
1497 continue;
1498 }
1499
1500 let best = freq
1502 .iter()
1503 .max_by_key(|&(_, count)| count)
1504 .map(|(&lbl, _)| lbl);
1505
1506 if let Some(new_label) = best {
1507 let current = labels.entry(node.clone()).or_insert(0);
1508 if *current != new_label {
1509 *current = new_label;
1510 changed = true;
1511 }
1512 }
1513 }
1514
1515 if !changed {
1516 break;
1517 }
1518 }
1519
1520 Ok(labels)
1521 }
1522
1523 pub fn detect_cycles(&self) -> Result<bool, AgentRuntimeError> {
1533 let mut inner = recover_lock(self.inner.lock(), "detect_cycles");
1534
1535 if let Some(cached) = inner.cycle_cache {
1536 return Ok(cached);
1537 }
1538
1539 let mut color: HashMap<&EntityId, u8> =
1541 inner.entities.keys().map(|id| (id, 0u8)).collect();
1542
1543 let has_cycle = 'outer: {
1544 for start in inner.entities.keys() {
1545 if *color.get(start).unwrap_or(&0) != 0 {
1546 continue;
1547 }
1548
1549 let mut stack: Vec<(&EntityId, usize)> = vec![(start, 0)];
1551 *color.entry(start).or_insert(0) = 1;
1552
1553 while let Some((node, idx)) = stack.last_mut() {
1554 let rels = inner
1557 .adjacency
1558 .get(*node)
1559 .map(|v| v.as_slice())
1560 .unwrap_or(&[]);
1561
1562 if *idx < rels.len() {
1563 let next = &rels[*idx].to;
1564 *idx += 1;
1565 match color.get(next).copied().unwrap_or(0) {
1566 1 => break 'outer true, 0 => {
1568 *color.entry(next).or_insert(0) = 1;
1569 stack.push((next, 0));
1570 }
1571 _ => {} }
1573 } else {
1574 *color.entry(*node).or_insert(0) = 2;
1576 stack.pop();
1577 }
1578 }
1579 }
1580 false
1581 };
1582
1583 inner.cycle_cache = Some(has_cycle);
1584 Ok(has_cycle)
1585 }
1586
1587 pub fn topological_sort(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
1595 let inner = recover_lock(self.inner.lock(), "topological_sort");
1596
1597 let mut color: HashMap<&EntityId, u8> =
1599 inner.entities.keys().map(|id| (id, 0u8)).collect();
1600 let mut result: Vec<EntityId> = Vec::with_capacity(inner.entities.len());
1601
1602 for start in inner.entities.keys() {
1603 if *color.get(start).unwrap_or(&0) != 0 {
1604 continue;
1605 }
1606 let mut stack: Vec<(&EntityId, usize, bool)> = vec![(start, 0, false)];
1608 *color.entry(start).or_insert(0) = 1;
1609
1610 while let Some((node, idx, pushed)) = stack.last_mut() {
1611 let rels = inner.adjacency.get(*node).map(|v| v.as_slice()).unwrap_or(&[]);
1612 if *idx < rels.len() {
1613 let next = &rels[*idx].to;
1614 *idx += 1;
1615 match color.get(next).copied().unwrap_or(0) {
1616 1 => return Err(AgentRuntimeError::Graph(
1617 "topological_sort: graph contains a cycle".into(),
1618 )),
1619 0 => {
1620 *color.entry(next).or_insert(0) = 1;
1621 stack.push((next, 0, false));
1622 }
1623 _ => {} }
1625 } else {
1626 if !*pushed {
1627 result.push((*node).clone());
1628 *pushed = true;
1629 }
1630 *color.entry(*node).or_insert(0) = 2;
1631 stack.pop();
1632 }
1633 }
1634 }
1635
1636 result.reverse();
1637 Ok(result)
1638 }
1639
1640 pub fn update_entity_property(
1644 &self,
1645 id: &EntityId,
1646 key: impl Into<String>,
1647 value: serde_json::Value,
1648 ) -> Result<bool, AgentRuntimeError> {
1649 let mut inner = recover_lock(self.inner.lock(), "update_entity_property");
1650 if let Some(entity) = inner.entities.get_mut(id) {
1651 entity.properties.insert(key.into(), value);
1652 Ok(true)
1653 } else {
1654 Ok(false)
1655 }
1656 }
1657
1658 pub fn is_dag(&self) -> Result<bool, AgentRuntimeError> {
1663 Ok(!self.detect_cycles()?)
1664 }
1665
1666 pub fn connected_components(&self) -> Result<usize, AgentRuntimeError> {
1671 let inner = recover_lock(self.inner.lock(), "connected_components");
1672 let ids: Vec<&EntityId> = inner.entities.keys().collect();
1673 if ids.is_empty() {
1674 return Ok(0);
1675 }
1676
1677 let idx: HashMap<&EntityId, usize> =
1679 ids.iter().enumerate().map(|(i, id)| (*id, i)).collect();
1680 let mut parent: Vec<usize> = (0..ids.len()).collect();
1681
1682 fn find(parent: &mut Vec<usize>, x: usize) -> usize {
1683 if parent[x] != x {
1684 parent[x] = find(parent, parent[x]);
1685 }
1686 parent[x]
1687 }
1688
1689 fn union(parent: &mut Vec<usize>, a: usize, b: usize) {
1690 let ra = find(parent, a);
1691 let rb = find(parent, b);
1692 if ra != rb {
1693 parent[ra] = rb;
1694 }
1695 }
1696
1697 for rel in &inner.relationships {
1698 if let (Some(&a), Some(&b)) = (idx.get(&rel.from), idx.get(&rel.to)) {
1699 union(&mut parent, a, b);
1700 }
1701 }
1702
1703 let components = ids
1704 .iter()
1705 .enumerate()
1706 .filter(|(i, _)| find(&mut parent, *i) == *i)
1707 .count();
1708 Ok(components)
1709 }
1710
1711 pub fn weakly_connected(&self) -> Result<bool, AgentRuntimeError> {
1719 Ok(self.connected_components()? <= 1)
1720 }
1721
1722 pub fn sink_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1727 let inner = recover_lock(self.inner.lock(), "sink_nodes");
1728 Ok(inner
1730 .entities
1731 .values()
1732 .filter(|e| {
1733 inner
1734 .adjacency
1735 .get(&e.id)
1736 .map_or(true, |v| v.is_empty())
1737 })
1738 .cloned()
1739 .collect())
1740 }
1741
1742 pub fn source_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1747 let inner = recover_lock(self.inner.lock(), "source_nodes");
1748 Ok(inner
1750 .entities
1751 .values()
1752 .filter(|e| {
1753 inner
1754 .reverse_adjacency
1755 .get(&e.id)
1756 .map_or(true, |v| v.is_empty())
1757 })
1758 .cloned()
1759 .collect())
1760 }
1761
1762 pub fn isolates(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1764 let inner = recover_lock(self.inner.lock(), "isolates");
1765 Ok(inner
1768 .entities
1769 .values()
1770 .filter(|e| {
1771 inner
1772 .adjacency
1773 .get(&e.id)
1774 .map_or(true, |v| v.is_empty())
1775 && inner
1776 .reverse_adjacency
1777 .get(&e.id)
1778 .map_or(true, |v| v.is_empty())
1779 })
1780 .cloned()
1781 .collect())
1782 }
1783
1784 pub fn out_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
1788 let inner = recover_lock(self.inner.lock(), "out_degree");
1789 Ok(inner.adjacency.get(id).map_or(0, |rels| rels.len()))
1790 }
1791
1792 pub fn in_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
1796 let inner = recover_lock(self.inner.lock(), "in_degree");
1797 Ok(inner
1798 .reverse_adjacency
1799 .get(id)
1800 .map_or(0, |srcs| srcs.len()))
1801 }
1802
1803 pub fn path_exists(&self, from: &str, to: &str) -> Result<bool, AgentRuntimeError> {
1808 let from_id = EntityId::new(from);
1809 let to_id = EntityId::new(to);
1810 match self.shortest_path(&from_id, &to_id) {
1811 Ok(Some(_)) => Ok(true),
1812 Ok(None) => Ok(false),
1813 Err(e) => Err(e),
1814 }
1815 }
1816
1817 pub fn shortest_path_length(
1822 &self,
1823 from: &EntityId,
1824 to: &EntityId,
1825 ) -> Result<Option<usize>, AgentRuntimeError> {
1826 Ok(self.shortest_path(from, to)?.map(|path| path.len().saturating_sub(1)))
1827 }
1828
1829 pub fn bfs_bounded(
1833 &self,
1834 start: &str,
1835 max_depth: usize,
1836 max_nodes: usize,
1837 ) -> Result<Vec<EntityId>, AgentRuntimeError> {
1838 let inner = recover_lock(self.inner.lock(), "bfs_bounded");
1839 let start_id = EntityId::new(start);
1840 if !inner.entities.contains_key(&start_id) {
1841 return Err(AgentRuntimeError::Graph(format!(
1842 "start entity '{start}' not found"
1843 )));
1844 }
1845
1846 let mut visited: std::collections::HashMap<EntityId, usize> = std::collections::HashMap::new();
1847 let mut queue: VecDeque<(EntityId, usize)> = VecDeque::new();
1848 let mut result: Vec<EntityId> = Vec::new();
1849
1850 visited.insert(start_id.clone(), 0);
1851 queue.push_back((start_id.clone(), 0));
1852 result.push(start_id);
1853
1854 while let Some((current, depth)) = queue.pop_front() {
1855 if result.len() >= max_nodes {
1856 break;
1857 }
1858 if depth >= max_depth {
1859 continue;
1860 }
1861 for neighbour in Self::neighbours(&inner.adjacency, ¤t) {
1862 if !visited.contains_key(&neighbour) {
1863 let new_depth = depth + 1;
1864 visited.insert(neighbour.clone(), new_depth);
1865 result.push(neighbour.clone());
1866 if result.len() >= max_nodes {
1867 break;
1868 }
1869 queue.push_back((neighbour, new_depth));
1870 }
1871 }
1872 }
1873
1874 Ok(result)
1875 }
1876
1877 pub fn dfs_bounded(
1881 &self,
1882 start: &str,
1883 max_depth: usize,
1884 max_nodes: usize,
1885 ) -> Result<Vec<EntityId>, AgentRuntimeError> {
1886 let inner = recover_lock(self.inner.lock(), "dfs_bounded");
1887 let start_id = EntityId::new(start);
1888 if !inner.entities.contains_key(&start_id) {
1889 return Err(AgentRuntimeError::Graph(format!(
1890 "start entity '{start}' not found"
1891 )));
1892 }
1893
1894 let mut visited: HashSet<EntityId> = HashSet::new();
1895 let mut stack: Vec<(EntityId, usize)> = Vec::new();
1896 let mut result: Vec<EntityId> = Vec::new();
1897
1898 visited.insert(start_id.clone());
1899 stack.push((start_id.clone(), 0));
1900 result.push(start_id);
1901
1902 while let Some((current, depth)) = stack.pop() {
1903 if result.len() >= max_nodes {
1904 break;
1905 }
1906 if depth >= max_depth {
1907 continue;
1908 }
1909 for neighbour in Self::neighbours(&inner.adjacency, ¤t) {
1910 if visited.insert(neighbour.clone()) {
1911 result.push(neighbour.clone());
1912 if result.len() >= max_nodes {
1913 break;
1914 }
1915 stack.push((neighbour, depth + 1));
1916 }
1917 }
1918 }
1919
1920 Ok(result)
1921 }
1922
1923 pub fn all_entities(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1925 let inner = recover_lock(self.inner.lock(), "all_entities");
1926 Ok(inner.entities.values().cloned().collect())
1927 }
1928
1929 pub fn all_relationships(&self) -> Result<Vec<Relationship>, AgentRuntimeError> {
1931 let inner = recover_lock(self.inner.lock(), "all_relationships");
1932 Ok(inner.relationships.clone())
1933 }
1934
1935 pub fn find_relationships_by_kind(&self, kind: &str) -> Result<Vec<Relationship>, AgentRuntimeError> {
1937 let inner = recover_lock(self.inner.lock(), "find_relationships_by_kind");
1938 Ok(inner.relationships.iter().filter(|r| r.kind == kind).cloned().collect())
1939 }
1940
1941 pub fn count_relationships_by_kind(&self, kind: &str) -> Result<usize, AgentRuntimeError> {
1943 let inner = recover_lock(self.inner.lock(), "count_relationships_by_kind");
1944 Ok(inner.relationships.iter().filter(|r| r.kind == kind).count())
1945 }
1946
1947 pub fn find_entities_by_label(&self, label: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
1949 let inner = recover_lock(self.inner.lock(), "find_entities_by_label");
1950 Ok(inner
1951 .entities
1952 .values()
1953 .filter(|e| e.label == label)
1954 .cloned()
1955 .collect())
1956 }
1957
1958 pub fn find_entities_by_labels(&self, labels: &[&str]) -> Result<Vec<Entity>, AgentRuntimeError> {
1963 let inner = recover_lock(self.inner.lock(), "find_entities_by_labels");
1964 let label_set: std::collections::HashSet<&str> = labels.iter().copied().collect();
1965 Ok(inner
1966 .entities
1967 .values()
1968 .filter(|e| label_set.contains(e.label.as_str()))
1969 .cloned()
1970 .collect())
1971 }
1972
1973 pub fn remove_isolated(&self) -> Result<usize, AgentRuntimeError> {
1977 let mut inner = recover_lock(self.inner.lock(), "remove_isolated");
1978 let isolated: Vec<EntityId> = inner
1979 .entities
1980 .keys()
1981 .filter(|id| {
1982 inner.adjacency.get(*id).map_or(true, |v| v.is_empty())
1983 && inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty())
1984 })
1985 .cloned()
1986 .collect();
1987 let count = isolated.len();
1988 for id in &isolated {
1989 inner.entities.remove(id);
1990 inner.adjacency.remove(id);
1991 inner.reverse_adjacency.remove(id);
1992 }
1993 if count > 0 {
1994 inner.cycle_cache = None;
1995 }
1996 Ok(count)
1997 }
1998
1999 pub fn get_relationships_for(
2001 &self,
2002 id: &EntityId,
2003 ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2004 let inner = recover_lock(self.inner.lock(), "get_relationships_for");
2005 Ok(inner
2006 .adjacency
2007 .get(id)
2008 .cloned()
2009 .unwrap_or_default())
2010 }
2011
2012 pub fn relationships_between(
2014 &self,
2015 from: &EntityId,
2016 to: &EntityId,
2017 ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2018 let inner = recover_lock(self.inner.lock(), "relationships_between");
2019 Ok(inner
2020 .relationships
2021 .iter()
2022 .filter(|r| {
2023 (r.from == *from && r.to == *to) || (r.from == *to && r.to == *from)
2024 })
2025 .cloned()
2026 .collect())
2027 }
2028
2029 pub fn find_entities_by_property(
2034 &self,
2035 key: &str,
2036 expected: &serde_json::Value,
2037 ) -> Result<Vec<Entity>, AgentRuntimeError> {
2038 let inner = recover_lock(self.inner.lock(), "find_entities_by_property");
2039 Ok(inner
2040 .entities
2041 .values()
2042 .filter(|e| e.properties.get(key) == Some(expected))
2043 .cloned()
2044 .collect())
2045 }
2046
2047 pub fn merge(&self, other: &GraphStore) -> Result<(), AgentRuntimeError> {
2052 let other_inner = recover_lock(other.inner.lock(), "merge:read");
2053 let other_entities: Vec<Entity> = other_inner.entities.values().cloned().collect();
2054 let other_rels: Vec<Relationship> = other_inner.relationships.clone();
2055 drop(other_inner);
2056
2057 let mut inner = recover_lock(self.inner.lock(), "merge:write");
2058 inner.cycle_cache = None;
2059 for entity in other_entities {
2060 inner.adjacency.entry(entity.id.clone()).or_default();
2061 inner.entities.insert(entity.id.clone(), entity);
2062 }
2063 for rel in other_rels {
2064 let already_exists = inner
2065 .relationships
2066 .iter()
2067 .any(|r| r.from == rel.from && r.to == rel.to && r.kind == rel.kind);
2068 if !already_exists && inner.entities.contains_key(&rel.from) && inner.entities.contains_key(&rel.to) {
2069 inner
2070 .adjacency
2071 .entry(rel.from.clone())
2072 .or_default()
2073 .push(rel.clone());
2074 inner.relationships.push(rel);
2075 }
2076 }
2077 Ok(())
2078 }
2079
2080 pub fn neighbor_entities(&self, id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
2085 let inner = recover_lock(self.inner.lock(), "neighbor_entities");
2086 let neighbors: Vec<Entity> = inner
2087 .adjacency
2088 .get(id)
2089 .iter()
2090 .flat_map(|rels| rels.iter())
2091 .filter_map(|r| inner.entities.get(&r.to).cloned())
2092 .collect();
2093 Ok(neighbors)
2094 }
2095
2096 pub fn neighbor_ids(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
2103 let inner = recover_lock(self.inner.lock(), "neighbor_ids");
2104 let ids: Vec<EntityId> = inner
2105 .adjacency
2106 .get(id)
2107 .iter()
2108 .flat_map(|rels| rels.iter())
2109 .map(|r| r.to.clone())
2110 .collect();
2111 Ok(ids)
2112 }
2113
2114 pub fn remove_all_relationships_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
2124 let mut inner = recover_lock(self.inner.lock(), "remove_all_relationships_for");
2125 inner.adjacency.remove(id);
2126 let before = inner.relationships.len();
2127 inner.relationships.retain(|r| &r.from != id);
2128 inner.cycle_cache = None;
2129 Ok(before - inner.relationships.len())
2130 }
2131
2132 pub fn entity_exists(&self, id: &EntityId) -> Result<bool, AgentRuntimeError> {
2134 let inner = recover_lock(self.inner.lock(), "entity_exists");
2135 Ok(inner.entities.contains_key(id))
2136 }
2137
2138 pub fn relationship_exists(
2143 &self,
2144 from: &EntityId,
2145 to: &EntityId,
2146 kind: &str,
2147 ) -> Result<bool, AgentRuntimeError> {
2148 let inner = recover_lock(self.inner.lock(), "relationship_exists");
2149 Ok(inner
2150 .adjacency
2151 .get(from)
2152 .map_or(false, |rels| rels.iter().any(|r| r.to == *to && r.kind == kind)))
2153 }
2154
2155 pub fn subgraph(&self, node_ids: &[EntityId]) -> Result<GraphStore, AgentRuntimeError> {
2158 let inner = recover_lock(self.inner.lock(), "subgraph");
2159 let id_set: HashSet<&EntityId> = node_ids.iter().collect();
2160
2161 let new_store = GraphStore::new();
2162
2163 let entities_to_copy: Vec<Entity> = node_ids
2166 .iter()
2167 .map(|id| {
2168 inner
2169 .entities
2170 .get(id)
2171 .cloned()
2172 .ok_or_else(|| {
2173 AgentRuntimeError::Graph(format!("entity '{}' not found", id.0))
2174 })
2175 })
2176 .collect::<Result<_, _>>()?;
2177
2178 {
2180 let mut new_inner = recover_lock(new_store.inner.lock(), "subgraph:add_entities");
2181 for entity in entities_to_copy {
2182 new_inner.adjacency.entry(entity.id.clone()).or_default();
2184 new_inner.entities.insert(entity.id.clone(), entity);
2185 }
2186 }
2187
2188 {
2191 let mut new_inner =
2192 recover_lock(new_store.inner.lock(), "subgraph:add_relationships");
2193 for rel in inner.relationships.iter() {
2194 if id_set.contains(&rel.from) && id_set.contains(&rel.to) {
2195 new_inner
2197 .adjacency
2198 .entry(rel.from.clone())
2199 .or_default()
2200 .push(rel.clone());
2201 new_inner.relationships.push(rel.clone());
2202 }
2203 }
2204 }
2205
2206 Ok(new_store)
2207 }
2208
2209 pub fn reverse(&self) -> Result<GraphStore, AgentRuntimeError> {
2214 let inner = recover_lock(self.inner.lock(), "reverse");
2215 let reversed = GraphStore::new();
2216
2217 {
2219 let mut r_inner = recover_lock(reversed.inner.lock(), "reverse:entities");
2220 for entity in inner.entities.values() {
2221 r_inner.adjacency.entry(entity.id.clone()).or_default();
2222 r_inner.entities.insert(entity.id.clone(), entity.clone());
2223 }
2224 }
2225
2226 for rel in &inner.relationships {
2228 let flipped = Relationship {
2229 from: rel.to.clone(),
2230 to: rel.from.clone(),
2231 kind: rel.kind.clone(),
2232 weight: rel.weight,
2233 };
2234 let mut r_inner = recover_lock(reversed.inner.lock(), "reverse:rels");
2235 r_inner
2236 .adjacency
2237 .entry(flipped.from.clone())
2238 .or_default()
2239 .push(flipped.clone());
2240 r_inner.relationships.push(flipped);
2241 }
2242
2243 Ok(reversed)
2244 }
2245
2246 pub fn common_neighbors(
2251 &self,
2252 a: &EntityId,
2253 b: &EntityId,
2254 ) -> Result<Vec<Entity>, AgentRuntimeError> {
2255 let inner = recover_lock(self.inner.lock(), "common_neighbors");
2256 let a_set: HashSet<&EntityId> = inner
2257 .adjacency
2258 .get(a)
2259 .map_or(HashSet::new(), |rels| rels.iter().map(|r| &r.to).collect());
2260 let b_set: HashSet<&EntityId> = inner
2261 .adjacency
2262 .get(b)
2263 .map_or(HashSet::new(), |rels| rels.iter().map(|r| &r.to).collect());
2264 let common: Vec<Entity> = a_set
2265 .intersection(&b_set)
2266 .filter_map(|id| inner.entities.get(*id).cloned())
2267 .collect();
2268 Ok(common)
2269 }
2270
2271 pub fn weight_of(
2275 &self,
2276 from: &EntityId,
2277 to: &EntityId,
2278 ) -> Result<Option<f32>, AgentRuntimeError> {
2279 let inner = recover_lock(self.inner.lock(), "weight_of");
2280 let weight = inner
2281 .adjacency
2282 .get(from)
2283 .and_then(|rels| rels.iter().find(|r| &r.to == to))
2284 .map(|r| r.weight);
2285 Ok(weight)
2286 }
2287
2288 pub fn neighbors_in(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
2293 let inner = recover_lock(self.inner.lock(), "neighbors_in");
2294 Ok(inner
2296 .reverse_adjacency
2297 .get(id)
2298 .cloned()
2299 .unwrap_or_default())
2300 }
2301
2302 pub fn density(&self) -> Result<f64, AgentRuntimeError> {
2307 let inner = recover_lock(self.inner.lock(), "density");
2308 let n = inner.entities.len();
2309 if n < 2 {
2310 return Ok(0.0);
2311 }
2312 let max_edges = n * (n - 1);
2313 Ok(inner.relationships.len() as f64 / max_edges as f64)
2314 }
2315
2316 pub fn avg_degree(&self) -> Result<f64, AgentRuntimeError> {
2320 let inner = recover_lock(self.inner.lock(), "avg_degree");
2321 let n = inner.entities.len();
2322 if n == 0 {
2323 return Ok(0.0);
2324 }
2325 Ok(inner.relationships.len() as f64 / n as f64)
2326 }
2327
2328 pub fn total_weight(&self) -> Result<f32, AgentRuntimeError> {
2332 let inner = recover_lock(self.inner.lock(), "total_weight");
2333 Ok(inner.relationships.iter().map(|r| r.weight).sum())
2334 }
2335
2336 pub fn max_edge_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
2338 let inner = recover_lock(self.inner.lock(), "max_edge_weight");
2339 Ok(inner
2340 .relationships
2341 .iter()
2342 .map(|r| r.weight)
2343 .reduce(f32::max))
2344 }
2345
2346 pub fn min_edge_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
2348 let inner = recover_lock(self.inner.lock(), "min_edge_weight");
2349 Ok(inner
2350 .relationships
2351 .iter()
2352 .map(|r| r.weight)
2353 .reduce(f32::min))
2354 }
2355
2356 pub fn top_n_by_out_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2361 if n == 0 {
2362 return Ok(Vec::new());
2363 }
2364 let inner = recover_lock(self.inner.lock(), "top_n_by_out_degree");
2365 let mut pairs: Vec<(&EntityId, usize)> = inner
2366 .adjacency
2367 .iter()
2368 .map(|(id, rels)| (id, rels.len()))
2369 .collect();
2370 pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
2371 Ok(pairs
2372 .into_iter()
2373 .take(n)
2374 .filter_map(|(id, _)| inner.entities.get(id).cloned())
2375 .collect())
2376 }
2377
2378 pub fn remove_entity_and_edges(&self, id: &EntityId) -> Result<(), AgentRuntimeError> {
2386 let mut inner = recover_lock(self.inner.lock(), "remove_entity_and_edges");
2387 if !inner.entities.contains_key(id) {
2388 return Err(AgentRuntimeError::Graph(format!(
2389 "entity '{}' not found",
2390 id.0
2391 )));
2392 }
2393 inner.entities.remove(id);
2394 inner.relationships.retain(|r| &r.from != id && &r.to != id);
2395 inner.adjacency.remove(id);
2396 for adj in inner.adjacency.values_mut() {
2397 adj.retain(|r| &r.to != id);
2398 }
2399 inner.reverse_adjacency.remove(id);
2400 for rev in inner.reverse_adjacency.values_mut() {
2401 rev.retain(|src| src != id);
2402 }
2403 inner.cycle_cache = None;
2404 Ok(())
2405 }
2406
2407 pub fn hub_nodes(&self, threshold: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2411 let inner = recover_lock(self.inner.lock(), "hub_nodes");
2412 Ok(inner
2413 .entities
2414 .values()
2415 .filter(|e| {
2416 inner
2417 .adjacency
2418 .get(&e.id)
2419 .map_or(0, |rels| rels.len())
2420 >= threshold
2421 })
2422 .cloned()
2423 .collect())
2424 }
2425
2426 pub fn incident_relationships(
2429 &self,
2430 entity_id: &EntityId,
2431 ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2432 let inner = recover_lock(self.inner.lock(), "incident_relationships");
2433 Ok(inner
2434 .relationships
2435 .iter()
2436 .filter(|r| &r.from == entity_id || &r.to == entity_id)
2437 .cloned()
2438 .collect())
2439 }
2440
2441 pub fn max_out_degree_entity(&self) -> Result<Option<Entity>, AgentRuntimeError> {
2447 let inner = recover_lock(self.inner.lock(), "max_out_degree_entity");
2448 let best = inner
2449 .adjacency
2450 .iter()
2451 .max_by_key(|(_, rels)| rels.len())
2452 .and_then(|(id, _)| inner.entities.get(id).cloned());
2453 Ok(best)
2454 }
2455
2456 pub fn max_in_degree_entity(&self) -> Result<Option<Entity>, AgentRuntimeError> {
2461 let inner = recover_lock(self.inner.lock(), "max_in_degree_entity");
2462 let best = inner
2463 .reverse_adjacency
2464 .iter()
2465 .max_by_key(|(_, srcs)| srcs.len())
2466 .and_then(|(id, _)| inner.entities.get(id).cloned());
2467 Ok(best)
2468 }
2469
2470 pub fn leaf_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
2478 let inner = recover_lock(self.inner.lock(), "leaf_nodes");
2479 Ok(inner
2480 .entities
2481 .values()
2482 .filter(|e| {
2483 inner
2484 .adjacency
2485 .get(&e.id)
2486 .map_or(true, |rels| rels.is_empty())
2487 })
2488 .cloned()
2489 .collect())
2490 }
2491
2492 pub fn top_nodes_by_out_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2498 let inner = recover_lock(self.inner.lock(), "top_nodes_by_out_degree");
2499 let mut pairs: Vec<(&EntityId, usize)> = inner
2500 .entities
2501 .keys()
2502 .map(|id| (id, inner.adjacency.get(id).map_or(0, |v| v.len())))
2503 .collect();
2504 pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
2505 pairs.truncate(n);
2506 Ok(pairs
2507 .into_iter()
2508 .filter_map(|(id, _)| inner.entities.get(id).cloned())
2509 .collect())
2510 }
2511
2512 pub fn top_nodes_by_in_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2517 let inner = recover_lock(self.inner.lock(), "top_nodes_by_in_degree");
2518 let mut pairs: Vec<(&EntityId, usize)> = inner
2519 .entities
2520 .keys()
2521 .map(|id| (id, inner.reverse_adjacency.get(id).map_or(0, |v| v.len())))
2522 .collect();
2523 pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
2524 pairs.truncate(n);
2525 Ok(pairs
2526 .into_iter()
2527 .filter_map(|(id, _)| inner.entities.get(id).cloned())
2528 .collect())
2529 }
2530}
2531
2532impl Default for GraphStore {
2533 fn default() -> Self {
2534 Self::new()
2535 }
2536}
2537
2538#[cfg(test)]
2541mod tests {
2542 use super::*;
2543
2544 fn make_graph() -> GraphStore {
2545 GraphStore::new()
2546 }
2547
2548 fn add(g: &GraphStore, id: &str) {
2549 g.add_entity(Entity::new(id, "Node")).unwrap();
2550 }
2551
2552 fn link(g: &GraphStore, from: &str, to: &str) {
2553 g.add_relationship(Relationship::new(from, to, "CONNECTS", 1.0))
2554 .unwrap();
2555 }
2556
2557 fn link_w(g: &GraphStore, from: &str, to: &str, weight: f32) {
2558 g.add_relationship(Relationship::new(from, to, "CONNECTS", weight))
2559 .unwrap();
2560 }
2561
2562 #[test]
2565 fn test_entity_id_equality() {
2566 assert_eq!(EntityId::new("a"), EntityId::new("a"));
2567 assert_ne!(EntityId::new("a"), EntityId::new("b"));
2568 }
2569
2570 #[test]
2571 fn test_entity_id_display() {
2572 let id = EntityId::new("hello");
2573 assert_eq!(id.to_string(), "hello");
2574 }
2575
2576 #[test]
2579 fn test_entity_new_has_empty_properties() {
2580 let e = Entity::new("e1", "Person");
2581 assert!(e.properties.is_empty());
2582 }
2583
2584 #[test]
2585 fn test_entity_with_properties_stores_props() {
2586 let mut props = HashMap::new();
2587 props.insert("age".into(), Value::Number(42.into()));
2588 let e = Entity::with_properties("e1", "Person", props);
2589 assert!(e.properties.contains_key("age"));
2590 }
2591
2592 #[test]
2595 fn test_graph_add_entity_increments_count() {
2596 let g = make_graph();
2597 add(&g, "a");
2598 assert_eq!(g.entity_count().unwrap(), 1);
2599 }
2600
2601 #[test]
2602 fn test_graph_get_entity_returns_entity() {
2603 let g = make_graph();
2604 g.add_entity(Entity::new("e1", "Person")).unwrap();
2605 let e = g.get_entity(&EntityId::new("e1")).unwrap();
2606 assert_eq!(e.label, "Person");
2607 }
2608
2609 #[test]
2610 fn test_graph_get_entity_missing_returns_error() {
2611 let g = make_graph();
2612 assert!(g.get_entity(&EntityId::new("ghost")).is_err());
2613 }
2614
2615 #[test]
2616 fn test_graph_add_relationship_increments_count() {
2617 let g = make_graph();
2618 add(&g, "a");
2619 add(&g, "b");
2620 link(&g, "a", "b");
2621 assert_eq!(g.relationship_count().unwrap(), 1);
2622 }
2623
2624 #[test]
2625 fn test_graph_add_relationship_missing_source_fails() {
2626 let g = make_graph();
2627 add(&g, "b");
2628 let result = g.add_relationship(Relationship::new("ghost", "b", "X", 1.0));
2629 assert!(result.is_err());
2630 }
2631
2632 #[test]
2633 fn test_graph_add_relationship_missing_target_fails() {
2634 let g = make_graph();
2635 add(&g, "a");
2636 let result = g.add_relationship(Relationship::new("a", "ghost", "X", 1.0));
2637 assert!(result.is_err());
2638 }
2639
2640 #[test]
2641 fn test_graph_remove_entity_removes_relationships() {
2642 let g = make_graph();
2643 add(&g, "a");
2644 add(&g, "b");
2645 link(&g, "a", "b");
2646 g.remove_entity(&EntityId::new("a")).unwrap();
2647 assert_eq!(g.entity_count().unwrap(), 1);
2648 assert_eq!(g.relationship_count().unwrap(), 0);
2649 }
2650
2651 #[test]
2652 fn test_graph_remove_entity_missing_returns_error() {
2653 let g = make_graph();
2654 assert!(g.remove_entity(&EntityId::new("ghost")).is_err());
2655 }
2656
2657 #[test]
2660 fn test_bfs_finds_direct_neighbours() {
2661 let g = make_graph();
2662 add(&g, "a");
2663 add(&g, "b");
2664 add(&g, "c");
2665 link(&g, "a", "b");
2666 link(&g, "a", "c");
2667 let visited = g.bfs(&EntityId::new("a")).unwrap();
2668 assert_eq!(visited.len(), 2);
2669 }
2670
2671 #[test]
2672 fn test_bfs_traverses_chain() {
2673 let g = make_graph();
2674 add(&g, "a");
2675 add(&g, "b");
2676 add(&g, "c");
2677 add(&g, "d");
2678 link(&g, "a", "b");
2679 link(&g, "b", "c");
2680 link(&g, "c", "d");
2681 let visited = g.bfs(&EntityId::new("a")).unwrap();
2682 assert_eq!(visited.len(), 3);
2683 assert_eq!(visited[0], EntityId::new("b"));
2684 }
2685
2686 #[test]
2687 fn test_bfs_handles_isolated_node() {
2688 let g = make_graph();
2689 add(&g, "a");
2690 let visited = g.bfs(&EntityId::new("a")).unwrap();
2691 assert!(visited.is_empty());
2692 }
2693
2694 #[test]
2695 fn test_bfs_missing_start_returns_error() {
2696 let g = make_graph();
2697 assert!(g.bfs(&EntityId::new("ghost")).is_err());
2698 }
2699
2700 #[test]
2703 fn test_dfs_visits_all_reachable_nodes() {
2704 let g = make_graph();
2705 add(&g, "a");
2706 add(&g, "b");
2707 add(&g, "c");
2708 add(&g, "d");
2709 link(&g, "a", "b");
2710 link(&g, "a", "c");
2711 link(&g, "b", "d");
2712 let visited = g.dfs(&EntityId::new("a")).unwrap();
2713 assert_eq!(visited.len(), 3);
2714 }
2715
2716 #[test]
2717 fn test_dfs_handles_isolated_node() {
2718 let g = make_graph();
2719 add(&g, "a");
2720 let visited = g.dfs(&EntityId::new("a")).unwrap();
2721 assert!(visited.is_empty());
2722 }
2723
2724 #[test]
2725 fn test_dfs_missing_start_returns_error() {
2726 let g = make_graph();
2727 assert!(g.dfs(&EntityId::new("ghost")).is_err());
2728 }
2729
2730 #[test]
2733 fn test_shortest_path_direct_connection() {
2734 let g = make_graph();
2735 add(&g, "a");
2736 add(&g, "b");
2737 link(&g, "a", "b");
2738 let path = g
2739 .shortest_path(&EntityId::new("a"), &EntityId::new("b"))
2740 .unwrap();
2741 assert_eq!(path, Some(vec![EntityId::new("a"), EntityId::new("b")]));
2742 }
2743
2744 #[test]
2745 fn test_shortest_path_multi_hop() {
2746 let g = make_graph();
2747 add(&g, "a");
2748 add(&g, "b");
2749 add(&g, "c");
2750 link(&g, "a", "b");
2751 link(&g, "b", "c");
2752 let path = g
2753 .shortest_path(&EntityId::new("a"), &EntityId::new("c"))
2754 .unwrap();
2755 assert_eq!(path.as_ref().map(|p| p.len()), Some(3));
2756 }
2757
2758 #[test]
2759 fn test_shortest_path_returns_none_for_disconnected() {
2760 let g = make_graph();
2761 add(&g, "a");
2762 add(&g, "b");
2763 let path = g
2764 .shortest_path(&EntityId::new("a"), &EntityId::new("b"))
2765 .unwrap();
2766 assert_eq!(path, None);
2767 }
2768
2769 #[test]
2770 fn test_shortest_path_same_node_returns_single_element() {
2771 let g = make_graph();
2772 add(&g, "a");
2773 let path = g
2774 .shortest_path(&EntityId::new("a"), &EntityId::new("a"))
2775 .unwrap();
2776 assert_eq!(path, Some(vec![EntityId::new("a")]));
2777 }
2778
2779 #[test]
2780 fn test_shortest_path_missing_source_returns_error() {
2781 let g = make_graph();
2782 add(&g, "b");
2783 assert!(g
2784 .shortest_path(&EntityId::new("ghost"), &EntityId::new("b"))
2785 .is_err());
2786 }
2787
2788 #[test]
2789 fn test_shortest_path_missing_target_returns_error() {
2790 let g = make_graph();
2791 add(&g, "a");
2792 assert!(g
2793 .shortest_path(&EntityId::new("a"), &EntityId::new("ghost"))
2794 .is_err());
2795 }
2796
2797 #[test]
2800 fn test_transitive_closure_includes_start() {
2801 let g = make_graph();
2802 add(&g, "a");
2803 add(&g, "b");
2804 link(&g, "a", "b");
2805 let closure = g.transitive_closure(&EntityId::new("a")).unwrap();
2806 assert!(closure.contains(&EntityId::new("a")));
2807 assert!(closure.contains(&EntityId::new("b")));
2808 }
2809
2810 #[test]
2811 fn test_transitive_closure_isolated_node_contains_only_self() {
2812 let g = make_graph();
2813 add(&g, "a");
2814 let closure = g.transitive_closure(&EntityId::new("a")).unwrap();
2815 assert_eq!(closure.len(), 1);
2816 }
2817
2818 #[test]
2821 fn test_mem_graph_error_converts_to_runtime_error() {
2822 let e = MemGraphError::EntityNotFound("x".into());
2823 let re: AgentRuntimeError = e.into();
2824 assert!(matches!(re, AgentRuntimeError::Graph(_)));
2825 }
2826
2827 #[test]
2830 fn test_shortest_path_weighted_simple() {
2831 let g = make_graph();
2834 add(&g, "a");
2835 add(&g, "b");
2836 add(&g, "c");
2837 link_w(&g, "a", "b", 1.0);
2838 link_w(&g, "b", "c", 2.0);
2839 g.add_relationship(Relationship::new("a", "c", "DIRECT", 10.0))
2840 .unwrap();
2841
2842 let result = g
2843 .shortest_path_weighted(&EntityId::new("a"), &EntityId::new("c"))
2844 .unwrap();
2845 assert!(result.is_some());
2846 let (path, weight) = result.unwrap();
2847 assert_eq!(
2849 path,
2850 vec![EntityId::new("a"), EntityId::new("b"), EntityId::new("c")]
2851 );
2852 assert!((weight - 3.0).abs() < 1e-5);
2853 }
2854
2855 #[test]
2856 fn test_shortest_path_weighted_returns_none_for_disconnected() {
2857 let g = make_graph();
2858 add(&g, "a");
2859 add(&g, "b");
2860 let result = g
2861 .shortest_path_weighted(&EntityId::new("a"), &EntityId::new("b"))
2862 .unwrap();
2863 assert!(result.is_none());
2864 }
2865
2866 #[test]
2867 fn test_shortest_path_weighted_same_node() {
2868 let g = make_graph();
2869 add(&g, "a");
2870 let result = g
2871 .shortest_path_weighted(&EntityId::new("a"), &EntityId::new("a"))
2872 .unwrap();
2873 assert_eq!(result, Some((vec![EntityId::new("a")], 0.0)));
2874 }
2875
2876 #[test]
2877 fn test_shortest_path_weighted_negative_weight_errors() {
2878 let g = make_graph();
2879 add(&g, "a");
2880 add(&g, "b");
2881 g.add_relationship(Relationship::new("a", "b", "NEG", -1.0))
2882 .unwrap();
2883 let result = g.shortest_path_weighted(&EntityId::new("a"), &EntityId::new("b"));
2884 assert!(result.is_err());
2885 }
2886
2887 #[test]
2890 fn test_degree_centrality_basic() {
2891 let g = make_graph();
2895 add(&g, "a");
2896 add(&g, "b");
2897 add(&g, "c");
2898 add(&g, "d");
2899 link(&g, "a", "b");
2900 link(&g, "a", "c");
2901 link(&g, "a", "d");
2902
2903 let centrality = g.degree_centrality().unwrap();
2904 let a_cent = *centrality.get(&EntityId::new("a")).unwrap();
2905 let b_cent = *centrality.get(&EntityId::new("b")).unwrap();
2906
2907 assert!((a_cent - 1.0).abs() < 1e-5, "a centrality was {a_cent}");
2908 assert!(
2909 (b_cent - 1.0 / 3.0).abs() < 1e-5,
2910 "b centrality was {b_cent}"
2911 );
2912 }
2913
2914 #[test]
2917 fn test_betweenness_centrality_chain() {
2918 let g = make_graph();
2922 add(&g, "a");
2923 add(&g, "b");
2924 add(&g, "c");
2925 add(&g, "d");
2926 link(&g, "a", "b");
2927 link(&g, "b", "c");
2928 link(&g, "c", "d");
2929
2930 let centrality = g.betweenness_centrality().unwrap();
2931 let a_cent = *centrality.get(&EntityId::new("a")).unwrap();
2932 let b_cent = *centrality.get(&EntityId::new("b")).unwrap();
2933 let c_cent = *centrality.get(&EntityId::new("c")).unwrap();
2934 let d_cent = *centrality.get(&EntityId::new("d")).unwrap();
2935
2936 assert!((a_cent).abs() < 1e-5, "expected a_cent ~ 0, got {a_cent}");
2938 assert!(b_cent > 0.0, "expected b_cent > 0, got {b_cent}");
2939 assert!(c_cent > 0.0, "expected c_cent > 0, got {c_cent}");
2940 assert!((d_cent).abs() < 1e-5, "expected d_cent ~ 0, got {d_cent}");
2941 }
2942
2943 #[test]
2946 fn test_label_propagation_communities_two_clusters() {
2947 let g = make_graph();
2951 for id in &["a", "b", "c", "x", "y", "z"] {
2952 add(&g, id);
2953 }
2954 link(&g, "a", "b");
2956 link(&g, "b", "a");
2957 link(&g, "b", "c");
2958 link(&g, "c", "b");
2959 link(&g, "a", "c");
2960 link(&g, "c", "a");
2961 link(&g, "x", "y");
2963 link(&g, "y", "x");
2964 link(&g, "y", "z");
2965 link(&g, "z", "y");
2966 link(&g, "x", "z");
2967 link(&g, "z", "x");
2968
2969 let communities = g.label_propagation_communities(100).unwrap();
2970
2971 let label_a = communities[&EntityId::new("a")];
2972 let label_b = communities[&EntityId::new("b")];
2973 let label_c = communities[&EntityId::new("c")];
2974 let label_x = communities[&EntityId::new("x")];
2975 let label_y = communities[&EntityId::new("y")];
2976 let label_z = communities[&EntityId::new("z")];
2977
2978 assert_eq!(label_a, label_b, "a and b should be in same community");
2981 assert_eq!(label_b, label_c, "b and c should be in same community");
2982 assert_eq!(label_x, label_y, "x and y should be in same community");
2983 assert_eq!(label_y, label_z, "y and z should be in same community");
2984 assert_ne!(
2985 label_a, label_x,
2986 "cluster 1 and cluster 2 should be different communities"
2987 );
2988 }
2989
2990 #[test]
2993 fn test_subgraph_extracts_correct_nodes_and_edges() {
2994 let g = make_graph();
2997 add(&g, "a");
2998 add(&g, "b");
2999 add(&g, "c");
3000 add(&g, "d");
3001 link(&g, "a", "b");
3002 link(&g, "b", "c");
3003 link(&g, "c", "d");
3004
3005 let sub = g
3006 .subgraph(&[EntityId::new("a"), EntityId::new("b"), EntityId::new("c")])
3007 .unwrap();
3008
3009 assert_eq!(sub.entity_count().unwrap(), 3);
3010 assert_eq!(sub.relationship_count().unwrap(), 2);
3011
3012 assert!(sub.get_entity(&EntityId::new("d")).is_err());
3014
3015 let path = sub
3017 .shortest_path(&EntityId::new("a"), &EntityId::new("c"))
3018 .unwrap();
3019 assert!(path.is_some());
3020 assert_eq!(path.unwrap().len(), 3);
3021 }
3022
3023 #[test]
3026 fn test_detect_cycles_dag_returns_false() {
3027 let g = make_graph();
3028 add(&g, "a");
3029 add(&g, "b");
3030 add(&g, "c");
3031 link(&g, "a", "b");
3032 link(&g, "b", "c");
3033 assert_eq!(g.detect_cycles().unwrap(), false);
3034 }
3035
3036 #[test]
3037 fn test_detect_cycles_self_loop_returns_true() {
3038 let g = make_graph();
3039 add(&g, "a");
3040 g.add_relationship(Relationship::new("a", "a", "SELF", 1.0))
3042 .unwrap();
3043 assert_eq!(g.detect_cycles().unwrap(), true);
3044 }
3045
3046 #[test]
3047 fn test_detect_cycles_simple_cycle_returns_true() {
3048 let g = make_graph();
3049 add(&g, "a");
3050 add(&g, "b");
3051 link(&g, "a", "b");
3052 g.add_relationship(Relationship::new("b", "a", "BACK", 1.0))
3053 .unwrap();
3054 assert_eq!(g.detect_cycles().unwrap(), true);
3055 }
3056
3057 #[test]
3058 fn test_detect_cycles_empty_graph_returns_false() {
3059 let g = make_graph();
3060 assert_eq!(g.detect_cycles().unwrap(), false);
3061 }
3062
3063 #[test]
3064 fn test_detect_cycles_result_is_cached() {
3065 let g = make_graph();
3066 add(&g, "x");
3067 add(&g, "y");
3068 link(&g, "x", "y");
3069 let r1 = g.detect_cycles().unwrap();
3071 let r2 = g.detect_cycles().unwrap();
3073 assert_eq!(r1, r2);
3074 }
3075
3076 #[test]
3077 fn test_detect_cycles_cache_invalidated_on_mutation() {
3078 let g = make_graph();
3079 add(&g, "a");
3080 add(&g, "b");
3081 link(&g, "a", "b");
3082 assert_eq!(g.detect_cycles().unwrap(), false);
3083
3084 g.add_relationship(Relationship::new("b", "a", "BACK", 1.0))
3086 .unwrap();
3087 assert_eq!(
3088 g.detect_cycles().unwrap(),
3089 true,
3090 "cache should be invalidated after adding a back edge"
3091 );
3092 }
3093
3094 #[test]
3097 fn test_bfs_bounded_respects_max_depth() {
3098 let g = make_graph();
3100 add(&g, "a");
3101 add(&g, "b");
3102 add(&g, "c");
3103 add(&g, "d");
3104 link(&g, "a", "b");
3105 link(&g, "b", "c");
3106 link(&g, "c", "d");
3107
3108 let visited = g.bfs_bounded("a", 1, 100).unwrap();
3110 assert!(visited.contains(&EntityId::new("a")));
3111 assert!(visited.contains(&EntityId::new("b")));
3112 assert!(!visited.contains(&EntityId::new("c")), "c is at depth 2, should not be visited");
3113 }
3114
3115 #[test]
3118 fn test_path_exists_returns_true() {
3119 let g = make_graph();
3120 add(&g, "a");
3121 add(&g, "b");
3122 add(&g, "c");
3123 link(&g, "a", "b");
3124 link(&g, "b", "c");
3125 assert_eq!(g.path_exists("a", "c").unwrap(), true);
3126 }
3127
3128 #[test]
3129 fn test_path_exists_returns_false() {
3130 let g = make_graph();
3131 add(&g, "a");
3132 add(&g, "b");
3133 assert_eq!(g.path_exists("a", "b").unwrap(), false);
3134 }
3135
3136 #[test]
3139 fn test_entity_id_as_str() {
3140 let id = EntityId::new("my-entity");
3141 assert_eq!(id.as_str(), "my-entity");
3142 }
3143
3144 #[test]
3147 fn test_entity_id_as_ref_str() {
3148 let id = EntityId::new("asref-test");
3149 let s: &str = id.as_ref();
3150 assert_eq!(s, "asref-test");
3151 }
3152
3153 #[test]
3154 fn test_dfs_bounded_respects_max_nodes() {
3155 let g = make_graph();
3157 add(&g, "a");
3158 add(&g, "b");
3159 add(&g, "c");
3160 add(&g, "d");
3161 link(&g, "a", "b");
3162 link(&g, "b", "c");
3163 link(&g, "c", "d");
3164
3165 let visited = g.dfs_bounded("a", 100, 2).unwrap();
3167 assert_eq!(visited.len(), 2, "should stop at 2 nodes");
3168 }
3169
3170 #[test]
3173 fn test_entity_exists_and_relationship_exists() {
3174 let g = GraphStore::new();
3175 let a = EntityId::new("a");
3176 let b = EntityId::new("b");
3177 assert!(!g.entity_exists(&a).unwrap());
3178 g.add_entity(Entity::new("a", "Node")).unwrap();
3179 assert!(g.entity_exists(&a).unwrap());
3180 assert!(!g.relationship_exists(&a, &b, "knows").unwrap());
3181 g.add_entity(Entity::new("b", "Node")).unwrap();
3182 g.add_relationship(Relationship::new("a", "b", "knows", 1.0)).unwrap();
3183 assert!(g.relationship_exists(&a, &b, "knows").unwrap());
3184 assert!(!g.relationship_exists(&a, &b, "likes").unwrap());
3185 }
3186
3187 #[test]
3188 fn test_get_relationships_for_returns_outgoing() {
3189 let g = GraphStore::new();
3190 g.add_entity(Entity::new("a", "N")).unwrap();
3191 g.add_entity(Entity::new("b", "N")).unwrap();
3192 g.add_entity(Entity::new("c", "N")).unwrap();
3193 let a = EntityId::new("a");
3194 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
3195 g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
3196 let rels = g.get_relationships_for(&a).unwrap();
3197 assert_eq!(rels.len(), 2);
3198 }
3199
3200 #[test]
3201 fn test_relationships_between_finds_both_directions() {
3202 let g = GraphStore::new();
3203 g.add_entity(Entity::new("x", "N")).unwrap();
3204 g.add_entity(Entity::new("y", "N")).unwrap();
3205 let x = EntityId::new("x");
3206 let y = EntityId::new("y");
3207 g.add_relationship(Relationship::new("x", "y", "follows", 1.0)).unwrap();
3208 g.add_relationship(Relationship::new("y", "x", "blocks", 1.0)).unwrap();
3209 let rels = g.relationships_between(&x, &y).unwrap();
3210 assert_eq!(rels.len(), 2);
3211 }
3212
3213 #[test]
3214 fn test_find_entities_by_property() {
3215 let g = GraphStore::new();
3216 g.add_entity(Entity::new("a", "Person").with_property("age", serde_json::json!(30))).unwrap();
3217 g.add_entity(Entity::new("b", "Person").with_property("age", serde_json::json!(25))).unwrap();
3218 g.add_entity(Entity::new("c", "Person").with_property("age", serde_json::json!(30))).unwrap();
3219 let found = g.find_entities_by_property("age", &serde_json::json!(30)).unwrap();
3220 assert_eq!(found.len(), 2);
3221 let ids: Vec<_> = found.iter().map(|e| e.id.as_str()).collect();
3222 assert!(ids.contains(&"a") && ids.contains(&"c"));
3223 }
3224
3225 #[test]
3226 fn test_neighbor_entities_returns_entity_objects() {
3227 let g = GraphStore::new();
3228 g.add_entity(Entity::new("root", "R")).unwrap();
3229 g.add_entity(Entity::new("child1", "C")).unwrap();
3230 g.add_entity(Entity::new("child2", "C")).unwrap();
3231 let root = EntityId::new("root");
3232 g.add_relationship(Relationship::new("root", "child1", "has", 1.0)).unwrap();
3233 g.add_relationship(Relationship::new("root", "child2", "has", 1.0)).unwrap();
3234 let neighbors = g.neighbor_entities(&root).unwrap();
3235 assert_eq!(neighbors.len(), 2);
3236 let labels: Vec<_> = neighbors.iter().map(|e| e.label.as_str()).collect();
3237 assert!(labels.iter().all(|l| *l == "C"));
3238 }
3239
3240 #[test]
3241 fn test_remove_all_relationships_for() {
3242 let g = GraphStore::new();
3243 g.add_entity(Entity::new("a", "N")).unwrap();
3244 g.add_entity(Entity::new("b", "N")).unwrap();
3245 let a = EntityId::new("a");
3246 g.add_relationship(Relationship::new("a", "b", "r1", 1.0)).unwrap();
3247 g.add_relationship(Relationship::new("a", "b", "r2", 1.0)).unwrap();
3248 let removed = g.remove_all_relationships_for(&a).unwrap();
3249 assert_eq!(removed, 2);
3250 assert_eq!(g.relationship_count().unwrap(), 0);
3251 }
3252
3253 #[test]
3254 fn test_topological_sort_returns_valid_order() {
3255 let g = GraphStore::new();
3256 for id in &["a", "b", "c", "d"] {
3257 g.add_entity(Entity::new(*id, "N")).unwrap();
3258 }
3259 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
3260 g.add_relationship(Relationship::new("b", "c", "r", 1.0)).unwrap();
3261 g.add_relationship(Relationship::new("c", "d", "r", 1.0)).unwrap();
3262 let order = g.topological_sort().unwrap();
3263 assert_eq!(order.len(), 4);
3264 let pos: std::collections::HashMap<_, _> = order.iter().enumerate().map(|(i, id)| (id.as_str().to_owned(), i)).collect();
3265 assert!(pos["a"] < pos["b"]);
3266 assert!(pos["b"] < pos["c"]);
3267 assert!(pos["c"] < pos["d"]);
3268 }
3269
3270 #[test]
3271 fn test_topological_sort_rejects_cycle() {
3272 let g = GraphStore::new();
3273 g.add_entity(Entity::new("x", "N")).unwrap();
3274 g.add_entity(Entity::new("y", "N")).unwrap();
3275 g.add_relationship(Relationship::new("x", "y", "r", 1.0)).unwrap();
3276 g.add_relationship(Relationship::new("y", "x", "r", 1.0)).unwrap();
3277 assert!(g.topological_sort().is_err());
3278 }
3279
3280 #[test]
3281 fn test_entity_count_by_label() {
3282 let g = GraphStore::new();
3283 g.add_entity(Entity::new("a", "Person")).unwrap();
3284 g.add_entity(Entity::new("b", "Person")).unwrap();
3285 g.add_entity(Entity::new("c", "Organization")).unwrap();
3286 assert_eq!(g.entity_count_by_label("Person").unwrap(), 2);
3287 assert_eq!(g.entity_count_by_label("Organization").unwrap(), 1);
3288 assert_eq!(g.entity_count_by_label("Unknown").unwrap(), 0);
3289 }
3290
3291 #[test]
3292 fn test_update_entity_label_and_property() {
3293 let g = GraphStore::new();
3294 let id = EntityId::new("e1");
3295 g.add_entity(Entity::new("e1", "Old")).unwrap();
3296 assert!(g.update_entity_label(&id, "New").unwrap());
3297 assert_eq!(g.get_entity(&id).unwrap().label, "New");
3298 assert!(g.update_entity_property(&id, "key", serde_json::json!("val")).unwrap());
3299 assert_eq!(g.get_entity(&id).unwrap().properties["key"], serde_json::json!("val"));
3300 }
3301
3302 #[test]
3303 fn test_connected_components_single_node() {
3304 let g = GraphStore::new();
3305 g.add_entity(Entity::new("a", "A")).unwrap();
3306 assert_eq!(g.connected_components().unwrap(), 1);
3307 }
3308
3309 #[test]
3310 fn test_connected_components_two_isolated_nodes() {
3311 let g = GraphStore::new();
3312 g.add_entity(Entity::new("a", "A")).unwrap();
3313 g.add_entity(Entity::new("b", "B")).unwrap();
3314 assert_eq!(g.connected_components().unwrap(), 2);
3315 }
3316
3317 #[test]
3318 fn test_connected_components_connected_pair() {
3319 let g = GraphStore::new();
3320 g.add_entity(Entity::new("a", "A")).unwrap();
3321 g.add_entity(Entity::new("b", "B")).unwrap();
3322 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3323 assert_eq!(g.connected_components().unwrap(), 1);
3324 }
3325
3326 #[test]
3327 fn test_connected_components_two_separate_pairs() {
3328 let g = GraphStore::new();
3329 g.add_entity(Entity::new("a", "A")).unwrap();
3330 g.add_entity(Entity::new("b", "B")).unwrap();
3331 g.add_entity(Entity::new("c", "C")).unwrap();
3332 g.add_entity(Entity::new("d", "D")).unwrap();
3333 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3334 g.add_relationship(Relationship::new("c", "d", "link", 1.0)).unwrap();
3335 assert_eq!(g.connected_components().unwrap(), 2);
3336 }
3337
3338 #[test]
3339 fn test_connected_components_empty_graph() {
3340 let g = GraphStore::new();
3341 assert_eq!(g.connected_components().unwrap(), 0);
3342 }
3343
3344 #[test]
3347 fn test_weakly_connected_true_for_empty_graph() {
3348 let g = GraphStore::new();
3349 assert!(g.weakly_connected().unwrap());
3350 }
3351
3352 #[test]
3353 fn test_weakly_connected_true_for_single_node() {
3354 let g = GraphStore::new();
3355 g.add_entity(Entity::new("a", "A")).unwrap();
3356 assert!(g.weakly_connected().unwrap());
3357 }
3358
3359 #[test]
3360 fn test_weakly_connected_true_when_all_nodes_connected() {
3361 let g = GraphStore::new();
3362 g.add_entity(Entity::new("a", "A")).unwrap();
3363 g.add_entity(Entity::new("b", "B")).unwrap();
3364 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3365 assert!(g.weakly_connected().unwrap());
3366 }
3367
3368 #[test]
3369 fn test_weakly_connected_false_when_nodes_isolated() {
3370 let g = GraphStore::new();
3371 g.add_entity(Entity::new("a", "A")).unwrap();
3372 g.add_entity(Entity::new("b", "B")).unwrap();
3373 assert!(!g.weakly_connected().unwrap());
3374 }
3375
3376 #[test]
3377 fn test_isolates_returns_nodes_with_no_edges() {
3378 let g = GraphStore::new();
3379 g.add_entity(Entity::new("a", "A")).unwrap();
3380 g.add_entity(Entity::new("b", "B")).unwrap();
3381 g.add_entity(Entity::new("c", "C")).unwrap();
3382 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3383 let iso = g.isolates().unwrap();
3384 let mut ids: Vec<String> = iso.iter().map(|e| e.id.as_str().to_string()).collect();
3385 ids.sort();
3386 assert_eq!(ids, vec!["c".to_string()]);
3387 }
3388
3389 #[test]
3390 fn test_isolates_returns_empty_when_all_connected() {
3391 let g = GraphStore::new();
3392 g.add_entity(Entity::new("a", "A")).unwrap();
3393 g.add_entity(Entity::new("b", "B")).unwrap();
3394 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3395 assert!(g.isolates().unwrap().is_empty());
3396 }
3397
3398 #[test]
3399 fn test_isolates_all_isolated() {
3400 let g = GraphStore::new();
3401 g.add_entity(Entity::new("x", "X")).unwrap();
3402 g.add_entity(Entity::new("y", "Y")).unwrap();
3403 let iso = g.isolates().unwrap();
3404 let mut ids: Vec<String> = iso.iter().map(|e| e.id.as_str().to_string()).collect();
3405 ids.sort();
3406 assert_eq!(ids, vec!["x".to_string(), "y".to_string()]);
3407 }
3408
3409 #[test]
3410 fn test_is_dag_on_dag() {
3411 let g = GraphStore::new();
3412 g.add_entity(Entity::new("a", "A")).unwrap();
3413 g.add_entity(Entity::new("b", "B")).unwrap();
3414 g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
3415 assert!(g.is_dag().unwrap());
3416 }
3417
3418 #[test]
3419 fn test_is_dag_on_cyclic_graph() {
3420 let g = GraphStore::new();
3421 g.add_entity(Entity::new("a", "A")).unwrap();
3422 g.add_entity(Entity::new("b", "B")).unwrap();
3423 g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
3424 g.add_relationship(Relationship::new("b", "a", "back", 1.0)).unwrap();
3425 assert!(!g.is_dag().unwrap());
3426 }
3427
3428 #[test]
3429 fn test_in_degree_and_out_degree() {
3430 let g = GraphStore::new();
3431 g.add_entity(Entity::new("a", "A")).unwrap();
3432 g.add_entity(Entity::new("b", "B")).unwrap();
3433 g.add_entity(Entity::new("c", "C")).unwrap();
3434 g.add_relationship(Relationship::new("a", "b", "e1", 1.0)).unwrap();
3435 g.add_relationship(Relationship::new("c", "b", "e2", 1.0)).unwrap();
3436 let a = EntityId::new("a");
3437 let b = EntityId::new("b");
3438 let c = EntityId::new("c");
3439 assert_eq!(g.out_degree(&a).unwrap(), 1);
3440 assert_eq!(g.in_degree(&a).unwrap(), 0);
3441 assert_eq!(g.in_degree(&b).unwrap(), 2);
3442 assert_eq!(g.out_degree(&b).unwrap(), 0);
3443 assert_eq!(g.out_degree(&c).unwrap(), 1);
3444 assert_eq!(g.in_degree(&c).unwrap(), 0);
3445 }
3446
3447 #[test]
3448 fn test_in_degree_missing_entity_returns_zero() {
3449 let g = GraphStore::new();
3450 let id = EntityId::new("ghost");
3451 assert_eq!(g.in_degree(&id).unwrap(), 0);
3452 }
3453
3454 #[test]
3455 fn test_out_degree_missing_entity_returns_zero() {
3456 let g = GraphStore::new();
3457 let id = EntityId::new("ghost");
3458 assert_eq!(g.out_degree(&id).unwrap(), 0);
3459 }
3460
3461 #[test]
3462 fn test_node_count_is_alias_for_entity_count() {
3463 let g = GraphStore::new();
3464 g.add_entity(Entity::new("a", "A")).unwrap();
3465 g.add_entity(Entity::new("b", "B")).unwrap();
3466 assert_eq!(g.node_count().unwrap(), g.entity_count().unwrap());
3467 assert_eq!(g.node_count().unwrap(), 2);
3468 }
3469
3470 #[test]
3471 fn test_edge_count_is_alias_for_relationship_count() {
3472 let g = GraphStore::new();
3473 g.add_entity(Entity::new("a", "A")).unwrap();
3474 g.add_entity(Entity::new("b", "B")).unwrap();
3475 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3476 assert_eq!(g.edge_count().unwrap(), g.relationship_count().unwrap());
3477 assert_eq!(g.edge_count().unwrap(), 1);
3478 }
3479
3480 #[test]
3481 fn test_source_nodes_returns_nodes_with_no_incoming_edges() {
3482 let g = GraphStore::new();
3483 g.add_entity(Entity::new("root", "Root")).unwrap();
3484 g.add_entity(Entity::new("child", "Child")).unwrap();
3485 g.add_entity(Entity::new("leaf", "Leaf")).unwrap();
3486 g.add_relationship(Relationship::new("root", "child", "e", 1.0)).unwrap();
3487 g.add_relationship(Relationship::new("child", "leaf", "e", 1.0)).unwrap();
3488 let sources = g.source_nodes().unwrap();
3489 assert_eq!(sources.len(), 1);
3490 assert_eq!(sources[0].id.as_str(), "root");
3491 }
3492
3493 #[test]
3494 fn test_sink_nodes_returns_nodes_with_no_outgoing_edges() {
3495 let g = GraphStore::new();
3496 g.add_entity(Entity::new("root", "Root")).unwrap();
3497 g.add_entity(Entity::new("child", "Child")).unwrap();
3498 g.add_entity(Entity::new("leaf", "Leaf")).unwrap();
3499 g.add_relationship(Relationship::new("root", "child", "e", 1.0)).unwrap();
3500 g.add_relationship(Relationship::new("child", "leaf", "e", 1.0)).unwrap();
3501 let sinks = g.sink_nodes().unwrap();
3502 assert_eq!(sinks.len(), 1);
3503 assert_eq!(sinks[0].id.as_str(), "leaf");
3504 }
3505
3506 #[test]
3507 fn test_source_and_sink_empty_on_isolated_node() {
3508 let g = GraphStore::new();
3510 g.add_entity(Entity::new("solo", "Solo")).unwrap();
3511 assert_eq!(g.source_nodes().unwrap().len(), 1);
3512 assert_eq!(g.sink_nodes().unwrap().len(), 1);
3513 }
3514
3515 #[test]
3516 fn test_reverse_flips_all_edges() {
3517 let g = GraphStore::new();
3518 g.add_entity(Entity::new("a", "A")).unwrap();
3519 g.add_entity(Entity::new("b", "B")).unwrap();
3520 g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
3521 let rev = g.reverse().unwrap();
3522 assert_eq!(rev.entity_count().unwrap(), 2);
3524 assert_eq!(rev.relationship_count().unwrap(), 1);
3525 let b_id = EntityId::new("b");
3526 let a_id = EntityId::new("a");
3527 assert!(rev.relationship_exists(&b_id, &a_id, "edge").unwrap());
3528 }
3529
3530 #[test]
3531 fn test_reverse_empty_graph_stays_empty() {
3532 let g = GraphStore::new();
3533 let rev = g.reverse().unwrap();
3534 assert_eq!(rev.entity_count().unwrap(), 0);
3535 }
3536
3537 #[test]
3538 fn test_common_neighbors_finds_shared_targets() {
3539 let g = GraphStore::new();
3540 g.add_entity(Entity::new("a", "A")).unwrap();
3541 g.add_entity(Entity::new("b", "B")).unwrap();
3542 g.add_entity(Entity::new("shared", "S")).unwrap();
3543 g.add_entity(Entity::new("only_a", "OA")).unwrap();
3544 g.add_relationship(Relationship::new("a", "shared", "e", 1.0)).unwrap();
3545 g.add_relationship(Relationship::new("b", "shared", "e", 1.0)).unwrap();
3546 g.add_relationship(Relationship::new("a", "only_a", "e", 1.0)).unwrap();
3547 let a_id = EntityId::new("a");
3548 let b_id = EntityId::new("b");
3549 let common = g.common_neighbors(&a_id, &b_id).unwrap();
3550 assert_eq!(common.len(), 1);
3551 assert_eq!(common[0].id.as_str(), "shared");
3552 }
3553
3554 #[test]
3555 fn test_common_neighbors_empty_when_none_shared() {
3556 let g = GraphStore::new();
3557 g.add_entity(Entity::new("a", "A")).unwrap();
3558 g.add_entity(Entity::new("b", "B")).unwrap();
3559 g.add_entity(Entity::new("x", "X")).unwrap();
3560 g.add_entity(Entity::new("y", "Y")).unwrap();
3561 g.add_relationship(Relationship::new("a", "x", "e", 1.0)).unwrap();
3562 g.add_relationship(Relationship::new("b", "y", "e", 1.0)).unwrap();
3563 let a_id = EntityId::new("a");
3564 let b_id = EntityId::new("b");
3565 assert!(g.common_neighbors(&a_id, &b_id).unwrap().is_empty());
3566 }
3567
3568 #[test]
3571 fn test_graph_is_empty_initially() {
3572 let g = GraphStore::new();
3573 assert!(g.is_empty().unwrap());
3574 }
3575
3576 #[test]
3577 fn test_graph_is_empty_false_after_add() {
3578 let g = GraphStore::new();
3579 g.add_entity(Entity::new("a", "A")).unwrap();
3580 assert!(!g.is_empty().unwrap());
3581 }
3582
3583 #[test]
3584 fn test_graph_entity_ids_returns_all_ids() {
3585 let g = GraphStore::new();
3586 g.add_entity(Entity::new("x", "X")).unwrap();
3587 g.add_entity(Entity::new("y", "Y")).unwrap();
3588 let ids = g.entity_ids().unwrap();
3589 assert_eq!(ids.len(), 2);
3590 assert!(ids.iter().any(|id| id.0 == "x"));
3591 assert!(ids.iter().any(|id| id.0 == "y"));
3592 }
3593
3594 #[test]
3595 fn test_graph_clear_removes_entities_and_relationships() {
3596 let g = GraphStore::new();
3597 g.add_entity(Entity::new("a", "A")).unwrap();
3598 g.add_entity(Entity::new("b", "B")).unwrap();
3599 g.add_relationship(Relationship::new("a", "b", "links", 1.0))
3600 .unwrap();
3601 g.clear().unwrap();
3602 assert_eq!(g.entity_count().unwrap(), 0);
3603 assert_eq!(g.relationship_count().unwrap(), 0);
3604 assert!(g.is_empty().unwrap());
3605 }
3606
3607 #[test]
3610 fn test_weight_of_returns_edge_weight() {
3611 let g = make_graph();
3612 add(&g, "x"); add(&g, "y");
3613 link_w(&g, "x", "y", 3.5);
3614 let w = g.weight_of(&EntityId::new("x"), &EntityId::new("y")).unwrap();
3615 assert!(w.is_some());
3616 assert!((w.unwrap() - 3.5).abs() < 1e-6);
3617 }
3618
3619 #[test]
3620 fn test_weight_of_absent_edge_returns_none() {
3621 let g = make_graph();
3622 add(&g, "a"); add(&g, "b");
3623 let w = g.weight_of(&EntityId::new("a"), &EntityId::new("b")).unwrap();
3624 assert!(w.is_none());
3625 }
3626
3627 #[test]
3628 fn test_neighbors_in_returns_predecessors() {
3629 let g = make_graph();
3630 add(&g, "a"); add(&g, "b"); add(&g, "c");
3631 link(&g, "a", "c"); link(&g, "b", "c");
3632 let mut preds: Vec<String> = g
3633 .neighbors_in(&EntityId::new("c"))
3634 .unwrap()
3635 .into_iter()
3636 .map(|id| id.as_str().to_string())
3637 .collect();
3638 preds.sort();
3639 assert_eq!(preds, vec!["a", "b"]);
3640 }
3641
3642 #[test]
3643 fn test_neighbors_in_empty_for_node_with_no_incoming() {
3644 let g = make_graph();
3645 add(&g, "isolated");
3646 let preds = g.neighbors_in(&EntityId::new("isolated")).unwrap();
3647 assert!(preds.is_empty());
3648 }
3649
3650 #[test]
3651 fn test_path_exists_reachable() {
3652 let g = make_graph();
3653 add(&g, "s"); add(&g, "m"); add(&g, "t");
3654 link(&g, "s", "m"); link(&g, "m", "t");
3655 assert!(g.path_exists("s", "t").unwrap());
3656 }
3657
3658 #[test]
3659 fn test_path_exists_unreachable() {
3660 let g = make_graph();
3661 add(&g, "a"); add(&g, "b");
3662 assert!(!g.path_exists("a", "b").unwrap());
3663 }
3664
3665 #[test]
3668 fn test_neighbor_ids_returns_direct_successors() {
3669 let g = make_graph();
3670 add(&g, "src"); add(&g, "dst1"); add(&g, "dst2");
3671 link(&g, "src", "dst1"); link(&g, "src", "dst2");
3672 let mut ids = g.neighbor_ids(&EntityId::new("src")).unwrap();
3673 ids.sort_by_key(|id| id.0.clone());
3674 assert_eq!(ids, vec![EntityId::new("dst1"), EntityId::new("dst2")]);
3675 }
3676
3677 #[test]
3678 fn test_neighbor_ids_empty_for_isolated_node() {
3679 let g = make_graph();
3680 add(&g, "isolated");
3681 let ids = g.neighbor_ids(&EntityId::new("isolated")).unwrap();
3682 assert!(ids.is_empty());
3683 }
3684
3685 #[test]
3688 fn test_density_zero_for_empty_graph() {
3689 let g = make_graph();
3690 assert_eq!(g.density().unwrap(), 0.0);
3691 }
3692
3693 #[test]
3694 fn test_density_zero_for_single_node() {
3695 let g = make_graph();
3696 add(&g, "solo");
3697 assert_eq!(g.density().unwrap(), 0.0);
3698 }
3699
3700 #[test]
3701 fn test_density_one_for_complete_directed_graph() {
3702 let g = make_graph();
3704 add(&g, "a"); add(&g, "b");
3705 link(&g, "a", "b"); link(&g, "b", "a");
3706 assert!((g.density().unwrap() - 1.0).abs() < 1e-9);
3707 }
3708
3709 #[test]
3710 fn test_density_partial() {
3711 let g = make_graph();
3713 add(&g, "a"); add(&g, "b"); add(&g, "c");
3714 link(&g, "a", "b");
3715 let d = g.density().unwrap();
3716 assert!((d - 1.0/6.0).abs() < 1e-9);
3717 }
3718
3719 #[test]
3720 fn test_all_entities_returns_all_nodes() {
3721 let g = make_graph();
3722 add(&g, "x"); add(&g, "y"); add(&g, "z");
3723 assert_eq!(g.all_entities().unwrap().len(), 3);
3724 }
3725
3726 #[test]
3727 fn test_all_relationships_returns_all_edges() {
3728 let g = make_graph();
3729 add(&g, "a"); add(&g, "b"); add(&g, "c");
3730 link(&g, "a", "b"); link(&g, "b", "c");
3731 assert_eq!(g.all_relationships().unwrap().len(), 2);
3732 }
3733
3734 #[test]
3735 fn test_find_entities_by_label_returns_matches() {
3736 let g = make_graph();
3737 g.add_entity(Entity::new("n1", "Person")).unwrap();
3738 g.add_entity(Entity::new("n2", "Person")).unwrap();
3739 g.add_entity(Entity::new("n3", "Car")).unwrap();
3740 let people = g.find_entities_by_label("Person").unwrap();
3741 assert_eq!(people.len(), 2);
3742 }
3743
3744 #[test]
3745 fn test_bfs_bounded_limits_depth() {
3746 let g = make_graph();
3748 add(&g, "a"); add(&g, "b"); add(&g, "c"); add(&g, "d");
3749 link(&g, "a", "b"); link(&g, "b", "c"); link(&g, "c", "d");
3750 let visited = g.bfs_bounded("a", 2, 100).unwrap();
3751 assert!(visited.contains(&EntityId::new("a")));
3752 assert!(visited.contains(&EntityId::new("b")));
3753 assert!(visited.contains(&EntityId::new("c")));
3754 assert!(!visited.contains(&EntityId::new("d")));
3755 }
3756
3757 #[test]
3760 fn test_avg_degree_zero_for_empty_graph() {
3761 let g = make_graph();
3762 assert_eq!(g.avg_degree().unwrap(), 0.0);
3763 }
3764
3765 #[test]
3766 fn test_avg_degree_correct_value() {
3767 let g = make_graph();
3769 add(&g, "a"); add(&g, "b"); add(&g, "c");
3770 link(&g, "a", "b"); link(&g, "a", "c");
3771 let d = g.avg_degree().unwrap();
3772 assert!((d - 2.0/3.0).abs() < 1e-9);
3773 }
3774
3775 #[test]
3778 fn test_total_weight_zero_for_empty_graph() {
3779 let g = make_graph();
3780 assert_eq!(g.total_weight().unwrap(), 0.0);
3781 }
3782
3783 #[test]
3784 fn test_total_weight_sums_all_edges() {
3785 let g = make_graph();
3786 add(&g, "a"); add(&g, "b"); add(&g, "c");
3787 link_w(&g, "a", "b", 2.0); link_w(&g, "b", "c", 3.5);
3788 assert!((g.total_weight().unwrap() - 5.5).abs() < 1e-6);
3789 }
3790
3791 #[test]
3792 fn test_max_edge_weight_none_for_empty() {
3793 let g = make_graph();
3794 assert!(g.max_edge_weight().unwrap().is_none());
3795 }
3796
3797 #[test]
3798 fn test_max_edge_weight_returns_largest() {
3799 let g = make_graph();
3800 add(&g, "a"); add(&g, "b"); add(&g, "c");
3801 link_w(&g, "a", "b", 1.0); link_w(&g, "a", "c", 9.5);
3802 assert!((g.max_edge_weight().unwrap().unwrap() - 9.5).abs() < 1e-6);
3803 }
3804
3805 #[test]
3806 fn test_min_edge_weight_returns_smallest() {
3807 let g = make_graph();
3808 add(&g, "a"); add(&g, "b"); add(&g, "c");
3809 link_w(&g, "a", "b", 1.0); link_w(&g, "a", "c", 9.5);
3810 assert!((g.min_edge_weight().unwrap().unwrap() - 1.0).abs() < 1e-6);
3811 }
3812
3813 #[test]
3816 fn test_max_out_degree_entity_returns_node_with_most_edges() {
3817 let g = make_graph();
3818 add(&g, "hub"); add(&g, "a"); add(&g, "b"); add(&g, "leaf");
3819 link(&g, "hub", "a"); link(&g, "hub", "b"); link(&g, "a", "leaf");
3820 let best = g.max_out_degree_entity().unwrap().unwrap();
3821 assert_eq!(best.id, EntityId::new("hub"));
3822 }
3823
3824 #[test]
3825 fn test_max_out_degree_entity_none_for_empty_graph() {
3826 let g = make_graph();
3827 assert!(g.max_out_degree_entity().unwrap().is_none());
3828 }
3829
3830 #[test]
3831 fn test_leaf_nodes_returns_nodes_with_no_outgoing_edges() {
3832 let g = make_graph();
3833 add(&g, "root"); add(&g, "mid"); add(&g, "leaf1"); add(&g, "leaf2");
3834 link(&g, "root", "mid"); link(&g, "mid", "leaf1"); link(&g, "mid", "leaf2");
3835 let mut leaf_ids: Vec<String> = g
3836 .leaf_nodes()
3837 .unwrap()
3838 .into_iter()
3839 .map(|e| e.id.0.clone())
3840 .collect();
3841 leaf_ids.sort();
3842 assert_eq!(leaf_ids, vec!["leaf1", "leaf2"]);
3843 }
3844
3845 #[test]
3846 fn test_leaf_nodes_all_are_leaves_with_no_edges() {
3847 let g = make_graph();
3848 add(&g, "a"); add(&g, "b");
3849 assert_eq!(g.leaf_nodes().unwrap().len(), 2);
3850 }
3851
3852 #[test]
3855 fn test_top_n_by_out_degree_returns_descending() {
3856 let g = make_graph();
3857 add(&g, "hub"); add(&g, "mid"); add(&g, "tip"); add(&g, "leaf");
3858 link(&g, "hub", "mid"); link(&g, "hub", "tip"); link(&g, "hub", "leaf");
3859 link(&g, "mid", "leaf");
3860 let top2 = g.top_n_by_out_degree(2).unwrap();
3861 assert_eq!(top2.len(), 2);
3862 assert_eq!(top2[0].id, EntityId::new("hub"));
3863 }
3864
3865 #[test]
3866 fn test_top_n_by_out_degree_zero_returns_empty() {
3867 let g = make_graph();
3868 add(&g, "x");
3869 assert!(g.top_n_by_out_degree(0).unwrap().is_empty());
3870 }
3871
3872 #[test]
3873 fn test_remove_entity_and_edges_removes_node_and_incident_edges() {
3874 let g = make_graph();
3875 add(&g, "a"); add(&g, "b"); add(&g, "c");
3876 link(&g, "a", "b"); link(&g, "b", "c");
3877 g.remove_entity_and_edges(&EntityId::new("b")).unwrap();
3878 assert_eq!(g.entity_count().unwrap(), 2);
3879 assert_eq!(g.relationship_count().unwrap(), 0);
3880 }
3881
3882 #[test]
3883 fn test_remove_entity_and_edges_errors_for_unknown_id() {
3884 let g = make_graph();
3885 let result = g.remove_entity_and_edges(&EntityId::new("ghost"));
3886 assert!(result.is_err());
3887 }
3888
3889 #[test]
3892 fn test_relationship_kinds_returns_sorted_distinct_kinds() {
3893 let g = make_graph();
3894 add(&g, "a"); add(&g, "b"); add(&g, "c");
3895 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
3896 g.add_relationship(Relationship::new("b", "c", "LIKES", 1.0)).unwrap();
3897 g.add_relationship(Relationship::new("a", "c", "KNOWS", 1.0)).unwrap();
3898 let kinds = g.relationship_kinds().unwrap();
3899 assert_eq!(kinds, vec!["KNOWS", "LIKES"]);
3900 }
3901
3902 #[test]
3903 fn test_relationship_kinds_empty_graph_returns_empty() {
3904 let g = make_graph();
3905 assert!(g.relationship_kinds().unwrap().is_empty());
3906 }
3907
3908 #[test]
3909 fn test_graph_density_zero_for_empty() {
3910 let g = make_graph();
3911 assert_eq!(g.graph_density().unwrap(), 0.0);
3912 }
3913
3914 #[test]
3915 fn test_graph_density_correct_for_partial_graph() {
3916 let g = make_graph();
3917 add(&g, "a"); add(&g, "b"); add(&g, "c");
3918 link(&g, "a", "b");
3920 let d = g.graph_density().unwrap();
3921 assert!((d - 1.0 / 6.0).abs() < 1e-9);
3922 }
3923
3924 #[test]
3925 fn test_entity_id_try_new_rejects_empty() {
3926 let result = EntityId::try_new("");
3927 assert!(result.is_err());
3928 }
3929
3930 #[test]
3931 fn test_entity_id_try_new_accepts_nonempty() {
3932 let id = EntityId::try_new("valid").unwrap();
3933 assert_eq!(id.as_str(), "valid");
3934 }
3935
3936 #[test]
3939 fn test_hub_nodes_returns_nodes_meeting_threshold() {
3940 let g = make_graph();
3941 add(&g, "hub"); add(&g, "mid"); add(&g, "leaf");
3942 link(&g, "hub", "mid"); link(&g, "hub", "leaf"); link(&g, "mid", "leaf");
3943 let hubs = g.hub_nodes(2).unwrap();
3944 assert_eq!(hubs.len(), 1);
3945 assert_eq!(hubs[0].id, EntityId::new("hub"));
3946 }
3947
3948 #[test]
3949 fn test_hub_nodes_threshold_zero_returns_all() {
3950 let g = make_graph();
3951 add(&g, "a"); add(&g, "b");
3952 assert_eq!(g.hub_nodes(0).unwrap().len(), 2);
3953 }
3954
3955 #[test]
3956 fn test_incident_relationships_includes_outgoing_and_incoming() {
3957 let g = make_graph();
3958 add(&g, "a"); add(&g, "b"); add(&g, "c");
3959 link(&g, "a", "b"); link(&g, "c", "b");
3960 let rels = g.incident_relationships(&EntityId::new("b")).unwrap();
3961 assert_eq!(rels.len(), 2);
3962 }
3963
3964 #[test]
3965 fn test_incident_relationships_empty_for_isolated_node() {
3966 let g = make_graph();
3967 add(&g, "iso");
3968 assert!(g.incident_relationships(&EntityId::new("iso")).unwrap().is_empty());
3969 }
3970
3971 #[test]
3974 fn test_average_out_degree_empty_graph_is_zero() {
3975 let g = GraphStore::new();
3976 assert!((g.average_out_degree().unwrap() - 0.0).abs() < 1e-9);
3977 }
3978
3979 #[test]
3980 fn test_average_out_degree_two_nodes_one_edge() {
3981 let g = GraphStore::new();
3982 g.add_entity(Entity::new("a", "A")).unwrap();
3983 g.add_entity(Entity::new("b", "B")).unwrap();
3984 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
3985 assert!((g.average_out_degree().unwrap() - 0.5).abs() < 1e-9);
3987 }
3988
3989 #[test]
3990 fn test_in_degree_for_counts_incoming_edges() {
3991 let g = GraphStore::new();
3992 g.add_entity(Entity::new("a", "A")).unwrap();
3993 g.add_entity(Entity::new("b", "B")).unwrap();
3994 g.add_entity(Entity::new("c", "C")).unwrap();
3995 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
3996 g.add_relationship(Relationship::new("c", "b", "r", 1.0)).unwrap();
3997 assert_eq!(g.in_degree_for(&EntityId::new("b")).unwrap(), 2);
3998 assert_eq!(g.in_degree_for(&EntityId::new("a")).unwrap(), 0);
3999 }
4000
4001 #[test]
4002 fn test_in_degree_for_returns_zero_for_unknown_entity() {
4003 let g = GraphStore::new();
4004 assert_eq!(g.in_degree_for(&EntityId::new("ghost")).unwrap(), 0);
4005 }
4006
4007 #[test]
4010 fn test_entity_id_is_empty_false_for_nonempty() {
4011 let id = EntityId::new("node-1");
4012 assert!(!id.is_empty());
4013 }
4014
4015 #[test]
4016 fn test_entity_id_starts_with_matches_prefix() {
4017 let id = EntityId::new("concept-42");
4018 assert!(id.starts_with("concept-"));
4019 assert!(!id.starts_with("entity-"));
4020 }
4021
4022 #[test]
4023 fn test_entity_id_starts_with_empty_always_true() {
4024 let id = EntityId::new("anything");
4025 assert!(id.starts_with(""));
4026 }
4027
4028 #[test]
4029 fn test_entity_has_property_returns_true_when_present() {
4030 let e = Entity::new("e", "Node")
4031 .with_property("color", serde_json::json!("blue"));
4032 assert!(e.has_property("color"));
4033 assert!(!e.has_property("size"));
4034 }
4035
4036 #[test]
4037 fn test_entity_has_property_false_when_no_properties() {
4038 let e = Entity::new("e", "Node");
4039 assert!(!e.has_property("any"));
4040 }
4041
4042 #[test]
4043 fn test_entity_labels_returns_distinct_sorted_labels() {
4044 let g = make_graph();
4045 g.add_entity(Entity::new("a", "Person")).unwrap();
4046 g.add_entity(Entity::new("b", "Concept")).unwrap();
4047 g.add_entity(Entity::new("c", "Person")).unwrap();
4048 let labels = g.entity_labels().unwrap();
4049 assert_eq!(labels, vec!["Concept", "Person"]);
4050 }
4051
4052 #[test]
4053 fn test_entity_labels_empty_for_empty_graph() {
4054 let g = make_graph();
4055 assert!(g.entity_labels().unwrap().is_empty());
4056 }
4057
4058 #[test]
4061 fn test_out_degree_for_returns_outgoing_edge_count() {
4062 let g = GraphStore::new();
4063 g.add_entity(Entity::new("a", "A")).unwrap();
4064 g.add_entity(Entity::new("b", "B")).unwrap();
4065 g.add_entity(Entity::new("c", "C")).unwrap();
4066 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4067 g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
4068 assert_eq!(g.out_degree_for(&EntityId::new("a")).unwrap(), 2);
4069 assert_eq!(g.out_degree_for(&EntityId::new("b")).unwrap(), 0);
4070 }
4071
4072 #[test]
4073 fn test_out_degree_for_returns_zero_for_unknown_entity() {
4074 let g = GraphStore::new();
4075 assert_eq!(g.out_degree_for(&EntityId::new("ghost")).unwrap(), 0);
4076 }
4077
4078 #[test]
4079 fn test_predecessors_returns_nodes_with_incoming_edges() {
4080 let g = GraphStore::new();
4081 g.add_entity(Entity::new("a", "A")).unwrap();
4082 g.add_entity(Entity::new("b", "B")).unwrap();
4083 g.add_entity(Entity::new("c", "C")).unwrap();
4084 g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
4085 g.add_relationship(Relationship::new("b", "c", "r", 1.0)).unwrap();
4086 let mut preds: Vec<String> = g
4087 .predecessors(&EntityId::new("c"))
4088 .unwrap()
4089 .iter()
4090 .map(|e| e.id.as_str().to_string())
4091 .collect();
4092 preds.sort();
4093 assert_eq!(preds, vec!["a", "b"]);
4094 }
4095
4096 #[test]
4097 fn test_predecessors_empty_for_source_node() {
4098 let g = GraphStore::new();
4099 g.add_entity(Entity::new("root", "Root")).unwrap();
4100 g.add_entity(Entity::new("child", "Child")).unwrap();
4101 g.add_relationship(Relationship::new("root", "child", "r", 1.0)).unwrap();
4102 assert!(g.predecessors(&EntityId::new("root")).unwrap().is_empty());
4103 }
4104
4105 #[test]
4106 fn test_is_source_true_for_node_with_no_incoming_edges() {
4107 let g = GraphStore::new();
4108 g.add_entity(Entity::new("src", "Src")).unwrap();
4109 g.add_entity(Entity::new("dst", "Dst")).unwrap();
4110 g.add_relationship(Relationship::new("src", "dst", "r", 1.0)).unwrap();
4111 assert!(g.is_source(&EntityId::new("src")).unwrap());
4112 assert!(!g.is_source(&EntityId::new("dst")).unwrap());
4113 }
4114
4115 #[test]
4118 fn test_relationship_is_self_loop_true_when_from_equals_to() {
4119 let r = Relationship::new("a", "a", "self", 1.0);
4120 assert!(r.is_self_loop());
4121 }
4122
4123 #[test]
4124 fn test_relationship_is_self_loop_false_for_normal_edge() {
4125 let r = Relationship::new("a", "b", "edge", 1.0);
4126 assert!(!r.is_self_loop());
4127 }
4128
4129 #[test]
4130 fn test_relationship_reversed_swaps_endpoints() {
4131 let r = Relationship::new("from", "to", "knows", 0.5);
4132 let rev = r.reversed();
4133 assert_eq!(rev.from.as_str(), "to");
4134 assert_eq!(rev.to.as_str(), "from");
4135 assert_eq!(rev.kind, "knows");
4136 assert!((rev.weight - 0.5).abs() < 1e-6);
4137 }
4138
4139 #[test]
4140 fn test_find_entities_by_labels_returns_matching() {
4141 let g = make_graph();
4142 g.add_entity(Entity::new("p1", "Person")).unwrap();
4143 g.add_entity(Entity::new("p2", "Person")).unwrap();
4144 g.add_entity(Entity::new("c1", "Concept")).unwrap();
4145 let results = g.find_entities_by_labels(&["Person"]).unwrap();
4146 assert_eq!(results.len(), 2);
4147 assert!(results.iter().all(|e| e.label == "Person"));
4148 }
4149
4150 #[test]
4151 fn test_find_entities_by_labels_empty_when_no_match() {
4152 let g = make_graph();
4153 g.add_entity(Entity::new("n1", "Node")).unwrap();
4154 let results = g.find_entities_by_labels(&["Missing"]).unwrap();
4155 assert!(results.is_empty());
4156 }
4157
4158 #[test]
4159 fn test_remove_isolated_removes_nodes_without_edges() {
4160 let g = make_graph();
4161 g.add_entity(Entity::new("connected", "N")).unwrap();
4162 g.add_entity(Entity::new("isolated", "N")).unwrap();
4163 g.add_entity(Entity::new("other", "N")).unwrap();
4164 g.add_relationship(Relationship::new("connected", "other", "r", 1.0)).unwrap();
4165 let removed = g.remove_isolated().unwrap();
4166 assert_eq!(removed, 1);
4167 assert!(g.get_entity(&EntityId::new("isolated")).is_err());
4168 assert!(g.get_entity(&EntityId::new("connected")).is_ok());
4169 }
4170
4171 #[test]
4172 fn test_remove_isolated_zero_when_all_connected() {
4173 let g = make_graph();
4174 g.add_entity(Entity::new("a", "N")).unwrap();
4175 g.add_entity(Entity::new("b", "N")).unwrap();
4176 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4177 assert_eq!(g.remove_isolated().unwrap(), 0);
4178 }
4179
4180 #[test]
4183 fn test_successors_returns_direct_out_neighbors() {
4184 let g = GraphStore::new();
4185 g.add_entity(Entity::new("a", "A")).unwrap();
4186 g.add_entity(Entity::new("b", "B")).unwrap();
4187 g.add_entity(Entity::new("c", "C")).unwrap();
4188 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4189 g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
4190 let mut ids: Vec<String> = g
4191 .successors(&EntityId::new("a"))
4192 .unwrap()
4193 .iter()
4194 .map(|e| e.id.as_str().to_string())
4195 .collect();
4196 ids.sort();
4197 assert_eq!(ids, vec!["b", "c"]);
4198 }
4199
4200 #[test]
4201 fn test_successors_empty_for_sink_node() {
4202 let g = GraphStore::new();
4203 g.add_entity(Entity::new("leaf", "L")).unwrap();
4204 assert!(g.successors(&EntityId::new("leaf")).unwrap().is_empty());
4205 }
4206
4207 #[test]
4208 fn test_is_sink_true_for_node_with_no_outgoing_edges() {
4209 let g = GraphStore::new();
4210 g.add_entity(Entity::new("a", "A")).unwrap();
4211 g.add_entity(Entity::new("b", "B")).unwrap();
4212 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4213 assert!(!g.is_sink(&EntityId::new("a")).unwrap());
4214 assert!(g.is_sink(&EntityId::new("b")).unwrap());
4215 }
4216
4217 #[test]
4218 fn test_is_sink_true_for_unknown_entity() {
4219 let g = GraphStore::new();
4220 assert!(g.is_sink(&EntityId::new("ghost")).unwrap());
4221 }
4222
4223 #[test]
4226 fn test_reachable_from_returns_all_downstream_nodes() {
4227 let g = GraphStore::new();
4228 g.add_entity(Entity::new("a", "N")).unwrap();
4229 g.add_entity(Entity::new("b", "N")).unwrap();
4230 g.add_entity(Entity::new("c", "N")).unwrap();
4231 g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
4232 g.add_relationship(Relationship::new("b", "c", "edge", 1.0)).unwrap();
4233 let reachable = g.reachable_from(&EntityId::new("a")).unwrap();
4234 assert!(reachable.contains(&EntityId::new("b")));
4235 assert!(reachable.contains(&EntityId::new("c")));
4236 assert!(!reachable.contains(&EntityId::new("a")));
4237 }
4238
4239 #[test]
4240 fn test_reachable_from_empty_for_sink_node() {
4241 let g = GraphStore::new();
4242 g.add_entity(Entity::new("sink", "N")).unwrap();
4243 let reachable = g.reachable_from(&EntityId::new("sink")).unwrap();
4244 assert!(reachable.is_empty());
4245 }
4246
4247 #[test]
4248 fn test_reachable_from_empty_for_unknown_node() {
4249 let g = GraphStore::new();
4250 let reachable = g.reachable_from(&EntityId::new("ghost")).unwrap();
4251 assert!(reachable.is_empty());
4252 }
4253
4254 #[test]
4255 fn test_contains_cycle_false_for_dag() {
4256 let g = GraphStore::new();
4257 g.add_entity(Entity::new("a", "N")).unwrap();
4258 g.add_entity(Entity::new("b", "N")).unwrap();
4259 g.add_entity(Entity::new("c", "N")).unwrap();
4260 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4261 g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
4262 assert!(!g.contains_cycle().unwrap());
4263 }
4264
4265 #[test]
4266 fn test_contains_cycle_true_for_cyclic_graph() {
4267 let g = GraphStore::new();
4268 g.add_entity(Entity::new("x", "N")).unwrap();
4269 g.add_entity(Entity::new("y", "N")).unwrap();
4270 g.add_relationship(Relationship::new("x", "y", "e", 1.0)).unwrap();
4271 g.add_relationship(Relationship::new("y", "x", "e", 1.0)).unwrap();
4272 assert!(g.contains_cycle().unwrap());
4273 }
4274
4275 #[test]
4276 fn test_contains_cycle_false_for_empty_graph() {
4277 let g = GraphStore::new();
4278 assert!(!g.contains_cycle().unwrap());
4279 }
4280
4281 #[test]
4284 fn test_is_acyclic_true_for_dag() {
4285 let g = GraphStore::new();
4286 g.add_entity(Entity::new("a", "N")).unwrap();
4287 g.add_entity(Entity::new("b", "N")).unwrap();
4288 g.add_entity(Entity::new("c", "N")).unwrap();
4289 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4290 g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
4291 assert!(g.is_acyclic().unwrap());
4292 }
4293
4294 #[test]
4295 fn test_is_acyclic_false_for_cyclic_graph() {
4296 let g = GraphStore::new();
4297 g.add_entity(Entity::new("x", "N")).unwrap();
4298 g.add_entity(Entity::new("y", "N")).unwrap();
4299 g.add_relationship(Relationship::new("x", "y", "e", 1.0)).unwrap();
4300 g.add_relationship(Relationship::new("y", "x", "e", 1.0)).unwrap();
4301 assert!(!g.is_acyclic().unwrap());
4302 }
4303
4304 #[test]
4305 fn test_is_acyclic_true_for_empty_graph() {
4306 let g = GraphStore::new();
4307 assert!(g.is_acyclic().unwrap());
4308 }
4309
4310 #[test]
4313 fn test_count_relationships_by_kind_returns_correct_count() {
4314 let g = make_graph();
4315 g.add_entity(Entity::new("a", "N")).unwrap();
4316 g.add_entity(Entity::new("b", "N")).unwrap();
4317 g.add_entity(Entity::new("c", "N")).unwrap();
4318 g.add_relationship(Relationship::new("a", "b", "knows", 1.0)).unwrap();
4319 g.add_relationship(Relationship::new("b", "c", "knows", 1.0)).unwrap();
4320 g.add_relationship(Relationship::new("a", "c", "likes", 0.5)).unwrap();
4321 assert_eq!(g.count_relationships_by_kind("knows").unwrap(), 2);
4322 assert_eq!(g.count_relationships_by_kind("likes").unwrap(), 1);
4323 assert_eq!(g.count_relationships_by_kind("absent").unwrap(), 0);
4324 }
4325
4326 #[test]
4327 fn test_merge_imports_entities_and_relationships() {
4328 let g1 = make_graph();
4329 g1.add_entity(Entity::new("a", "N")).unwrap();
4330 g1.add_entity(Entity::new("b", "N")).unwrap();
4331 g1.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4332
4333 let g2 = make_graph();
4334 g2.add_entity(Entity::new("c", "N")).unwrap();
4335 g2.add_entity(Entity::new("a", "N")).unwrap(); g2.add_relationship(Relationship::new("c", "a", "s", 0.5)).unwrap();
4337
4338 g1.merge(&g2).unwrap();
4339 assert_eq!(g1.entity_count().unwrap(), 3); assert_eq!(g1.relationship_count().unwrap(), 2); }
4342
4343 #[test]
4344 fn test_top_nodes_by_in_degree_returns_sinks() {
4345 let g = make_graph();
4346 g.add_entity(Entity::new("hub", "N")).unwrap();
4347 g.add_entity(Entity::new("src1", "N")).unwrap();
4348 g.add_entity(Entity::new("src2", "N")).unwrap();
4349 g.add_relationship(Relationship::new("src1", "hub", "r", 1.0)).unwrap();
4350 g.add_relationship(Relationship::new("src2", "hub", "r", 1.0)).unwrap();
4351 let top = g.top_nodes_by_in_degree(1).unwrap();
4352 assert_eq!(top.len(), 1);
4353 assert_eq!(top[0].id.as_str(), "hub");
4354 }
4355
4356 #[test]
4357 fn test_top_nodes_by_out_degree_returns_sources() {
4358 let g = make_graph();
4359 g.add_entity(Entity::new("src", "N")).unwrap();
4360 g.add_entity(Entity::new("a", "N")).unwrap();
4361 g.add_entity(Entity::new("b", "N")).unwrap();
4362 g.add_relationship(Relationship::new("src", "a", "r", 1.0)).unwrap();
4363 g.add_relationship(Relationship::new("src", "b", "r", 1.0)).unwrap();
4364 let top = g.top_nodes_by_out_degree(1).unwrap();
4365 assert_eq!(top.len(), 1);
4366 assert_eq!(top[0].id.as_str(), "src");
4367 }
4368
4369 #[test]
4372 fn test_entity_property_value_returns_value() {
4373 let e = Entity::new("n1", "Node")
4374 .with_property("age", serde_json::Value::Number(42.into()));
4375 let val = e.property_value("age");
4376 assert!(val.is_some());
4377 assert_eq!(val.unwrap(), &serde_json::Value::Number(42.into()));
4378 }
4379
4380 #[test]
4381 fn test_entity_property_value_missing_returns_none() {
4382 let e = Entity::new("n1", "Node");
4383 assert!(e.property_value("missing").is_none());
4384 }
4385
4386 #[test]
4387 fn test_find_relationships_by_kind_returns_matching() {
4388 let g = GraphStore::new();
4389 g.add_entity(Entity::new("a", "N")).unwrap();
4390 g.add_entity(Entity::new("b", "N")).unwrap();
4391 g.add_entity(Entity::new("c", "N")).unwrap();
4392 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
4393 g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
4394 g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
4395 let rels = g.find_relationships_by_kind("KNOWS").unwrap();
4396 assert_eq!(rels.len(), 2);
4397 assert!(rels.iter().all(|r| r.kind == "KNOWS"));
4398 }
4399
4400 #[test]
4401 fn test_find_relationships_by_kind_no_match_returns_empty() {
4402 let g = GraphStore::new();
4403 g.add_entity(Entity::new("a", "N")).unwrap();
4404 g.add_entity(Entity::new("b", "N")).unwrap();
4405 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
4406 let rels = g.find_relationships_by_kind("HATES").unwrap();
4407 assert!(rels.is_empty());
4408 }
4409
4410 #[test]
4411 fn test_count_relationships_by_kind_correct() {
4412 let g = GraphStore::new();
4413 g.add_entity(Entity::new("a", "N")).unwrap();
4414 g.add_entity(Entity::new("b", "N")).unwrap();
4415 g.add_entity(Entity::new("c", "N")).unwrap();
4416 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
4417 g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
4418 g.add_relationship(Relationship::new("a", "c", "PART_OF", 1.0)).unwrap();
4419 assert_eq!(g.count_relationships_by_kind("KNOWS").unwrap(), 2);
4420 assert_eq!(g.count_relationships_by_kind("PART_OF").unwrap(), 1);
4421 assert_eq!(g.count_relationships_by_kind("MISSING").unwrap(), 0);
4422 }
4423
4424 #[test]
4427 fn test_max_out_degree_returns_highest_out_degree() {
4428 let g = GraphStore::new();
4429 g.add_entity(Entity::new("a", "N")).unwrap();
4430 g.add_entity(Entity::new("b", "N")).unwrap();
4431 g.add_entity(Entity::new("c", "N")).unwrap();
4432 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4435 g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
4436 g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
4437 assert_eq!(g.max_out_degree().unwrap(), 2);
4438 }
4439
4440 #[test]
4441 fn test_max_out_degree_zero_for_empty_graph() {
4442 let g = GraphStore::new();
4443 assert_eq!(g.max_out_degree().unwrap(), 0);
4444 }
4445
4446 #[test]
4447 fn test_max_in_degree_returns_highest_in_degree() {
4448 let g = GraphStore::new();
4449 g.add_entity(Entity::new("a", "N")).unwrap();
4450 g.add_entity(Entity::new("b", "N")).unwrap();
4451 g.add_entity(Entity::new("c", "N")).unwrap();
4452 g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
4454 g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
4455 assert_eq!(g.max_in_degree().unwrap(), 2);
4456 }
4457
4458 #[test]
4459 fn test_max_in_degree_zero_for_empty_graph() {
4460 let g = GraphStore::new();
4461 assert_eq!(g.max_in_degree().unwrap(), 0);
4462 }
4463
4464 #[test]
4467 fn test_sum_edge_weights_correct_sum() {
4468 let g = GraphStore::new();
4469 g.add_entity(Entity::new("a", "N")).unwrap();
4470 g.add_entity(Entity::new("b", "N")).unwrap();
4471 g.add_entity(Entity::new("c", "N")).unwrap();
4472 g.add_relationship(Relationship::new("a", "b", "e", 1.5)).unwrap();
4473 g.add_relationship(Relationship::new("b", "c", "e", 2.5)).unwrap();
4474 let total = g.sum_edge_weights().unwrap();
4475 assert!((total - 4.0).abs() < 1e-9);
4476 }
4477
4478 #[test]
4479 fn test_sum_edge_weights_zero_for_empty_graph() {
4480 let g = GraphStore::new();
4481 assert!((g.sum_edge_weights().unwrap() - 0.0).abs() < 1e-9);
4482 }
4483
4484 #[test]
4487 fn test_weight_stats_none_for_empty_graph() {
4488 let g = GraphStore::new();
4489 assert!(g.weight_stats().unwrap().is_none());
4490 }
4491
4492 #[test]
4493 fn test_weight_stats_returns_correct_min_max_mean() {
4494 let g = GraphStore::new();
4495 g.add_entity(Entity::new("a", "N")).unwrap();
4496 g.add_entity(Entity::new("b", "N")).unwrap();
4497 g.add_entity(Entity::new("c", "N")).unwrap();
4498 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4499 g.add_relationship(Relationship::new("b", "c", "e", 3.0)).unwrap();
4500 let (min, max, mean) = g.weight_stats().unwrap().unwrap();
4501 assert!((min - 1.0).abs() < 1e-9);
4502 assert!((max - 3.0).abs() < 1e-9);
4503 assert!((mean - 2.0).abs() < 1e-9);
4504 }
4505
4506 #[test]
4509 fn test_isolated_nodes_empty_graph_returns_empty_set() {
4510 let g = GraphStore::new();
4511 assert!(g.isolated_nodes().unwrap().is_empty());
4512 }
4513
4514 #[test]
4515 fn test_isolated_nodes_all_connected_returns_empty() {
4516 let g = GraphStore::new();
4517 g.add_entity(Entity::new("a", "N")).unwrap();
4518 g.add_entity(Entity::new("b", "N")).unwrap();
4519 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4520 assert!(g.isolated_nodes().unwrap().is_empty());
4522 }
4523
4524 #[test]
4525 fn test_isolated_nodes_returns_orphan_entity() {
4526 let g = GraphStore::new();
4527 g.add_entity(Entity::new("a", "N")).unwrap();
4528 g.add_entity(Entity::new("b", "N")).unwrap();
4529 g.add_entity(Entity::new("orphan", "N")).unwrap();
4530 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4531 let iso = g.isolated_nodes().unwrap();
4532 assert_eq!(iso.len(), 1);
4533 assert!(iso.contains(&EntityId::new("orphan")));
4534 }
4535
4536 #[test]
4539 fn test_reverse_flips_edge_direction() {
4540 let g = GraphStore::new();
4541 g.add_entity(Entity::new("x", "N")).unwrap();
4542 g.add_entity(Entity::new("y", "N")).unwrap();
4543 g.add_relationship(Relationship::new("x", "y", "edge", 1.0)).unwrap();
4544 let rev = g.reverse().unwrap();
4545 let y_id = EntityId::new("y");
4547 let succs = rev.successors(&y_id).unwrap();
4548 assert!(succs.iter().any(|e| e.id == EntityId::new("x")));
4549 }
4550
4551 #[test]
4552 fn test_reverse_empty_graph_produces_empty_reverse() {
4553 let g = GraphStore::new();
4554 let rev = g.reverse().unwrap();
4555 assert!(rev.is_empty().unwrap());
4556 }
4557
4558 #[test]
4559 fn test_max_in_degree_entity_none_for_empty_graph() {
4560 let g = GraphStore::new();
4561 assert!(g.max_in_degree_entity().unwrap().is_none());
4562 }
4563
4564 #[test]
4565 fn test_max_in_degree_entity_returns_node_with_most_incoming() {
4566 let g = GraphStore::new();
4567 g.add_entity(Entity::new("hub", "N")).unwrap();
4568 g.add_entity(Entity::new("a", "N")).unwrap();
4569 g.add_entity(Entity::new("b", "N")).unwrap();
4570 g.add_relationship(Relationship::new("a", "hub", "e", 1.0)).unwrap();
4571 g.add_relationship(Relationship::new("b", "hub", "e", 1.0)).unwrap();
4572 let best = g.max_in_degree_entity().unwrap().unwrap();
4573 assert_eq!(best.id, EntityId::new("hub"));
4574 }
4575
4576 #[test]
4577 fn test_shortest_path_length_none_when_no_path() {
4578 let g = GraphStore::new();
4579 g.add_entity(Entity::new("a", "N")).unwrap();
4580 g.add_entity(Entity::new("b", "N")).unwrap();
4581 let len = g
4582 .shortest_path_length(&EntityId::new("a"), &EntityId::new("b"))
4583 .unwrap();
4584 assert!(len.is_none());
4585 }
4586
4587 #[test]
4588 fn test_shortest_path_length_one_for_direct_edge() {
4589 let g = GraphStore::new();
4590 g.add_entity(Entity::new("a", "N")).unwrap();
4591 g.add_entity(Entity::new("b", "N")).unwrap();
4592 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4593 let len = g
4594 .shortest_path_length(&EntityId::new("a"), &EntityId::new("b"))
4595 .unwrap();
4596 assert_eq!(len, Some(1));
4597 }
4598
4599 #[test]
4600 fn test_shortest_path_length_two_for_two_hop_path() {
4601 let g = GraphStore::new();
4602 g.add_entity(Entity::new("a", "N")).unwrap();
4603 g.add_entity(Entity::new("b", "N")).unwrap();
4604 g.add_entity(Entity::new("c", "N")).unwrap();
4605 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4606 g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
4607 let len = g
4608 .shortest_path_length(&EntityId::new("a"), &EntityId::new("c"))
4609 .unwrap();
4610 assert_eq!(len, Some(2));
4611 }
4612
4613 #[test]
4616 fn test_avg_out_degree_zero_for_empty_graph() {
4617 let g = GraphStore::new();
4618 assert!((g.avg_out_degree().unwrap() - 0.0).abs() < 1e-9);
4619 }
4620
4621 #[test]
4622 fn test_avg_out_degree_correct_mean() {
4623 let g = GraphStore::new();
4624 g.add_entity(Entity::new("a", "N")).unwrap();
4625 g.add_entity(Entity::new("b", "N")).unwrap();
4626 g.add_entity(Entity::new("c", "N")).unwrap();
4627 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
4629 g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
4630 let avg = g.avg_out_degree().unwrap();
4632 assert!((avg - 2.0 / 3.0).abs() < 1e-9);
4633 }
4634
4635 #[test]
4636 fn test_avg_in_degree_zero_for_empty_graph() {
4637 let g = GraphStore::new();
4638 assert!((g.avg_in_degree().unwrap() - 0.0).abs() < 1e-9);
4639 }
4640
4641 #[test]
4642 fn test_avg_in_degree_correct_mean() {
4643 let g = GraphStore::new();
4644 g.add_entity(Entity::new("a", "N")).unwrap();
4645 g.add_entity(Entity::new("b", "N")).unwrap();
4646 g.add_entity(Entity::new("c", "N")).unwrap();
4647 g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
4649 g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
4650 let avg = g.avg_in_degree().unwrap();
4652 assert!((avg - 2.0 / 3.0).abs() < 1e-9);
4653 }
4654
4655 #[test]
4658 fn test_graph_store_has_entity_false_when_missing() {
4659 let g = GraphStore::new();
4660 let id = EntityId::new("nonexistent");
4661 assert!(!g.has_entity(&id).unwrap());
4662 }
4663
4664 #[test]
4665 fn test_graph_store_has_entity_true_after_add() {
4666 let g = GraphStore::new();
4667 g.add_entity(Entity::new("node-a", "Person")).unwrap();
4668 let id = EntityId::new("node-a");
4669 assert!(g.has_entity(&id).unwrap());
4670 }
4671
4672 #[test]
4676 fn test_entity_with_property_stores_value() {
4677 let e = Entity::new("e1", "Label")
4678 .with_property("score", serde_json::json!(42));
4679 assert_eq!(e.property_value("score"), Some(&serde_json::json!(42)));
4680 }
4681
4682 #[test]
4683 fn test_entity_with_property_chaining() {
4684 let e = Entity::new("e2", "L")
4685 .with_property("a", serde_json::json!(1))
4686 .with_property("b", serde_json::json!(2));
4687 assert!(e.has_property("a"));
4688 assert!(e.has_property("b"));
4689 }
4690
4691 #[test]
4692 fn test_remove_relationship_succeeds_when_exists() {
4693 let g = GraphStore::new();
4694 g.add_entity(Entity::new("x", "N")).unwrap();
4695 g.add_entity(Entity::new("y", "N")).unwrap();
4696 g.add_relationship(Relationship::new("x", "y", "link", 1.0)).unwrap();
4697 g.remove_relationship(&EntityId::new("x"), &EntityId::new("y"), "link").unwrap();
4698 assert_eq!(g.relationship_count().unwrap(), 0);
4699 }
4700
4701 #[test]
4702 fn test_remove_relationship_errors_when_missing() {
4703 let g = GraphStore::new();
4704 g.add_entity(Entity::new("x", "N")).unwrap();
4705 g.add_entity(Entity::new("y", "N")).unwrap();
4706 let result = g.remove_relationship(&EntityId::new("x"), &EntityId::new("y"), "ghost");
4707 assert!(result.is_err());
4708 }
4709
4710 #[test]
4711 fn test_update_entity_property_returns_true_when_entity_exists() {
4712 let g = GraphStore::new();
4713 g.add_entity(Entity::new("node", "N")).unwrap();
4714 let updated = g
4715 .update_entity_property(&EntityId::new("node"), "color", serde_json::json!("red"))
4716 .unwrap();
4717 assert!(updated);
4718 let entity = g.get_entity(&EntityId::new("node")).unwrap();
4719 assert_eq!(entity.property_value("color"), Some(&serde_json::json!("red")));
4720 }
4721
4722 #[test]
4723 fn test_update_entity_property_returns_false_for_unknown_entity() {
4724 let g = GraphStore::new();
4725 let updated = g
4726 .update_entity_property(&EntityId::new("ghost"), "key", serde_json::json!(1))
4727 .unwrap();
4728 assert!(!updated);
4729 }
4730
4731 #[test]
4734 fn test_graph_store_has_any_entities_false_when_empty() {
4735 let g = GraphStore::new();
4736 assert!(!g.has_any_entities().unwrap());
4737 }
4738
4739 #[test]
4740 fn test_graph_store_has_any_entities_true_after_add() {
4741 let g = GraphStore::new();
4742 g.add_entity(Entity::new("x", "Node")).unwrap();
4743 assert!(g.has_any_entities().unwrap());
4744 }
4745
4746 #[test]
4749 fn test_entity_type_count_zero_for_empty_graph() {
4750 let g = GraphStore::new();
4751 assert_eq!(g.entity_type_count().unwrap(), 0);
4752 }
4753
4754 #[test]
4755 fn test_entity_type_count_counts_distinct_labels() {
4756 let g = GraphStore::new();
4757 g.add_entity(Entity::new("a", "Person")).unwrap();
4758 g.add_entity(Entity::new("b", "Person")).unwrap();
4759 g.add_entity(Entity::new("c", "Concept")).unwrap();
4760 assert_eq!(g.entity_type_count().unwrap(), 2);
4762 }
4763
4764 #[test]
4767 fn test_orphan_count_zero_for_empty_graph() {
4768 let g = GraphStore::new();
4769 assert_eq!(g.orphan_count().unwrap(), 0);
4770 }
4771
4772 #[test]
4773 fn test_orphan_count_all_orphans_with_no_relationships() {
4774 let g = GraphStore::new();
4775 g.add_entity(Entity::new("a", "N")).unwrap();
4776 g.add_entity(Entity::new("b", "N")).unwrap();
4777 assert_eq!(g.orphan_count().unwrap(), 2);
4778 }
4779
4780 #[test]
4781 fn test_orphan_count_excludes_entities_with_edges() {
4782 let g = GraphStore::new();
4783 g.add_entity(Entity::new("a", "N")).unwrap();
4784 g.add_entity(Entity::new("b", "N")).unwrap();
4785 g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
4786 assert_eq!(g.orphan_count().unwrap(), 1);
4788 }
4789
4790 #[test]
4793 fn test_labels_empty_for_empty_graph() {
4794 let g = GraphStore::new();
4795 assert!(g.labels().unwrap().is_empty());
4796 }
4797
4798 #[test]
4799 fn test_labels_returns_distinct_sorted_labels() {
4800 let g = GraphStore::new();
4801 g.add_entity(Entity::new("a", "Concept")).unwrap();
4802 g.add_entity(Entity::new("b", "Person")).unwrap();
4803 g.add_entity(Entity::new("c", "Concept")).unwrap();
4804 assert_eq!(g.labels().unwrap(), vec!["Concept".to_string(), "Person".to_string()]);
4805 }
4806
4807 #[test]
4808 fn test_incoming_count_for_counts_inbound_edges() {
4809 let g = GraphStore::new();
4810 g.add_entity(Entity::new("a", "Node")).unwrap();
4811 g.add_entity(Entity::new("b", "Node")).unwrap();
4812 g.add_entity(Entity::new("c", "Node")).unwrap();
4813 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4814 g.add_relationship(Relationship::new("c", "b", "link", 1.0)).unwrap();
4815 assert_eq!(g.incoming_count_for(&EntityId::new("b")).unwrap(), 2);
4816 assert_eq!(g.incoming_count_for(&EntityId::new("a")).unwrap(), 0);
4817 }
4818
4819 #[test]
4820 fn test_outgoing_count_for_counts_outbound_edges() {
4821 let g = GraphStore::new();
4822 g.add_entity(Entity::new("a", "Node")).unwrap();
4823 g.add_entity(Entity::new("b", "Node")).unwrap();
4824 g.add_entity(Entity::new("c", "Node")).unwrap();
4825 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4826 g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
4827 assert_eq!(g.outgoing_count_for(&EntityId::new("a")).unwrap(), 2);
4828 assert_eq!(g.outgoing_count_for(&EntityId::new("b")).unwrap(), 0);
4829 }
4830
4831 #[test]
4832 fn test_source_count_returns_number_of_nodes_with_no_incoming_edges() {
4833 let g = GraphStore::new();
4834 g.add_entity(Entity::new("a", "Node")).unwrap();
4835 g.add_entity(Entity::new("b", "Node")).unwrap();
4836 g.add_entity(Entity::new("c", "Node")).unwrap();
4837 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4839 g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
4840 assert_eq!(g.source_count().unwrap(), 1);
4841 }
4842
4843 #[test]
4844 fn test_source_count_all_isolated_nodes_are_sources() {
4845 let g = GraphStore::new();
4846 g.add_entity(Entity::new("x", "Node")).unwrap();
4847 g.add_entity(Entity::new("y", "Node")).unwrap();
4848 assert_eq!(g.source_count().unwrap(), 2);
4849 }
4850
4851 #[test]
4852 fn test_sink_count_returns_nodes_with_no_outgoing_edges() {
4853 let g = GraphStore::new();
4854 g.add_entity(Entity::new("a", "Node")).unwrap();
4855 g.add_entity(Entity::new("b", "Node")).unwrap();
4856 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4857 assert_eq!(g.sink_count().unwrap(), 1);
4859 assert_eq!(g.source_count().unwrap(), 1);
4860 }
4861
4862 #[test]
4863 fn test_has_self_loops_true_when_self_loop_exists() {
4864 let g = GraphStore::new();
4865 g.add_entity(Entity::new("a", "Node")).unwrap();
4866 g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
4867 assert!(g.has_self_loops().unwrap());
4868 }
4869
4870 #[test]
4871 fn test_has_self_loops_false_when_no_self_loops() {
4872 let g = GraphStore::new();
4873 g.add_entity(Entity::new("a", "Node")).unwrap();
4874 g.add_entity(Entity::new("b", "Node")).unwrap();
4875 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4876 assert!(!g.has_self_loops().unwrap());
4877 }
4878
4879 #[test]
4880 fn test_bidirectional_count_counts_mutual_edges() {
4881 let g = GraphStore::new();
4882 g.add_entity(Entity::new("a", "Node")).unwrap();
4883 g.add_entity(Entity::new("b", "Node")).unwrap();
4884 g.add_entity(Entity::new("c", "Node")).unwrap();
4885 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4886 g.add_relationship(Relationship::new("b", "a", "link", 1.0)).unwrap(); g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap(); assert_eq!(g.bidirectional_count().unwrap(), 1);
4889 }
4890
4891 #[test]
4892 fn test_bidirectional_count_zero_when_no_mutual_edges() {
4893 let g = GraphStore::new();
4894 g.add_entity(Entity::new("a", "Node")).unwrap();
4895 g.add_entity(Entity::new("b", "Node")).unwrap();
4896 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4897 assert_eq!(g.bidirectional_count().unwrap(), 0);
4898 }
4899
4900 #[test]
4903 fn test_relationship_kind_count_counts_distinct_kinds() {
4904 let g = GraphStore::new();
4905 g.add_entity(Entity::new("a", "N")).unwrap();
4906 g.add_entity(Entity::new("b", "N")).unwrap();
4907 g.add_entity(Entity::new("c", "N")).unwrap();
4908 g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
4909 g.add_relationship(Relationship::new("b", "c", "friend", 1.0)).unwrap();
4910 g.add_relationship(Relationship::new("a", "c", "enemy", 1.0)).unwrap();
4911 assert_eq!(g.relationship_kind_count().unwrap(), 2);
4912 }
4913
4914 #[test]
4915 fn test_relationship_kind_count_zero_when_empty() {
4916 let g = GraphStore::new();
4917 assert_eq!(g.relationship_kind_count().unwrap(), 0);
4918 }
4919
4920 #[test]
4921 fn test_entities_with_self_loops_returns_self_loop_ids() {
4922 let g = GraphStore::new();
4923 g.add_entity(Entity::new("a", "N")).unwrap();
4924 g.add_entity(Entity::new("b", "N")).unwrap();
4925 g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
4926 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4927 let ids = g.entities_with_self_loops().unwrap();
4928 assert_eq!(ids, vec![EntityId::new("a")]);
4929 }
4930
4931 #[test]
4932 fn test_entities_with_self_loops_empty_when_no_self_loops() {
4933 let g = GraphStore::new();
4934 g.add_entity(Entity::new("a", "N")).unwrap();
4935 g.add_entity(Entity::new("b", "N")).unwrap();
4936 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4937 assert!(g.entities_with_self_loops().unwrap().is_empty());
4938 }
4939
4940 #[test]
4943 fn test_isolated_entity_count_returns_count_with_no_edges() {
4944 let g = GraphStore::new();
4945 g.add_entity(Entity::new("a", "N")).unwrap();
4946 g.add_entity(Entity::new("b", "N")).unwrap();
4947 g.add_entity(Entity::new("c", "N")).unwrap();
4948 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4949 assert_eq!(g.isolated_entity_count().unwrap(), 1);
4951 }
4952
4953 #[test]
4954 fn test_isolated_entity_count_zero_when_all_connected() {
4955 let g = GraphStore::new();
4956 g.add_entity(Entity::new("a", "N")).unwrap();
4957 g.add_entity(Entity::new("b", "N")).unwrap();
4958 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4959 assert_eq!(g.isolated_entity_count().unwrap(), 0);
4960 }
4961
4962 #[test]
4963 fn test_avg_relationship_weight_returns_mean() {
4964 let g = GraphStore::new();
4965 g.add_entity(Entity::new("a", "N")).unwrap();
4966 g.add_entity(Entity::new("b", "N")).unwrap();
4967 g.add_entity(Entity::new("c", "N")).unwrap();
4968 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4969 g.add_relationship(Relationship::new("b", "c", "link", 3.0)).unwrap();
4970 let avg = g.avg_relationship_weight().unwrap();
4971 assert!((avg - 2.0).abs() < 1e-6);
4972 }
4973
4974 #[test]
4975 fn test_avg_relationship_weight_zero_when_no_relationships() {
4976 let g = GraphStore::new();
4977 assert_eq!(g.avg_relationship_weight().unwrap(), 0.0);
4978 }
4979
4980 #[test]
4983 fn test_total_in_degree_equals_relationship_count() {
4984 let g = GraphStore::new();
4985 g.add_entity(Entity::new("a", "N")).unwrap();
4986 g.add_entity(Entity::new("b", "N")).unwrap();
4987 g.add_entity(Entity::new("c", "N")).unwrap();
4988 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4989 g.add_relationship(Relationship::new("b", "c", "link", 1.0)).unwrap();
4990 assert_eq!(g.total_in_degree().unwrap(), 2);
4991 }
4992
4993 #[test]
4994 fn test_total_in_degree_zero_when_no_relationships() {
4995 let g = GraphStore::new();
4996 assert_eq!(g.total_in_degree().unwrap(), 0);
4997 }
4998
4999 #[test]
5000 fn test_relationship_count_between_counts_edges_between_pair() {
5001 let g = GraphStore::new();
5002 g.add_entity(Entity::new("a", "N")).unwrap();
5003 g.add_entity(Entity::new("b", "N")).unwrap();
5004 g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
5005 g.add_relationship(Relationship::new("a", "b", "colleague", 1.0)).unwrap();
5006 let from = EntityId::new("a");
5007 let to = EntityId::new("b");
5008 assert_eq!(g.relationship_count_between(&from, &to).unwrap(), 2);
5009 }
5010
5011 #[test]
5012 fn test_relationship_count_between_returns_zero_for_no_edge() {
5013 let g = GraphStore::new();
5014 g.add_entity(Entity::new("a", "N")).unwrap();
5015 g.add_entity(Entity::new("b", "N")).unwrap();
5016 let from = EntityId::new("a");
5017 let to = EntityId::new("b");
5018 assert_eq!(g.relationship_count_between(&from, &to).unwrap(), 0);
5019 }
5020
5021 #[test]
5024 fn test_edges_from_returns_all_outgoing_relationships() {
5025 let g = GraphStore::new();
5026 g.add_entity(Entity::new("a", "N")).unwrap();
5027 g.add_entity(Entity::new("b", "N")).unwrap();
5028 g.add_entity(Entity::new("c", "N")).unwrap();
5029 g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
5030 g.add_relationship(Relationship::new("a", "c", "enemy", 0.5)).unwrap();
5031 let edges = g.edges_from(&EntityId::new("a")).unwrap();
5032 assert_eq!(edges.len(), 2);
5033 }
5034
5035 #[test]
5036 fn test_edges_from_returns_empty_for_unknown_entity() {
5037 let g = GraphStore::new();
5038 assert!(g.edges_from(&EntityId::new("missing")).unwrap().is_empty());
5039 }
5040
5041 #[test]
5042 fn test_neighbors_of_returns_sorted_unique_targets() {
5043 let g = GraphStore::new();
5044 g.add_entity(Entity::new("a", "N")).unwrap();
5045 g.add_entity(Entity::new("b", "N")).unwrap();
5046 g.add_entity(Entity::new("c", "N")).unwrap();
5047 g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
5048 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
5049 let neighbors = g.neighbors_of(&EntityId::new("a")).unwrap();
5050 assert_eq!(neighbors, vec![EntityId::new("b"), EntityId::new("c")]);
5051 }
5052
5053 #[test]
5054 fn test_neighbors_of_returns_empty_for_no_outgoing_edges() {
5055 let g = GraphStore::new();
5056 g.add_entity(Entity::new("a", "N")).unwrap();
5057 assert!(g.neighbors_of(&EntityId::new("a")).unwrap().is_empty());
5058 }
5059}