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
110impl From<String> for EntityId {
111 fn from(s: String) -> Self {
115 Self::new(s)
116 }
117}
118
119impl From<&str> for EntityId {
120 fn from(s: &str) -> Self {
124 Self::new(s)
125 }
126}
127
128impl std::str::FromStr for EntityId {
129 type Err = AgentRuntimeError;
130
131 fn from_str(s: &str) -> Result<Self, Self::Err> {
136 Self::try_new(s)
137 }
138}
139
140impl std::ops::Deref for EntityId {
141 type Target = str;
142
143 fn deref(&self) -> &Self::Target {
147 &self.0
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct Entity {
156 pub id: EntityId,
158 pub label: String,
160 pub properties: HashMap<String, Value>,
162}
163
164impl Entity {
165 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
167 Self {
168 id: EntityId::new(id),
169 label: label.into(),
170 properties: HashMap::new(),
171 }
172 }
173
174 pub fn with_properties(
176 id: impl Into<String>,
177 label: impl Into<String>,
178 properties: HashMap<String, Value>,
179 ) -> Self {
180 Self {
181 id: EntityId::new(id),
182 label: label.into(),
183 properties,
184 }
185 }
186
187 pub fn with_property(mut self, key: impl Into<String>, value: Value) -> Self {
196 self.properties.insert(key.into(), value);
197 self
198 }
199
200 pub fn has_property(&self, key: &str) -> bool {
202 self.properties.contains_key(key)
203 }
204
205 pub fn property_value(&self, key: &str) -> Option<&serde_json::Value> {
207 self.properties.get(key)
208 }
209
210 pub fn remove_property(&mut self, key: &str) -> Option<serde_json::Value> {
215 self.properties.remove(key)
216 }
217
218 pub fn property_count(&self) -> usize {
220 self.properties.len()
221 }
222
223 pub fn properties_is_empty(&self) -> bool {
225 self.properties.is_empty()
226 }
227
228 pub fn property_keys(&self) -> Vec<&str> {
232 let mut keys: Vec<&str> = self.properties.keys().map(|k| k.as_str()).collect();
233 keys.sort_unstable();
234 keys
235 }
236}
237
238impl std::fmt::Display for Entity {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 write!(
242 f,
243 "Entity[id='{}', label='{}', props={}]",
244 self.id,
245 self.label,
246 self.properties.len()
247 )
248 }
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct Relationship {
256 pub from: EntityId,
258 pub to: EntityId,
260 pub kind: String,
262 pub weight: f32,
264}
265
266impl Relationship {
267 pub fn new(
269 from: impl Into<String>,
270 to: impl Into<String>,
271 kind: impl Into<String>,
272 weight: f32,
273 ) -> Self {
274 Self {
275 from: EntityId::new(from),
276 to: EntityId::new(to),
277 kind: kind.into(),
278 weight,
279 }
280 }
281
282 pub fn is_self_loop(&self) -> bool {
284 self.from == self.to
285 }
286
287 pub fn reversed(&self) -> Self {
291 Self {
292 from: self.to.clone(),
293 to: self.from.clone(),
294 kind: self.kind.clone(),
295 weight: self.weight,
296 }
297 }
298
299 pub fn with_weight(self, w: f32) -> Self {
310 Self { weight: w, ..self }
311 }
312}
313
314impl std::fmt::Display for Relationship {
315 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 write!(f, "{} --{}({:.2})--> {}", self.from, self.kind, self.weight, self.to)
320 }
321}
322
323#[derive(Debug, thiserror::Error)]
327pub enum MemGraphError {
328 #[error("Entity '{0}' not found")]
330 EntityNotFound(String),
331
332 #[error("Relationship '{kind}' from '{from}' to '{to}' already exists")]
334 DuplicateRelationship {
335 from: String,
337 to: String,
339 kind: String,
341 },
342
343 #[error("Graph internal error: {0}")]
345 Internal(String),
346}
347
348impl From<MemGraphError> for AgentRuntimeError {
349 fn from(e: MemGraphError) -> Self {
350 AgentRuntimeError::Graph(e.to_string())
351 }
352}
353
354#[derive(Debug, Clone)]
365pub struct GraphStore {
366 inner: Arc<Mutex<GraphInner>>,
367}
368
369#[derive(Debug)]
370struct GraphInner {
371 entities: HashMap<EntityId, Entity>,
372 relationships: Vec<Relationship>,
374 adjacency: HashMap<EntityId, Vec<Relationship>>,
378 reverse_adjacency: HashMap<EntityId, Vec<EntityId>>,
382 cycle_cache: Option<bool>,
384}
385
386impl GraphStore {
387 pub fn new() -> Self {
389 Self {
390 inner: Arc::new(Mutex::new(GraphInner {
391 entities: HashMap::new(),
392 relationships: Vec::new(),
393 adjacency: HashMap::new(),
394 reverse_adjacency: HashMap::new(),
395 cycle_cache: None,
396 })),
397 }
398 }
399
400 pub fn add_entity(&self, entity: Entity) -> Result<(), AgentRuntimeError> {
404 let mut inner = recover_lock(self.inner.lock(), "add_entity");
405 inner.cycle_cache = None;
406 inner.adjacency.entry(entity.id.clone()).or_default();
408 inner.entities.insert(entity.id.clone(), entity);
409 Ok(())
410 }
411
412 pub fn get_entity(&self, id: &EntityId) -> Result<Entity, AgentRuntimeError> {
414 let inner = recover_lock(self.inner.lock(), "get_entity");
415 inner
416 .entities
417 .get(id)
418 .cloned()
419 .ok_or_else(|| AgentRuntimeError::Graph(format!("entity '{}' not found", id.0)))
420 }
421
422 pub fn has_entity(&self, id: &EntityId) -> Result<bool, AgentRuntimeError> {
424 let inner = recover_lock(self.inner.lock(), "GraphStore::has_entity");
425 Ok(inner.entities.contains_key(id))
426 }
427
428 pub fn has_any_entities(&self) -> Result<bool, AgentRuntimeError> {
430 let inner = recover_lock(self.inner.lock(), "GraphStore::has_any_entities");
431 Ok(!inner.entities.is_empty())
432 }
433
434 pub fn entity_type_count(&self) -> Result<usize, AgentRuntimeError> {
436 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_type_count");
437 let types: std::collections::HashSet<&str> =
438 inner.entities.values().map(|e| e.label.as_str()).collect();
439 Ok(types.len())
440 }
441
442 pub fn labels(&self) -> Result<Vec<String>, AgentRuntimeError> {
444 let inner = recover_lock(self.inner.lock(), "GraphStore::labels");
445 let mut labels: Vec<String> = inner
446 .entities
447 .values()
448 .map(|e| e.label.clone())
449 .collect::<std::collections::HashSet<_>>()
450 .into_iter()
451 .collect();
452 labels.sort();
453 Ok(labels)
454 }
455
456 pub fn incoming_count_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
460 let inner = recover_lock(self.inner.lock(), "GraphStore::incoming_count_for");
461 Ok(inner.reverse_adjacency.get(id).map_or(0, |v| v.len()))
462 }
463
464 pub fn outgoing_count_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
468 let inner = recover_lock(self.inner.lock(), "GraphStore::outgoing_count_for");
469 Ok(inner.adjacency.get(id).map_or(0, |v| v.len()))
470 }
471
472 pub fn sink_count(&self) -> Result<usize, AgentRuntimeError> {
474 let inner = recover_lock(self.inner.lock(), "GraphStore::sink_count");
475 let count = inner
476 .entities
477 .keys()
478 .filter(|id| inner.adjacency.get(*id).map_or(true, |v| v.is_empty()))
479 .count();
480 Ok(count)
481 }
482
483 pub fn bidirectional_count(&self) -> Result<usize, AgentRuntimeError> {
487 let inner = recover_lock(self.inner.lock(), "GraphStore::bidirectional_count");
488 let mut count = 0usize;
489 for (from, rels) in &inner.adjacency {
490 for rel in rels {
491 let to = &rel.to;
492 if to > from {
493 if inner.adjacency.get(to).map_or(false, |v| v.iter().any(|r| &r.to == from)) {
495 count += 1;
496 }
497 }
498 }
499 }
500 Ok(count)
501 }
502
503 pub fn has_self_loops(&self) -> Result<bool, AgentRuntimeError> {
505 let inner = recover_lock(self.inner.lock(), "GraphStore::has_self_loops");
506 let found = inner.adjacency.iter().any(|(from, rels)| {
507 rels.iter().any(|r| &r.to == from)
508 });
509 Ok(found)
510 }
511
512 pub fn source_count(&self) -> Result<usize, AgentRuntimeError> {
514 let inner = recover_lock(self.inner.lock(), "GraphStore::source_count");
515 let count = inner
516 .entities
517 .keys()
518 .filter(|id| inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty()))
519 .count();
520 Ok(count)
521 }
522
523 pub fn orphan_count(&self) -> Result<usize, AgentRuntimeError> {
525 let inner = recover_lock(self.inner.lock(), "GraphStore::orphan_count");
526 let count = inner
527 .entities
528 .keys()
529 .filter(|id| inner.adjacency.get(*id).map_or(true, |v| v.is_empty()))
530 .count();
531 Ok(count)
532 }
533
534 pub fn isolated_entity_count(&self) -> Result<usize, AgentRuntimeError> {
536 let inner = recover_lock(self.inner.lock(), "GraphStore::isolated_entity_count");
537 let count = inner.entities.keys().filter(|id| {
538 inner.adjacency.get(*id).map_or(true, |v| v.is_empty())
539 && inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty())
540 }).count();
541 Ok(count)
542 }
543
544 pub fn avg_relationship_weight(&self) -> Result<f64, AgentRuntimeError> {
548 let inner = recover_lock(self.inner.lock(), "GraphStore::avg_relationship_weight");
549 if inner.relationships.is_empty() {
550 return Ok(0.0);
551 }
552 let total: f32 = inner.relationships.iter().map(|r| r.weight).sum();
553 Ok(total as f64 / inner.relationships.len() as f64)
554 }
555
556 pub fn total_in_degree(&self) -> Result<usize, AgentRuntimeError> {
561 let inner = recover_lock(self.inner.lock(), "GraphStore::total_in_degree");
562 Ok(inner.relationships.len())
563 }
564
565 pub fn relationship_count_between(
569 &self,
570 from: &EntityId,
571 to: &EntityId,
572 ) -> Result<usize, AgentRuntimeError> {
573 let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_count_between");
574 Ok(inner
575 .adjacency
576 .get(from)
577 .map_or(0, |rels| rels.iter().filter(|r| &r.to == to).count()))
578 }
579
580 pub fn edges_from(&self, id: &EntityId) -> Result<Vec<Relationship>, AgentRuntimeError> {
584 let inner = recover_lock(self.inner.lock(), "GraphStore::edges_from");
585 Ok(inner
586 .adjacency
587 .get(id)
588 .cloned()
589 .unwrap_or_default())
590 }
591
592 pub fn has_edge(
600 &self,
601 from: &EntityId,
602 to: &EntityId,
603 ) -> Result<bool, AgentRuntimeError> {
604 let inner = recover_lock(self.inner.lock(), "GraphStore::has_edge");
605 Ok(inner
606 .adjacency
607 .get(from)
608 .map_or(false, |rels| rels.iter().any(|r| &r.to == to)))
609 }
610
611 pub fn neighbors_of(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
615 let inner = recover_lock(self.inner.lock(), "GraphStore::neighbors_of");
616 let mut targets: Vec<EntityId> = inner
617 .adjacency
618 .get(id)
619 .map_or_else(Vec::new, |rels| rels.iter().map(|r| r.to.clone()).collect());
620 targets.sort_unstable();
621 targets.dedup();
622 Ok(targets)
623 }
624
625 pub fn add_relationship(&self, rel: Relationship) -> Result<(), AgentRuntimeError> {
629 let mut inner = recover_lock(self.inner.lock(), "add_relationship");
630
631 if !inner.entities.contains_key(&rel.from) {
632 return Err(AgentRuntimeError::Graph(format!(
633 "source entity '{}' not found",
634 rel.from.0
635 )));
636 }
637 if !inner.entities.contains_key(&rel.to) {
638 return Err(AgentRuntimeError::Graph(format!(
639 "target entity '{}' not found",
640 rel.to.0
641 )));
642 }
643
644 let duplicate = inner
648 .relationships
649 .iter()
650 .any(|r| r.from == rel.from && r.to == rel.to && r.kind == rel.kind);
651 if duplicate {
652 return Err(AgentRuntimeError::Graph(
653 MemGraphError::DuplicateRelationship {
654 from: rel.from.0.clone(),
655 to: rel.to.0.clone(),
656 kind: rel.kind.clone(),
657 }
658 .to_string(),
659 ));
660 }
661
662 inner.cycle_cache = None;
663 inner
665 .adjacency
666 .entry(rel.from.clone())
667 .or_default()
668 .push(rel.clone());
669 inner
671 .reverse_adjacency
672 .entry(rel.to.clone())
673 .or_default()
674 .push(rel.from.clone());
675 inner.relationships.push(rel);
676 Ok(())
677 }
678
679 pub fn remove_relationship(
683 &self,
684 from: &EntityId,
685 to: &EntityId,
686 kind: &str,
687 ) -> Result<(), AgentRuntimeError> {
688 let mut inner = recover_lock(self.inner.lock(), "remove_relationship");
689
690 let before = inner.relationships.len();
691 inner
692 .relationships
693 .retain(|r| !(&r.from == from && &r.to == to && r.kind == kind));
694 if inner.relationships.len() == before {
695 return Err(AgentRuntimeError::Graph(format!(
696 "relationship '{kind}' from '{}' to '{}' not found",
697 from.0, to.0
698 )));
699 }
700
701 if let Some(adj) = inner.adjacency.get_mut(from) {
703 adj.retain(|r| !(&r.to == to && r.kind == kind));
704 }
705 if let Some(rev) = inner.reverse_adjacency.get_mut(to) {
707 rev.retain(|src| src != from);
708 }
709
710 inner.cycle_cache = None;
711 Ok(())
712 }
713
714 pub fn remove_entity(&self, id: &EntityId) -> Result<(), AgentRuntimeError> {
716 let mut inner = recover_lock(self.inner.lock(), "remove_entity");
717
718 if inner.entities.remove(id).is_none() {
719 return Err(AgentRuntimeError::Graph(format!(
720 "entity '{}' not found",
721 id.0
722 )));
723 }
724 inner.cycle_cache = None;
725 inner.relationships.retain(|r| &r.from != id && &r.to != id);
726 inner.adjacency.remove(id);
728 for adj in inner.adjacency.values_mut() {
729 adj.retain(|r| &r.to != id);
730 }
731 inner.reverse_adjacency.remove(id);
733 for rev in inner.reverse_adjacency.values_mut() {
734 rev.retain(|src| src != id);
735 }
736 Ok(())
737 }
738
739 fn neighbours(adjacency: &HashMap<EntityId, Vec<Relationship>>, id: &EntityId) -> Vec<EntityId> {
743 adjacency
744 .get(id)
745 .map(|rels| rels.iter().map(|r| r.to.clone()).collect())
746 .unwrap_or_default()
747 }
748
749 #[tracing::instrument(skip(self))]
753 pub fn bfs(&self, start: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
754 let inner = recover_lock(self.inner.lock(), "bfs");
755
756 if !inner.entities.contains_key(start) {
757 return Err(AgentRuntimeError::Graph(format!(
758 "start entity '{}' not found",
759 start.0
760 )));
761 }
762
763 let mut visited: HashSet<EntityId> = HashSet::new();
764 let mut queue: VecDeque<EntityId> = VecDeque::new();
765 let mut result: Vec<EntityId> = Vec::new();
766
767 visited.insert(start.clone());
768 queue.push_back(start.clone());
769
770 while let Some(current) = queue.pop_front() {
771 let neighbours: Vec<EntityId> = Self::neighbours(&inner.adjacency, ¤t);
772 for neighbour in neighbours {
773 if visited.insert(neighbour.clone()) {
774 result.push(neighbour.clone());
775 queue.push_back(neighbour);
776 }
777 }
778 }
779
780 tracing::debug!("BFS visited {} nodes", result.len());
781 Ok(result)
782 }
783
784 #[tracing::instrument(skip(self))]
788 pub fn dfs(&self, start: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
789 let inner = recover_lock(self.inner.lock(), "dfs");
790
791 if !inner.entities.contains_key(start) {
792 return Err(AgentRuntimeError::Graph(format!(
793 "start entity '{}' not found",
794 start.0
795 )));
796 }
797
798 let mut visited: HashSet<EntityId> = HashSet::new();
799 let mut stack: Vec<EntityId> = Vec::new();
800 let mut result: Vec<EntityId> = Vec::new();
801
802 visited.insert(start.clone());
803 stack.push(start.clone());
804
805 while let Some(current) = stack.pop() {
806 let neighbours: Vec<EntityId> = Self::neighbours(&inner.adjacency, ¤t);
807 for neighbour in neighbours {
808 if visited.insert(neighbour.clone()) {
809 result.push(neighbour.clone());
810 stack.push(neighbour);
811 }
812 }
813 }
814
815 tracing::debug!("DFS visited {} nodes", result.len());
816 Ok(result)
817 }
818
819 #[tracing::instrument(skip(self))]
825 pub fn shortest_path(
826 &self,
827 from: &EntityId,
828 to: &EntityId,
829 ) -> Result<Option<Vec<EntityId>>, AgentRuntimeError> {
830 let inner = recover_lock(self.inner.lock(), "shortest_path");
831
832 if !inner.entities.contains_key(from) {
833 return Err(AgentRuntimeError::Graph(format!(
834 "source entity '{}' not found",
835 from.0
836 )));
837 }
838 if !inner.entities.contains_key(to) {
839 return Err(AgentRuntimeError::Graph(format!(
840 "target entity '{}' not found",
841 to.0
842 )));
843 }
844
845 if from == to {
846 return Ok(Some(vec![from.clone()]));
847 }
848
849 let mut visited: HashSet<EntityId> = HashSet::new();
851 let mut prev: HashMap<EntityId, EntityId> = HashMap::new();
852 let mut queue: VecDeque<EntityId> = VecDeque::new();
853
854 visited.insert(from.clone());
855 queue.push_back(from.clone());
856
857 while let Some(current) = queue.pop_front() {
858 for neighbour in Self::neighbours(&inner.adjacency, ¤t) {
859 if &neighbour == to {
860 let mut path = vec![neighbour, current.clone()];
862 let mut node = current;
863 while let Some(p) = prev.get(&node) {
864 path.push(p.clone());
865 node = p.clone();
866 }
867 path.reverse();
868 return Ok(Some(path));
869 }
870 if visited.insert(neighbour.clone()) {
871 prev.insert(neighbour.clone(), current.clone());
872 queue.push_back(neighbour);
873 }
874 }
875 }
876
877 Ok(None)
878 }
879
880 pub fn shortest_path_weighted(
890 &self,
891 from: &EntityId,
892 to: &EntityId,
893 ) -> Result<Option<(Vec<EntityId>, f32)>, AgentRuntimeError> {
894 let inner = recover_lock(self.inner.lock(), "shortest_path_weighted");
895
896 if !inner.entities.contains_key(from) {
897 return Err(AgentRuntimeError::Graph(format!(
898 "source entity '{}' not found",
899 from.0
900 )));
901 }
902 if !inner.entities.contains_key(to) {
903 return Err(AgentRuntimeError::Graph(format!(
904 "target entity '{}' not found",
905 to.0
906 )));
907 }
908
909 for rel in &inner.relationships {
911 if rel.weight < 0.0 {
912 return Err(AgentRuntimeError::Graph(format!(
913 "negative weight {:.4} on edge '{}' -> '{}'",
914 rel.weight, rel.from.0, rel.to.0
915 )));
916 }
917 }
918
919 if from == to {
920 return Ok(Some((vec![from.clone()], 0.0)));
921 }
922
923 let mut dist: HashMap<EntityId, f32> = HashMap::new();
926 let mut prev: HashMap<EntityId, EntityId> = HashMap::new();
927 let mut heap: BinaryHeap<(OrdF32, EntityId)> = BinaryHeap::new();
929
930 dist.insert(from.clone(), 0.0);
931 heap.push((OrdF32(-0.0), from.clone()));
932
933 while let Some((OrdF32(neg_cost), current)) = heap.pop() {
934 let cost = -neg_cost;
935
936 if let Some(&best) = dist.get(¤t) {
938 if cost > best {
939 continue;
940 }
941 }
942
943 if ¤t == to {
944 let mut path = vec![to.clone()];
946 let mut node = to.clone();
947 while let Some(p) = prev.get(&node) {
948 path.push(p.clone());
949 node = p.clone();
950 }
951 path.reverse();
952 return Ok(Some((path, cost)));
953 }
954
955 if let Some(rels) = inner.adjacency.get(¤t) {
958 for rel in rels {
959 let next_cost = cost + rel.weight;
960 let entry = dist.entry(rel.to.clone()).or_insert(f32::INFINITY);
961 if next_cost < *entry {
962 *entry = next_cost;
963 prev.insert(rel.to.clone(), current.clone());
964 heap.push((OrdF32(-next_cost), rel.to.clone()));
965 }
966 }
967 }
968 }
969
970 Ok(None)
971 }
972
973 fn bfs_into_set(inner: &GraphInner, start: &EntityId) -> HashSet<EntityId> {
978 let mut visited: HashSet<EntityId> = HashSet::new();
979 let mut queue: VecDeque<EntityId> = VecDeque::new();
980 visited.insert(start.clone());
981 queue.push_back(start.clone());
982 while let Some(current) = queue.pop_front() {
983 for neighbour in Self::neighbours(&inner.adjacency, ¤t) {
984 if visited.insert(neighbour.clone()) {
985 queue.push_back(neighbour);
986 }
987 }
988 }
989 visited
990 }
991
992 pub fn transitive_closure(
1001 &self,
1002 start: &EntityId,
1003 ) -> Result<HashSet<EntityId>, AgentRuntimeError> {
1004 let inner = recover_lock(self.inner.lock(), "transitive_closure");
1005 if !inner.entities.contains_key(start) {
1006 return Err(AgentRuntimeError::Graph(format!(
1007 "start entity '{}' not found",
1008 start.0
1009 )));
1010 }
1011 Ok(Self::bfs_into_set(&inner, start))
1012 }
1013
1014 pub fn entity_count(&self) -> Result<usize, AgentRuntimeError> {
1016 let inner = recover_lock(self.inner.lock(), "entity_count");
1017 Ok(inner.entities.len())
1018 }
1019
1020 pub fn node_count(&self) -> Result<usize, AgentRuntimeError> {
1026 self.entity_count()
1027 }
1028
1029 pub fn relationship_count(&self) -> Result<usize, AgentRuntimeError> {
1031 let inner = recover_lock(self.inner.lock(), "relationship_count");
1032 Ok(inner.relationships.len())
1033 }
1034
1035 pub fn average_out_degree(&self) -> Result<f64, AgentRuntimeError> {
1040 let inner = recover_lock(self.inner.lock(), "average_out_degree");
1041 let n = inner.entities.len();
1042 if n == 0 {
1043 return Ok(0.0);
1044 }
1045 Ok(inner.relationships.len() as f64 / n as f64)
1046 }
1047
1048 pub fn in_degree_for(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
1052 let inner = recover_lock(self.inner.lock(), "in_degree_for");
1053 Ok(inner
1054 .reverse_adjacency
1055 .get(entity_id)
1056 .map(|v| v.len())
1057 .unwrap_or(0))
1058 }
1059
1060 pub fn out_degree_for(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
1064 let inner = recover_lock(self.inner.lock(), "out_degree_for");
1065 Ok(inner
1066 .adjacency
1067 .get(entity_id)
1068 .map(|v| v.len())
1069 .unwrap_or(0))
1070 }
1071
1072 pub fn predecessors(&self, entity_id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
1078 let inner = recover_lock(self.inner.lock(), "predecessors");
1079 let ids = inner
1080 .reverse_adjacency
1081 .get(entity_id)
1082 .cloned()
1083 .unwrap_or_default();
1084 Ok(ids
1085 .iter()
1086 .filter_map(|id| inner.entities.get(id).cloned())
1087 .collect())
1088 }
1089
1090 pub fn is_source(&self, entity_id: &EntityId) -> Result<bool, AgentRuntimeError> {
1095 Ok(self.in_degree_for(entity_id)? == 0)
1096 }
1097
1098 pub fn successors(&self, entity_id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
1103 let inner = recover_lock(self.inner.lock(), "successors");
1104 let rels = inner.adjacency.get(entity_id).cloned().unwrap_or_default();
1105 Ok(rels
1106 .iter()
1107 .filter_map(|r| inner.entities.get(&r.to).cloned())
1108 .collect())
1109 }
1110
1111 pub fn is_sink(&self, entity_id: &EntityId) -> Result<bool, AgentRuntimeError> {
1116 Ok(self.out_degree_for(entity_id)? == 0)
1117 }
1118
1119 pub fn reachable_from(
1124 &self,
1125 start: &EntityId,
1126 ) -> Result<std::collections::HashSet<EntityId>, AgentRuntimeError> {
1127 let inner = recover_lock(self.inner.lock(), "reachable_from");
1128 let mut visited = std::collections::HashSet::new();
1129 let mut queue = std::collections::VecDeque::new();
1130 if let Some(rels) = inner.adjacency.get(start) {
1131 for r in rels {
1132 if visited.insert(r.to.clone()) {
1133 queue.push_back(r.to.clone());
1134 }
1135 }
1136 }
1137 while let Some(current) = queue.pop_front() {
1138 if let Some(rels) = inner.adjacency.get(¤t) {
1139 for r in rels {
1140 if visited.insert(r.to.clone()) {
1141 queue.push_back(r.to.clone());
1142 }
1143 }
1144 }
1145 }
1146 Ok(visited)
1147 }
1148
1149 pub fn contains_cycle(&self) -> Result<bool, AgentRuntimeError> {
1154 let inner = recover_lock(self.inner.lock(), "contains_cycle");
1155 let mut color: HashMap<&EntityId, u8> = HashMap::new();
1156
1157 for start in inner.entities.keys() {
1158 if color.get(start).copied().unwrap_or(0) != 0 {
1159 continue;
1160 }
1161 let mut stack: Vec<(&EntityId, usize)> = vec![(start, 0)];
1162 color.insert(start, 1);
1163 while let Some((node, idx)) = stack.last_mut() {
1164 let neighbors = inner.adjacency.get(node).map(|v| v.as_slice()).unwrap_or(&[]);
1165 if *idx < neighbors.len() {
1166 let neighbor = &neighbors[*idx].to;
1167 *idx += 1;
1168 match color.get(neighbor).copied().unwrap_or(0) {
1169 1 => return Ok(true),
1170 0 => {
1171 color.insert(neighbor, 1);
1172 stack.push((neighbor, 0));
1173 }
1174 _ => {}
1175 }
1176 } else {
1177 color.insert(*node, 2);
1178 stack.pop();
1179 }
1180 }
1181 }
1182 Ok(false)
1183 }
1184
1185 pub fn edge_count(&self) -> Result<usize, AgentRuntimeError> {
1191 self.relationship_count()
1192 }
1193
1194 pub fn is_acyclic(&self) -> Result<bool, AgentRuntimeError> {
1198 Ok(!self.contains_cycle()?)
1199 }
1200
1201 pub fn avg_out_degree(&self) -> Result<f64, AgentRuntimeError> {
1205 let inner = recover_lock(self.inner.lock(), "GraphStore::avg_out_degree");
1206 let n = inner.entities.len();
1207 if n == 0 {
1208 return Ok(0.0);
1209 }
1210 let total: usize = inner.entities.keys().map(|id| {
1211 inner.adjacency.get(id).map_or(0, |v| v.len())
1212 }).sum();
1213 Ok(total as f64 / n as f64)
1214 }
1215
1216 pub fn avg_in_degree(&self) -> Result<f64, AgentRuntimeError> {
1220 let inner = recover_lock(self.inner.lock(), "GraphStore::avg_in_degree");
1221 let n = inner.entities.len();
1222 if n == 0 {
1223 return Ok(0.0);
1224 }
1225 let total: usize = inner.entities.keys().map(|id| {
1226 inner.reverse_adjacency.get(id).map_or(0, |v| v.len())
1227 }).sum();
1228 Ok(total as f64 / n as f64)
1229 }
1230
1231 pub fn max_out_degree(&self) -> Result<usize, AgentRuntimeError> {
1233 let inner = recover_lock(self.inner.lock(), "max_out_degree");
1234 Ok(inner
1235 .adjacency
1236 .values()
1237 .map(|v| v.len())
1238 .max()
1239 .unwrap_or(0))
1240 }
1241
1242 pub fn max_in_degree(&self) -> Result<usize, AgentRuntimeError> {
1244 let inner = recover_lock(self.inner.lock(), "max_in_degree");
1245 Ok(inner
1246 .reverse_adjacency
1247 .values()
1248 .map(|v| v.len())
1249 .max()
1250 .unwrap_or(0))
1251 }
1252
1253 pub fn min_out_degree(&self) -> Result<usize, AgentRuntimeError> {
1258 let inner = recover_lock(self.inner.lock(), "min_out_degree");
1259 Ok(inner
1260 .adjacency
1261 .values()
1262 .map(|v| v.len())
1263 .min()
1264 .unwrap_or(0))
1265 }
1266
1267 pub fn min_in_degree(&self) -> Result<usize, AgentRuntimeError> {
1272 let inner = recover_lock(self.inner.lock(), "min_in_degree");
1273 let min_from_reverse = inner
1276 .reverse_adjacency
1277 .values()
1278 .map(|v| v.len())
1279 .min()
1280 .unwrap_or(0);
1281 let has_zero_in_degree = inner
1283 .entities
1284 .keys()
1285 .any(|id| !inner.reverse_adjacency.contains_key(id));
1286 if has_zero_in_degree {
1287 Ok(0)
1288 } else {
1289 Ok(min_from_reverse)
1290 }
1291 }
1292
1293 pub fn relationship_kinds_from(
1299 &self,
1300 id: &EntityId,
1301 ) -> Result<Vec<String>, AgentRuntimeError> {
1302 let inner = recover_lock(self.inner.lock(), "relationship_kinds_from");
1303 let mut kinds: Vec<String> = inner
1304 .adjacency
1305 .get(id)
1306 .map(|rels| {
1307 rels.iter()
1308 .map(|r| r.kind.clone())
1309 .collect::<std::collections::HashSet<_>>()
1310 .into_iter()
1311 .collect()
1312 })
1313 .unwrap_or_default();
1314 kinds.sort_unstable();
1315 Ok(kinds)
1316 }
1317
1318 pub fn weight_stats(&self) -> Result<Option<(f64, f64, f64)>, AgentRuntimeError> {
1322 let inner = recover_lock(self.inner.lock(), "weight_stats");
1323 let weights: Vec<f64> = inner
1324 .adjacency
1325 .values()
1326 .flat_map(|rels| rels.iter())
1327 .map(|r| r.weight as f64)
1328 .collect();
1329 if weights.is_empty() {
1330 return Ok(None);
1331 }
1332 let min = weights.iter().cloned().fold(f64::INFINITY, f64::min);
1333 let max = weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1334 let mean = weights.iter().sum::<f64>() / weights.len() as f64;
1335 Ok(Some((min, max, mean)))
1336 }
1337
1338 pub fn isolated_nodes(&self) -> Result<HashSet<EntityId>, AgentRuntimeError> {
1343 let inner = recover_lock(self.inner.lock(), "GraphStore::isolated_nodes");
1344 let mut result = HashSet::new();
1345 for id in inner.entities.keys() {
1346 let has_outbound = inner
1347 .adjacency
1348 .get(id)
1349 .map_or(false, |v| !v.is_empty());
1350 let has_inbound = inner
1351 .reverse_adjacency
1352 .get(id)
1353 .map_or(false, |v| !v.is_empty());
1354 if !has_outbound && !has_inbound {
1355 result.insert(id.clone());
1356 }
1357 }
1358 Ok(result)
1359 }
1360
1361 pub fn sum_edge_weights(&self) -> Result<f64, AgentRuntimeError> {
1365 let inner = recover_lock(self.inner.lock(), "sum_edge_weights");
1366 Ok(inner
1367 .adjacency
1368 .values()
1369 .flat_map(|rels| rels.iter())
1370 .map(|r| r.weight as f64)
1371 .sum())
1372 }
1373
1374 pub fn entity_ids(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
1380 let inner = recover_lock(self.inner.lock(), "entity_ids");
1381 Ok(inner.entities.keys().cloned().collect())
1382 }
1383
1384 pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
1386 Ok(self.entity_count()? == 0)
1387 }
1388
1389 pub fn clear(&self) -> Result<(), AgentRuntimeError> {
1393 let mut inner = recover_lock(self.inner.lock(), "GraphStore::clear");
1394 inner.entities.clear();
1395 inner.relationships.clear();
1396 inner.adjacency.clear();
1397 inner.reverse_adjacency.clear();
1398 inner.cycle_cache = None;
1399 Ok(())
1400 }
1401
1402 pub fn entity_count_by_label(&self, label: &str) -> Result<usize, AgentRuntimeError> {
1404 let inner = recover_lock(self.inner.lock(), "entity_count_by_label");
1405 Ok(inner.entities.values().filter(|e| e.label == label).count())
1406 }
1407
1408 pub fn graph_density(&self) -> Result<f64, AgentRuntimeError> {
1413 let inner = recover_lock(self.inner.lock(), "graph_density");
1414 let v = inner.entities.len();
1415 if v < 2 {
1416 return Ok(0.0);
1417 }
1418 let e = inner.relationships.len() as f64;
1419 Ok(e / (v as f64 * (v - 1) as f64))
1420 }
1421
1422 pub fn entity_labels(&self) -> Result<Vec<String>, AgentRuntimeError> {
1424 let inner = recover_lock(self.inner.lock(), "entity_labels");
1425 let mut labels: Vec<String> = inner
1426 .entities
1427 .values()
1428 .map(|e| e.label.clone())
1429 .collect::<std::collections::HashSet<_>>()
1430 .into_iter()
1431 .collect();
1432 labels.sort_unstable();
1433 Ok(labels)
1434 }
1435
1436 pub fn relationship_kinds(&self) -> Result<Vec<String>, AgentRuntimeError> {
1438 let inner = recover_lock(self.inner.lock(), "relationship_kinds");
1439 let mut kinds: Vec<String> = inner
1440 .relationships
1441 .iter()
1442 .map(|r| r.kind.clone())
1443 .collect::<std::collections::HashSet<_>>()
1444 .into_iter()
1445 .collect();
1446 kinds.sort_unstable();
1447 Ok(kinds)
1448 }
1449
1450 pub fn relationship_kind_count(&self) -> Result<usize, AgentRuntimeError> {
1452 let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_kind_count");
1453 let count = inner
1454 .relationships
1455 .iter()
1456 .map(|r| r.kind.as_str())
1457 .collect::<std::collections::HashSet<_>>()
1458 .len();
1459 Ok(count)
1460 }
1461
1462 pub fn entities_with_self_loops(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
1466 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_self_loops");
1467 let mut ids: Vec<EntityId> = inner
1468 .adjacency
1469 .iter()
1470 .filter(|(from, rels)| rels.iter().any(|r| &r.to == *from))
1471 .map(|(id, _)| id.clone())
1472 .collect();
1473 ids.sort_unstable();
1474 Ok(ids)
1475 }
1476
1477 pub fn update_entity_label(
1481 &self,
1482 id: &EntityId,
1483 new_label: impl Into<String>,
1484 ) -> Result<bool, AgentRuntimeError> {
1485 let mut inner = recover_lock(self.inner.lock(), "update_entity_label");
1486 if let Some(entity) = inner.entities.get_mut(id) {
1487 entity.label = new_label.into();
1488 inner.cycle_cache = None;
1489 Ok(true)
1490 } else {
1491 Ok(false)
1492 }
1493 }
1494
1495 pub fn degree_centrality(&self) -> Result<HashMap<EntityId, f32>, AgentRuntimeError> {
1498 let inner = recover_lock(self.inner.lock(), "degree_centrality");
1499 let n = inner.entities.len();
1500
1501 let denom = if n <= 1 { 1.0 } else { (n - 1) as f32 };
1505 let mut result = HashMap::new();
1506 for id in inner.entities.keys() {
1507 let od = inner.adjacency.get(id).map_or(0, |v| v.len());
1508 let id_ = inner.reverse_adjacency.get(id).map_or(0, |v| v.len());
1509 let centrality = if n <= 1 {
1510 0.0
1511 } else {
1512 (od + id_) as f32 / denom
1513 };
1514 result.insert(id.clone(), centrality);
1515 }
1516
1517 Ok(result)
1518 }
1519
1520 pub fn betweenness_centrality(&self) -> Result<HashMap<EntityId, f32>, AgentRuntimeError> {
1526 let inner = recover_lock(self.inner.lock(), "betweenness_centrality");
1527 let n = inner.entities.len();
1528 let nodes: Vec<EntityId> = inner.entities.keys().cloned().collect();
1529
1530 let mut centrality: HashMap<EntityId, f32> =
1531 nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
1532
1533 let mut stack: Vec<EntityId> = Vec::with_capacity(n);
1536 let mut predecessors: HashMap<EntityId, Vec<EntityId>> =
1537 nodes.iter().map(|id| (id.clone(), vec![])).collect();
1538 let mut sigma: HashMap<EntityId, f32> =
1539 nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
1540 let mut dist: HashMap<EntityId, i64> =
1541 nodes.iter().map(|id| (id.clone(), -1i64)).collect();
1542 let mut delta: HashMap<EntityId, f32> =
1543 nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
1544 let mut queue: VecDeque<EntityId> = VecDeque::with_capacity(n);
1545
1546 for source in &nodes {
1547 stack.clear();
1550 for v in predecessors.values_mut() {
1551 v.clear();
1552 }
1553 for v in sigma.values_mut() {
1554 *v = 0.0;
1555 }
1556 for v in dist.values_mut() {
1557 *v = -1;
1558 }
1559 for v in delta.values_mut() {
1560 *v = 0.0;
1561 }
1562 queue.clear();
1563
1564 *sigma.entry(source.clone()).or_insert(0.0) = 1.0;
1565 *dist.entry(source.clone()).or_insert(-1) = 0;
1566 queue.push_back(source.clone());
1567
1568 while let Some(v) = queue.pop_front() {
1569 stack.push(v.clone());
1570 let d_v = *dist.get(&v).unwrap_or(&0);
1571 let sigma_v = *sigma.get(&v).unwrap_or(&0.0);
1572 if let Some(rels) = inner.adjacency.get(&v) {
1574 for rel in rels {
1575 let w = &rel.to;
1576 let d_w = dist.get(w).copied().unwrap_or(-1);
1577 if d_w < 0 {
1578 queue.push_back(w.clone());
1579 *dist.entry(w.clone()).or_insert(-1) = d_v + 1;
1580 }
1581 if dist.get(w).copied().unwrap_or(-1) == d_v + 1 {
1582 *sigma.entry(w.clone()).or_insert(0.0) += sigma_v;
1583 predecessors.entry(w.clone()).or_default().push(v.clone());
1584 }
1585 }
1586 }
1587 }
1588
1589 while let Some(w) = stack.pop() {
1592 let delta_w = *delta.get(&w).unwrap_or(&0.0);
1593 let sigma_w = *sigma.get(&w).unwrap_or(&1.0);
1594 for v in predecessors
1596 .get(&w)
1597 .map(|ps| ps.as_slice())
1598 .unwrap_or_default()
1599 {
1600 let sigma_v = *sigma.get(v).unwrap_or(&1.0);
1601 let contribution = (sigma_v / sigma_w) * (1.0 + delta_w);
1602 *delta.entry(v.clone()).or_insert(0.0) += contribution;
1603 }
1604 if &w != source {
1605 *centrality.entry(w.clone()).or_insert(0.0) += delta_w;
1606 }
1607 }
1608 }
1609
1610 if n > 2 {
1612 let norm = 2.0 / (((n - 1) * (n - 2)) as f32);
1613 for v in centrality.values_mut() {
1614 *v *= norm;
1615 }
1616 } else {
1617 for v in centrality.values_mut() {
1618 *v = 0.0;
1619 }
1620 }
1621
1622 Ok(centrality)
1623 }
1624
1625 pub fn label_propagation_communities(
1630 &self,
1631 max_iterations: usize,
1632 ) -> Result<HashMap<EntityId, usize>, AgentRuntimeError> {
1633 let inner = recover_lock(self.inner.lock(), "label_propagation_communities");
1634 let nodes: Vec<EntityId> = inner.entities.keys().cloned().collect();
1635
1636 let mut reverse_adj: HashMap<EntityId, Vec<EntityId>> =
1640 nodes.iter().map(|id| (id.clone(), vec![])).collect();
1641 for rel in &inner.relationships {
1642 reverse_adj
1643 .entry(rel.to.clone())
1644 .or_default()
1645 .push(rel.from.clone());
1646 }
1647
1648 let mut labels: HashMap<EntityId, usize> = nodes
1650 .iter()
1651 .enumerate()
1652 .map(|(i, id)| (id.clone(), i))
1653 .collect();
1654
1655 let mut freq: HashMap<usize, usize> = HashMap::new();
1658
1659 for _ in 0..max_iterations {
1660 let mut changed = false;
1661 for node in &nodes {
1663 let out_labels = inner
1665 .adjacency
1666 .get(node)
1667 .map(|rels| {
1668 rels.iter()
1669 .map(|r| labels.get(&r.to).copied().unwrap_or(0))
1670 })
1671 .into_iter()
1672 .flatten();
1673 let in_labels = reverse_adj
1674 .get(node)
1675 .map(|froms| froms.iter().map(|f| labels.get(f).copied().unwrap_or(0)))
1676 .into_iter()
1677 .flatten();
1678
1679 freq.clear();
1680 for lbl in out_labels.chain(in_labels) {
1681 *freq.entry(lbl).or_insert(0) += 1;
1682 }
1683
1684 if freq.is_empty() {
1685 continue;
1686 }
1687
1688 let best = freq
1690 .iter()
1691 .max_by_key(|&(_, count)| count)
1692 .map(|(&lbl, _)| lbl);
1693
1694 if let Some(new_label) = best {
1695 let current = labels.entry(node.clone()).or_insert(0);
1696 if *current != new_label {
1697 *current = new_label;
1698 changed = true;
1699 }
1700 }
1701 }
1702
1703 if !changed {
1704 break;
1705 }
1706 }
1707
1708 Ok(labels)
1709 }
1710
1711 pub fn detect_cycles(&self) -> Result<bool, AgentRuntimeError> {
1721 let mut inner = recover_lock(self.inner.lock(), "detect_cycles");
1722
1723 if let Some(cached) = inner.cycle_cache {
1724 return Ok(cached);
1725 }
1726
1727 let mut color: HashMap<&EntityId, u8> =
1729 inner.entities.keys().map(|id| (id, 0u8)).collect();
1730
1731 let has_cycle = 'outer: {
1732 for start in inner.entities.keys() {
1733 if *color.get(start).unwrap_or(&0) != 0 {
1734 continue;
1735 }
1736
1737 let mut stack: Vec<(&EntityId, usize)> = vec![(start, 0)];
1739 *color.entry(start).or_insert(0) = 1;
1740
1741 while let Some((node, idx)) = stack.last_mut() {
1742 let rels = inner
1745 .adjacency
1746 .get(*node)
1747 .map(|v| v.as_slice())
1748 .unwrap_or(&[]);
1749
1750 if *idx < rels.len() {
1751 let next = &rels[*idx].to;
1752 *idx += 1;
1753 match color.get(next).copied().unwrap_or(0) {
1754 1 => break 'outer true, 0 => {
1756 *color.entry(next).or_insert(0) = 1;
1757 stack.push((next, 0));
1758 }
1759 _ => {} }
1761 } else {
1762 *color.entry(*node).or_insert(0) = 2;
1764 stack.pop();
1765 }
1766 }
1767 }
1768 false
1769 };
1770
1771 inner.cycle_cache = Some(has_cycle);
1772 Ok(has_cycle)
1773 }
1774
1775 pub fn topological_sort(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
1783 let inner = recover_lock(self.inner.lock(), "topological_sort");
1784
1785 let mut color: HashMap<&EntityId, u8> =
1787 inner.entities.keys().map(|id| (id, 0u8)).collect();
1788 let mut result: Vec<EntityId> = Vec::with_capacity(inner.entities.len());
1789
1790 for start in inner.entities.keys() {
1791 if *color.get(start).unwrap_or(&0) != 0 {
1792 continue;
1793 }
1794 let mut stack: Vec<(&EntityId, usize, bool)> = vec![(start, 0, false)];
1796 *color.entry(start).or_insert(0) = 1;
1797
1798 while let Some((node, idx, pushed)) = stack.last_mut() {
1799 let rels = inner.adjacency.get(*node).map(|v| v.as_slice()).unwrap_or(&[]);
1800 if *idx < rels.len() {
1801 let next = &rels[*idx].to;
1802 *idx += 1;
1803 match color.get(next).copied().unwrap_or(0) {
1804 1 => return Err(AgentRuntimeError::Graph(
1805 "topological_sort: graph contains a cycle".into(),
1806 )),
1807 0 => {
1808 *color.entry(next).or_insert(0) = 1;
1809 stack.push((next, 0, false));
1810 }
1811 _ => {} }
1813 } else {
1814 if !*pushed {
1815 result.push((*node).clone());
1816 *pushed = true;
1817 }
1818 *color.entry(*node).or_insert(0) = 2;
1819 stack.pop();
1820 }
1821 }
1822 }
1823
1824 result.reverse();
1825 Ok(result)
1826 }
1827
1828 pub fn update_entity_property(
1832 &self,
1833 id: &EntityId,
1834 key: impl Into<String>,
1835 value: serde_json::Value,
1836 ) -> Result<bool, AgentRuntimeError> {
1837 let mut inner = recover_lock(self.inner.lock(), "update_entity_property");
1838 if let Some(entity) = inner.entities.get_mut(id) {
1839 entity.properties.insert(key.into(), value);
1840 Ok(true)
1841 } else {
1842 Ok(false)
1843 }
1844 }
1845
1846 pub fn is_dag(&self) -> Result<bool, AgentRuntimeError> {
1851 Ok(!self.detect_cycles()?)
1852 }
1853
1854 pub fn connected_components(&self) -> Result<usize, AgentRuntimeError> {
1859 let inner = recover_lock(self.inner.lock(), "connected_components");
1860 let ids: Vec<&EntityId> = inner.entities.keys().collect();
1861 if ids.is_empty() {
1862 return Ok(0);
1863 }
1864
1865 let idx: HashMap<&EntityId, usize> =
1867 ids.iter().enumerate().map(|(i, id)| (*id, i)).collect();
1868 let mut parent: Vec<usize> = (0..ids.len()).collect();
1869
1870 fn find(parent: &mut Vec<usize>, x: usize) -> usize {
1871 if parent[x] != x {
1872 parent[x] = find(parent, parent[x]);
1873 }
1874 parent[x]
1875 }
1876
1877 fn union(parent: &mut Vec<usize>, a: usize, b: usize) {
1878 let ra = find(parent, a);
1879 let rb = find(parent, b);
1880 if ra != rb {
1881 parent[ra] = rb;
1882 }
1883 }
1884
1885 for rel in &inner.relationships {
1886 if let (Some(&a), Some(&b)) = (idx.get(&rel.from), idx.get(&rel.to)) {
1887 union(&mut parent, a, b);
1888 }
1889 }
1890
1891 let components = ids
1892 .iter()
1893 .enumerate()
1894 .filter(|(i, _)| find(&mut parent, *i) == *i)
1895 .count();
1896 Ok(components)
1897 }
1898
1899 pub fn weakly_connected(&self) -> Result<bool, AgentRuntimeError> {
1907 Ok(self.connected_components()? <= 1)
1908 }
1909
1910 pub fn sink_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1915 let inner = recover_lock(self.inner.lock(), "sink_nodes");
1916 Ok(inner
1918 .entities
1919 .values()
1920 .filter(|e| {
1921 inner
1922 .adjacency
1923 .get(&e.id)
1924 .map_or(true, |v| v.is_empty())
1925 })
1926 .cloned()
1927 .collect())
1928 }
1929
1930 pub fn source_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1935 let inner = recover_lock(self.inner.lock(), "source_nodes");
1936 Ok(inner
1938 .entities
1939 .values()
1940 .filter(|e| {
1941 inner
1942 .reverse_adjacency
1943 .get(&e.id)
1944 .map_or(true, |v| v.is_empty())
1945 })
1946 .cloned()
1947 .collect())
1948 }
1949
1950 pub fn isolates(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
1952 let inner = recover_lock(self.inner.lock(), "isolates");
1953 Ok(inner
1956 .entities
1957 .values()
1958 .filter(|e| {
1959 inner
1960 .adjacency
1961 .get(&e.id)
1962 .map_or(true, |v| v.is_empty())
1963 && inner
1964 .reverse_adjacency
1965 .get(&e.id)
1966 .map_or(true, |v| v.is_empty())
1967 })
1968 .cloned()
1969 .collect())
1970 }
1971
1972 pub fn out_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
1976 let inner = recover_lock(self.inner.lock(), "out_degree");
1977 Ok(inner.adjacency.get(id).map_or(0, |rels| rels.len()))
1978 }
1979
1980 pub fn in_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
1984 let inner = recover_lock(self.inner.lock(), "in_degree");
1985 Ok(inner
1986 .reverse_adjacency
1987 .get(id)
1988 .map_or(0, |srcs| srcs.len()))
1989 }
1990
1991 pub fn total_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
1996 let out = self.out_degree(id)?;
1997 let r#in = self.in_degree(id)?;
1998 Ok(out + r#in)
1999 }
2000
2001 pub fn entity_property_keys(&self, id: &EntityId) -> Result<Vec<String>, AgentRuntimeError> {
2005 let inner = recover_lock(self.inner.lock(), "entity_property_keys");
2006 let entity = match inner.entities.get(id) {
2007 Some(e) => e,
2008 None => return Ok(vec![]),
2009 };
2010 let mut keys: Vec<String> = entity.properties.keys().cloned().collect();
2011 keys.sort_unstable();
2012 Ok(keys)
2013 }
2014
2015 pub fn path_exists(&self, from: &str, to: &str) -> Result<bool, AgentRuntimeError> {
2020 let from_id = EntityId::new(from);
2021 let to_id = EntityId::new(to);
2022 match self.shortest_path(&from_id, &to_id) {
2023 Ok(Some(_)) => Ok(true),
2024 Ok(None) => Ok(false),
2025 Err(e) => Err(e),
2026 }
2027 }
2028
2029 pub fn shortest_path_length(
2034 &self,
2035 from: &EntityId,
2036 to: &EntityId,
2037 ) -> Result<Option<usize>, AgentRuntimeError> {
2038 Ok(self.shortest_path(from, to)?.map(|path| path.len().saturating_sub(1)))
2039 }
2040
2041 pub fn bfs_bounded(
2045 &self,
2046 start: &str,
2047 max_depth: usize,
2048 max_nodes: usize,
2049 ) -> Result<Vec<EntityId>, AgentRuntimeError> {
2050 let inner = recover_lock(self.inner.lock(), "bfs_bounded");
2051 let start_id = EntityId::new(start);
2052 if !inner.entities.contains_key(&start_id) {
2053 return Err(AgentRuntimeError::Graph(format!(
2054 "start entity '{start}' not found"
2055 )));
2056 }
2057
2058 let mut visited: std::collections::HashMap<EntityId, usize> = std::collections::HashMap::new();
2059 let mut queue: VecDeque<(EntityId, usize)> = VecDeque::new();
2060 let mut result: Vec<EntityId> = Vec::new();
2061
2062 visited.insert(start_id.clone(), 0);
2063 queue.push_back((start_id.clone(), 0));
2064 result.push(start_id);
2065
2066 while let Some((current, depth)) = queue.pop_front() {
2067 if result.len() >= max_nodes {
2068 break;
2069 }
2070 if depth >= max_depth {
2071 continue;
2072 }
2073 for neighbour in Self::neighbours(&inner.adjacency, ¤t) {
2074 if !visited.contains_key(&neighbour) {
2075 let new_depth = depth + 1;
2076 visited.insert(neighbour.clone(), new_depth);
2077 result.push(neighbour.clone());
2078 if result.len() >= max_nodes {
2079 break;
2080 }
2081 queue.push_back((neighbour, new_depth));
2082 }
2083 }
2084 }
2085
2086 Ok(result)
2087 }
2088
2089 pub fn dfs_bounded(
2093 &self,
2094 start: &str,
2095 max_depth: usize,
2096 max_nodes: usize,
2097 ) -> Result<Vec<EntityId>, AgentRuntimeError> {
2098 let inner = recover_lock(self.inner.lock(), "dfs_bounded");
2099 let start_id = EntityId::new(start);
2100 if !inner.entities.contains_key(&start_id) {
2101 return Err(AgentRuntimeError::Graph(format!(
2102 "start entity '{start}' not found"
2103 )));
2104 }
2105
2106 let mut visited: HashSet<EntityId> = HashSet::new();
2107 let mut stack: Vec<(EntityId, usize)> = Vec::new();
2108 let mut result: Vec<EntityId> = Vec::new();
2109
2110 visited.insert(start_id.clone());
2111 stack.push((start_id.clone(), 0));
2112 result.push(start_id);
2113
2114 while let Some((current, depth)) = stack.pop() {
2115 if result.len() >= max_nodes {
2116 break;
2117 }
2118 if depth >= max_depth {
2119 continue;
2120 }
2121 for neighbour in Self::neighbours(&inner.adjacency, ¤t) {
2122 if visited.insert(neighbour.clone()) {
2123 result.push(neighbour.clone());
2124 if result.len() >= max_nodes {
2125 break;
2126 }
2127 stack.push((neighbour, depth + 1));
2128 }
2129 }
2130 }
2131
2132 Ok(result)
2133 }
2134
2135 pub fn all_entities(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
2137 let inner = recover_lock(self.inner.lock(), "all_entities");
2138 Ok(inner.entities.values().cloned().collect())
2139 }
2140
2141 pub fn all_relationships(&self) -> Result<Vec<Relationship>, AgentRuntimeError> {
2143 let inner = recover_lock(self.inner.lock(), "all_relationships");
2144 Ok(inner.relationships.clone())
2145 }
2146
2147 pub fn find_relationships_by_kind(&self, kind: &str) -> Result<Vec<Relationship>, AgentRuntimeError> {
2149 let inner = recover_lock(self.inner.lock(), "find_relationships_by_kind");
2150 Ok(inner.relationships.iter().filter(|r| r.kind == kind).cloned().collect())
2151 }
2152
2153 pub fn count_relationships_by_kind(&self, kind: &str) -> Result<usize, AgentRuntimeError> {
2155 let inner = recover_lock(self.inner.lock(), "count_relationships_by_kind");
2156 Ok(inner.relationships.iter().filter(|r| r.kind == kind).count())
2157 }
2158
2159 pub fn find_entities_by_label(&self, label: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
2161 let inner = recover_lock(self.inner.lock(), "find_entities_by_label");
2162 Ok(inner
2163 .entities
2164 .values()
2165 .filter(|e| e.label == label)
2166 .cloned()
2167 .collect())
2168 }
2169
2170 pub fn find_entities_by_labels(&self, labels: &[&str]) -> Result<Vec<Entity>, AgentRuntimeError> {
2175 let inner = recover_lock(self.inner.lock(), "find_entities_by_labels");
2176 let label_set: std::collections::HashSet<&str> = labels.iter().copied().collect();
2177 Ok(inner
2178 .entities
2179 .values()
2180 .filter(|e| label_set.contains(e.label.as_str()))
2181 .cloned()
2182 .collect())
2183 }
2184
2185 pub fn remove_isolated(&self) -> Result<usize, AgentRuntimeError> {
2189 let mut inner = recover_lock(self.inner.lock(), "remove_isolated");
2190 let isolated: Vec<EntityId> = inner
2191 .entities
2192 .keys()
2193 .filter(|id| {
2194 inner.adjacency.get(*id).map_or(true, |v| v.is_empty())
2195 && inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty())
2196 })
2197 .cloned()
2198 .collect();
2199 let count = isolated.len();
2200 for id in &isolated {
2201 inner.entities.remove(id);
2202 inner.adjacency.remove(id);
2203 inner.reverse_adjacency.remove(id);
2204 }
2205 if count > 0 {
2206 inner.cycle_cache = None;
2207 }
2208 Ok(count)
2209 }
2210
2211 pub fn get_relationships_for(
2213 &self,
2214 id: &EntityId,
2215 ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2216 let inner = recover_lock(self.inner.lock(), "get_relationships_for");
2217 Ok(inner
2218 .adjacency
2219 .get(id)
2220 .cloned()
2221 .unwrap_or_default())
2222 }
2223
2224 pub fn relationships_between(
2226 &self,
2227 from: &EntityId,
2228 to: &EntityId,
2229 ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2230 let inner = recover_lock(self.inner.lock(), "relationships_between");
2231 Ok(inner
2232 .relationships
2233 .iter()
2234 .filter(|r| {
2235 (r.from == *from && r.to == *to) || (r.from == *to && r.to == *from)
2236 })
2237 .cloned()
2238 .collect())
2239 }
2240
2241 pub fn find_entities_by_property(
2246 &self,
2247 key: &str,
2248 expected: &serde_json::Value,
2249 ) -> Result<Vec<Entity>, AgentRuntimeError> {
2250 let inner = recover_lock(self.inner.lock(), "find_entities_by_property");
2251 Ok(inner
2252 .entities
2253 .values()
2254 .filter(|e| e.properties.get(key) == Some(expected))
2255 .cloned()
2256 .collect())
2257 }
2258
2259 pub fn merge(&self, other: &GraphStore) -> Result<(), AgentRuntimeError> {
2264 let other_inner = recover_lock(other.inner.lock(), "merge:read");
2265 let other_entities: Vec<Entity> = other_inner.entities.values().cloned().collect();
2266 let other_rels: Vec<Relationship> = other_inner.relationships.clone();
2267 drop(other_inner);
2268
2269 let mut inner = recover_lock(self.inner.lock(), "merge:write");
2270 inner.cycle_cache = None;
2271 for entity in other_entities {
2272 inner.adjacency.entry(entity.id.clone()).or_default();
2273 inner.entities.insert(entity.id.clone(), entity);
2274 }
2275 for rel in other_rels {
2276 let already_exists = inner
2277 .relationships
2278 .iter()
2279 .any(|r| r.from == rel.from && r.to == rel.to && r.kind == rel.kind);
2280 if !already_exists && inner.entities.contains_key(&rel.from) && inner.entities.contains_key(&rel.to) {
2281 inner
2282 .adjacency
2283 .entry(rel.from.clone())
2284 .or_default()
2285 .push(rel.clone());
2286 inner.relationships.push(rel);
2287 }
2288 }
2289 Ok(())
2290 }
2291
2292 pub fn neighbor_entities(&self, id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
2297 let inner = recover_lock(self.inner.lock(), "neighbor_entities");
2298 let neighbors: Vec<Entity> = inner
2299 .adjacency
2300 .get(id)
2301 .iter()
2302 .flat_map(|rels| rels.iter())
2303 .filter_map(|r| inner.entities.get(&r.to).cloned())
2304 .collect();
2305 Ok(neighbors)
2306 }
2307
2308 pub fn neighbor_ids(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
2315 let inner = recover_lock(self.inner.lock(), "neighbor_ids");
2316 let ids: Vec<EntityId> = inner
2317 .adjacency
2318 .get(id)
2319 .iter()
2320 .flat_map(|rels| rels.iter())
2321 .map(|r| r.to.clone())
2322 .collect();
2323 Ok(ids)
2324 }
2325
2326 pub fn remove_all_relationships_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
2336 let mut inner = recover_lock(self.inner.lock(), "remove_all_relationships_for");
2337 inner.adjacency.remove(id);
2338 let before = inner.relationships.len();
2339 inner.relationships.retain(|r| &r.from != id);
2340 inner.cycle_cache = None;
2341 Ok(before - inner.relationships.len())
2342 }
2343
2344 pub fn entity_exists(&self, id: &EntityId) -> Result<bool, AgentRuntimeError> {
2346 let inner = recover_lock(self.inner.lock(), "entity_exists");
2347 Ok(inner.entities.contains_key(id))
2348 }
2349
2350 pub fn relationship_exists(
2355 &self,
2356 from: &EntityId,
2357 to: &EntityId,
2358 kind: &str,
2359 ) -> Result<bool, AgentRuntimeError> {
2360 let inner = recover_lock(self.inner.lock(), "relationship_exists");
2361 Ok(inner
2362 .adjacency
2363 .get(from)
2364 .map_or(false, |rels| rels.iter().any(|r| r.to == *to && r.kind == kind)))
2365 }
2366
2367 pub fn subgraph(&self, node_ids: &[EntityId]) -> Result<GraphStore, AgentRuntimeError> {
2370 let inner = recover_lock(self.inner.lock(), "subgraph");
2371 let id_set: HashSet<&EntityId> = node_ids.iter().collect();
2372
2373 let new_store = GraphStore::new();
2374
2375 let entities_to_copy: Vec<Entity> = node_ids
2378 .iter()
2379 .map(|id| {
2380 inner
2381 .entities
2382 .get(id)
2383 .cloned()
2384 .ok_or_else(|| {
2385 AgentRuntimeError::Graph(format!("entity '{}' not found", id.0))
2386 })
2387 })
2388 .collect::<Result<_, _>>()?;
2389
2390 {
2392 let mut new_inner = recover_lock(new_store.inner.lock(), "subgraph:add_entities");
2393 for entity in entities_to_copy {
2394 new_inner.adjacency.entry(entity.id.clone()).or_default();
2396 new_inner.entities.insert(entity.id.clone(), entity);
2397 }
2398 }
2399
2400 {
2403 let mut new_inner =
2404 recover_lock(new_store.inner.lock(), "subgraph:add_relationships");
2405 for rel in inner.relationships.iter() {
2406 if id_set.contains(&rel.from) && id_set.contains(&rel.to) {
2407 new_inner
2409 .adjacency
2410 .entry(rel.from.clone())
2411 .or_default()
2412 .push(rel.clone());
2413 new_inner.relationships.push(rel.clone());
2414 }
2415 }
2416 }
2417
2418 Ok(new_store)
2419 }
2420
2421 pub fn reverse(&self) -> Result<GraphStore, AgentRuntimeError> {
2426 let inner = recover_lock(self.inner.lock(), "reverse");
2427 let reversed = GraphStore::new();
2428
2429 {
2431 let mut r_inner = recover_lock(reversed.inner.lock(), "reverse:entities");
2432 for entity in inner.entities.values() {
2433 r_inner.adjacency.entry(entity.id.clone()).or_default();
2434 r_inner.entities.insert(entity.id.clone(), entity.clone());
2435 }
2436 }
2437
2438 for rel in &inner.relationships {
2440 let flipped = Relationship {
2441 from: rel.to.clone(),
2442 to: rel.from.clone(),
2443 kind: rel.kind.clone(),
2444 weight: rel.weight,
2445 };
2446 let mut r_inner = recover_lock(reversed.inner.lock(), "reverse:rels");
2447 r_inner
2448 .adjacency
2449 .entry(flipped.from.clone())
2450 .or_default()
2451 .push(flipped.clone());
2452 r_inner.relationships.push(flipped);
2453 }
2454
2455 Ok(reversed)
2456 }
2457
2458 pub fn common_neighbors(
2463 &self,
2464 a: &EntityId,
2465 b: &EntityId,
2466 ) -> Result<Vec<Entity>, AgentRuntimeError> {
2467 let inner = recover_lock(self.inner.lock(), "common_neighbors");
2468 let a_set: HashSet<&EntityId> = inner
2469 .adjacency
2470 .get(a)
2471 .map_or(HashSet::new(), |rels| rels.iter().map(|r| &r.to).collect());
2472 let b_set: HashSet<&EntityId> = inner
2473 .adjacency
2474 .get(b)
2475 .map_or(HashSet::new(), |rels| rels.iter().map(|r| &r.to).collect());
2476 let common: Vec<Entity> = a_set
2477 .intersection(&b_set)
2478 .filter_map(|id| inner.entities.get(*id).cloned())
2479 .collect();
2480 Ok(common)
2481 }
2482
2483 pub fn weight_of(
2487 &self,
2488 from: &EntityId,
2489 to: &EntityId,
2490 ) -> Result<Option<f32>, AgentRuntimeError> {
2491 let inner = recover_lock(self.inner.lock(), "weight_of");
2492 let weight = inner
2493 .adjacency
2494 .get(from)
2495 .and_then(|rels| rels.iter().find(|r| &r.to == to))
2496 .map(|r| r.weight);
2497 Ok(weight)
2498 }
2499
2500 pub fn neighbors_in(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
2505 let inner = recover_lock(self.inner.lock(), "neighbors_in");
2506 Ok(inner
2508 .reverse_adjacency
2509 .get(id)
2510 .cloned()
2511 .unwrap_or_default())
2512 }
2513
2514 pub fn density(&self) -> Result<f64, AgentRuntimeError> {
2519 let inner = recover_lock(self.inner.lock(), "density");
2520 let n = inner.entities.len();
2521 if n < 2 {
2522 return Ok(0.0);
2523 }
2524 let max_edges = n * (n - 1);
2525 Ok(inner.relationships.len() as f64 / max_edges as f64)
2526 }
2527
2528 pub fn avg_degree(&self) -> Result<f64, AgentRuntimeError> {
2532 let inner = recover_lock(self.inner.lock(), "avg_degree");
2533 let n = inner.entities.len();
2534 if n == 0 {
2535 return Ok(0.0);
2536 }
2537 Ok(inner.relationships.len() as f64 / n as f64)
2538 }
2539
2540 pub fn total_weight(&self) -> Result<f32, AgentRuntimeError> {
2544 let inner = recover_lock(self.inner.lock(), "total_weight");
2545 Ok(inner.relationships.iter().map(|r| r.weight).sum())
2546 }
2547
2548 pub fn max_edge_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
2550 let inner = recover_lock(self.inner.lock(), "max_edge_weight");
2551 Ok(inner
2552 .relationships
2553 .iter()
2554 .map(|r| r.weight)
2555 .reduce(f32::max))
2556 }
2557
2558 pub fn min_edge_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
2560 let inner = recover_lock(self.inner.lock(), "min_edge_weight");
2561 Ok(inner
2562 .relationships
2563 .iter()
2564 .map(|r| r.weight)
2565 .reduce(f32::min))
2566 }
2567
2568 pub fn top_n_by_out_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2573 if n == 0 {
2574 return Ok(Vec::new());
2575 }
2576 let inner = recover_lock(self.inner.lock(), "top_n_by_out_degree");
2577 let mut pairs: Vec<(&EntityId, usize)> = inner
2578 .adjacency
2579 .iter()
2580 .map(|(id, rels)| (id, rels.len()))
2581 .collect();
2582 pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
2583 Ok(pairs
2584 .into_iter()
2585 .take(n)
2586 .filter_map(|(id, _)| inner.entities.get(id).cloned())
2587 .collect())
2588 }
2589
2590 pub fn remove_entity_and_edges(&self, id: &EntityId) -> Result<(), AgentRuntimeError> {
2598 let mut inner = recover_lock(self.inner.lock(), "remove_entity_and_edges");
2599 if !inner.entities.contains_key(id) {
2600 return Err(AgentRuntimeError::Graph(format!(
2601 "entity '{}' not found",
2602 id.0
2603 )));
2604 }
2605 inner.entities.remove(id);
2606 inner.relationships.retain(|r| &r.from != id && &r.to != id);
2607 inner.adjacency.remove(id);
2608 for adj in inner.adjacency.values_mut() {
2609 adj.retain(|r| &r.to != id);
2610 }
2611 inner.reverse_adjacency.remove(id);
2612 for rev in inner.reverse_adjacency.values_mut() {
2613 rev.retain(|src| src != id);
2614 }
2615 inner.cycle_cache = None;
2616 Ok(())
2617 }
2618
2619 pub fn hub_nodes(&self, threshold: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2623 let inner = recover_lock(self.inner.lock(), "hub_nodes");
2624 Ok(inner
2625 .entities
2626 .values()
2627 .filter(|e| {
2628 inner
2629 .adjacency
2630 .get(&e.id)
2631 .map_or(0, |rels| rels.len())
2632 >= threshold
2633 })
2634 .cloned()
2635 .collect())
2636 }
2637
2638 pub fn incident_relationships(
2641 &self,
2642 entity_id: &EntityId,
2643 ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2644 let inner = recover_lock(self.inner.lock(), "incident_relationships");
2645 Ok(inner
2646 .relationships
2647 .iter()
2648 .filter(|r| &r.from == entity_id || &r.to == entity_id)
2649 .cloned()
2650 .collect())
2651 }
2652
2653 pub fn max_out_degree_entity(&self) -> Result<Option<Entity>, AgentRuntimeError> {
2659 let inner = recover_lock(self.inner.lock(), "max_out_degree_entity");
2660 let best = inner
2661 .adjacency
2662 .iter()
2663 .max_by_key(|(_, rels)| rels.len())
2664 .and_then(|(id, _)| inner.entities.get(id).cloned());
2665 Ok(best)
2666 }
2667
2668 pub fn max_in_degree_entity(&self) -> Result<Option<Entity>, AgentRuntimeError> {
2673 let inner = recover_lock(self.inner.lock(), "max_in_degree_entity");
2674 let best = inner
2675 .reverse_adjacency
2676 .iter()
2677 .max_by_key(|(_, srcs)| srcs.len())
2678 .and_then(|(id, _)| inner.entities.get(id).cloned());
2679 Ok(best)
2680 }
2681
2682 pub fn leaf_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
2690 let inner = recover_lock(self.inner.lock(), "leaf_nodes");
2691 Ok(inner
2692 .entities
2693 .values()
2694 .filter(|e| {
2695 inner
2696 .adjacency
2697 .get(&e.id)
2698 .map_or(true, |rels| rels.is_empty())
2699 })
2700 .cloned()
2701 .collect())
2702 }
2703
2704 pub fn top_nodes_by_out_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2710 let inner = recover_lock(self.inner.lock(), "top_nodes_by_out_degree");
2711 let mut pairs: Vec<(&EntityId, usize)> = inner
2712 .entities
2713 .keys()
2714 .map(|id| (id, inner.adjacency.get(id).map_or(0, |v| v.len())))
2715 .collect();
2716 pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
2717 pairs.truncate(n);
2718 Ok(pairs
2719 .into_iter()
2720 .filter_map(|(id, _)| inner.entities.get(id).cloned())
2721 .collect())
2722 }
2723
2724 pub fn top_nodes_by_in_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
2729 let inner = recover_lock(self.inner.lock(), "top_nodes_by_in_degree");
2730 let mut pairs: Vec<(&EntityId, usize)> = inner
2731 .entities
2732 .keys()
2733 .map(|id| (id, inner.reverse_adjacency.get(id).map_or(0, |v| v.len())))
2734 .collect();
2735 pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
2736 pairs.truncate(n);
2737 Ok(pairs
2738 .into_iter()
2739 .filter_map(|(id, _)| inner.entities.get(id).cloned())
2740 .collect())
2741 }
2742
2743 pub fn relationship_type_counts(&self) -> Result<HashMap<String, usize>, AgentRuntimeError> {
2748 let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_type_counts");
2749 let mut counts: HashMap<String, usize> = HashMap::new();
2750 for rel in &inner.relationships {
2751 *counts.entry(rel.kind.clone()).or_insert(0) += 1;
2752 }
2753 Ok(counts)
2754 }
2755
2756 pub fn entities_without_property(&self, key: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
2761 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_without_property");
2762 Ok(inner
2763 .entities
2764 .values()
2765 .filter(|e| !e.properties.contains_key(key))
2766 .cloned()
2767 .collect())
2768 }
2769
2770 pub fn unique_relationship_types(&self) -> Result<Vec<String>, AgentRuntimeError> {
2775 let inner = recover_lock(self.inner.lock(), "GraphStore::unique_relationship_types");
2776 let mut kinds: Vec<String> = inner
2777 .relationships
2778 .iter()
2779 .map(|r| r.kind.clone())
2780 .collect::<std::collections::HashSet<_>>()
2781 .into_iter()
2782 .collect();
2783 kinds.sort_unstable();
2784 Ok(kinds)
2785 }
2786
2787 pub fn avg_property_count(&self) -> Result<f64, AgentRuntimeError> {
2791 let inner = recover_lock(self.inner.lock(), "GraphStore::avg_property_count");
2792 let n = inner.entities.len();
2793 if n == 0 {
2794 return Ok(0.0);
2795 }
2796 let total: usize = inner.entities.values().map(|e| e.properties.len()).sum();
2797 Ok(total as f64 / n as f64)
2798 }
2799
2800 pub fn property_key_frequency(&self) -> Result<HashMap<String, usize>, AgentRuntimeError> {
2809 let inner = recover_lock(self.inner.lock(), "GraphStore::property_key_frequency");
2810 let mut freq: HashMap<String, usize> = HashMap::new();
2811 for entity in inner.entities.values() {
2812 for key in entity.properties.keys() {
2813 *freq.entry(key.clone()).or_insert(0) += 1;
2814 }
2815 }
2816 Ok(freq)
2817 }
2818
2819 pub fn entities_sorted_by_label(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
2824 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_sorted_by_label");
2825 let mut entities: Vec<Entity> = inner.entities.values().cloned().collect();
2826 entities.sort_unstable_by(|a, b| a.label.cmp(&b.label).then_with(|| a.id.cmp(&b.id)));
2827 Ok(entities)
2828 }
2829
2830 pub fn entities_with_property(&self, key: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
2834 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_property");
2835 let entities: Vec<Entity> = inner
2836 .entities
2837 .values()
2838 .filter(|e| e.properties.contains_key(key))
2839 .cloned()
2840 .collect();
2841 Ok(entities)
2842 }
2843
2844 pub fn total_relationship_count(&self) -> Result<usize, AgentRuntimeError> {
2849 let inner = recover_lock(self.inner.lock(), "GraphStore::total_relationship_count");
2850 Ok(inner.adjacency.values().map(|rels| rels.len()).sum())
2851 }
2852
2853 pub fn path_to_string(&self, path: &[EntityId]) -> Result<String, AgentRuntimeError> {
2865 let inner = recover_lock(self.inner.lock(), "GraphStore::path_to_string");
2866 let parts: Vec<String> = path
2867 .iter()
2868 .map(|id| {
2869 inner
2870 .entities
2871 .get(id)
2872 .map(|e| e.label.clone())
2873 .unwrap_or_else(|| id.0.clone())
2874 })
2875 .collect();
2876 Ok(parts.join(" \u{2192} "))
2877 }
2878
2879 pub fn relationships_of_kind(&self, kind: &str) -> Result<Vec<Relationship>, AgentRuntimeError> {
2883 let inner = recover_lock(self.inner.lock(), "GraphStore::relationships_of_kind");
2884 let mut result = Vec::new();
2885 for rels in inner.adjacency.values() {
2886 for rel in rels {
2887 if rel.kind == kind {
2888 result.push(rel.clone());
2889 }
2890 }
2891 }
2892 Ok(result)
2893 }
2894
2895 pub fn entities_without_label(&self, label: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
2900 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_without_label");
2901 Ok(inner
2902 .entities
2903 .values()
2904 .filter(|e| e.label != label)
2905 .cloned()
2906 .collect())
2907 }
2908
2909 pub fn entities_without_outgoing(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
2914 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_without_outgoing");
2915 let entities: Vec<Entity> = inner
2916 .entities
2917 .values()
2918 .filter(|e| !inner.adjacency.contains_key(&e.id) || inner.adjacency[&e.id].is_empty())
2919 .cloned()
2920 .collect();
2921 Ok(entities)
2922 }
2923
2924 pub fn entities_without_incoming(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
2929 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_without_incoming");
2930 let entities: Vec<Entity> = inner
2931 .entities
2932 .values()
2933 .filter(|e| !inner.reverse_adjacency.contains_key(&e.id) || inner.reverse_adjacency[&e.id].is_empty())
2934 .cloned()
2935 .collect();
2936 Ok(entities)
2937 }
2938
2939 pub fn total_out_degree(&self) -> Result<usize, AgentRuntimeError> {
2945 let inner = recover_lock(self.inner.lock(), "GraphStore::total_out_degree");
2946 Ok(inner.adjacency.values().map(|rels| rels.len()).sum())
2947 }
2948
2949 pub fn relationships_with_weight_above(
2955 &self,
2956 threshold: f32,
2957 ) -> Result<Vec<Relationship>, AgentRuntimeError> {
2958 let inner = recover_lock(
2959 self.inner.lock(),
2960 "GraphStore::relationships_with_weight_above",
2961 );
2962 let rels: Vec<Relationship> = inner
2963 .adjacency
2964 .values()
2965 .flat_map(|rels| rels.iter())
2966 .filter(|r| r.weight > threshold)
2967 .cloned()
2968 .collect();
2969 Ok(rels)
2970 }
2971
2972 pub fn entity_with_most_properties(&self) -> Result<Option<Entity>, AgentRuntimeError> {
2977 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_with_most_properties");
2978 let entity = inner.entities.values().max_by(|a, b| {
2979 a.properties
2980 .len()
2981 .cmp(&b.properties.len())
2982 .then_with(|| b.id.0.cmp(&a.id.0))
2983 });
2984 Ok(entity.cloned())
2985 }
2986
2987 pub fn avg_weight(&self) -> Result<Option<f64>, AgentRuntimeError> {
2990 let inner = recover_lock(self.inner.lock(), "GraphStore::avg_weight");
2991 let all: Vec<f32> = inner
2992 .adjacency
2993 .values()
2994 .flat_map(|rels| rels.iter().map(|r| r.weight))
2995 .collect();
2996 if all.is_empty() {
2997 return Ok(None);
2998 }
2999 let sum: f64 = all.iter().map(|&w| w as f64).sum();
3000 Ok(Some(sum / all.len() as f64))
3001 }
3002
3003 pub fn entity_count_with_label(&self, label: &str) -> Result<usize, AgentRuntimeError> {
3007 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_count_with_label");
3008 Ok(inner.entities.values().filter(|e| e.label == label).count())
3009 }
3010
3011 pub fn edge_count_above_weight(&self, threshold: f32) -> Result<usize, AgentRuntimeError> {
3018 let inner = recover_lock(self.inner.lock(), "GraphStore::edge_count_above_weight");
3019 Ok(inner
3020 .adjacency
3021 .values()
3022 .flat_map(|rels| rels.iter())
3023 .filter(|r| r.weight > threshold)
3024 .count())
3025 }
3026
3027 pub fn entities_with_label_prefix(&self, prefix: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
3031 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_label_prefix");
3032 let entities: Vec<Entity> = inner
3033 .entities
3034 .values()
3035 .filter(|e| e.label.starts_with(prefix))
3036 .cloned()
3037 .collect();
3038 Ok(entities)
3039 }
3040
3041 pub fn bidirectional_pairs(&self) -> Result<Vec<(EntityId, EntityId)>, AgentRuntimeError> {
3046 let inner = recover_lock(self.inner.lock(), "GraphStore::bidirectional_pairs");
3047 let mut pairs: Vec<(EntityId, EntityId)> = Vec::new();
3048 for (from, rels) in &inner.adjacency {
3049 for rel in rels {
3050 let to = &rel.to;
3051 if from < to {
3052 if inner
3053 .adjacency
3054 .get(to)
3055 .map_or(false, |v| v.iter().any(|r| &r.to == from))
3056 {
3057 pairs.push((from.clone(), to.clone()));
3058 }
3059 }
3060 }
3061 }
3062 Ok(pairs)
3063 }
3064
3065 pub fn mean_in_degree(&self) -> Result<f64, AgentRuntimeError> {
3070 let inner = recover_lock(self.inner.lock(), "GraphStore::mean_in_degree");
3071 let entity_count = inner.entities.len();
3072 if entity_count == 0 {
3073 return Ok(0.0);
3074 }
3075 let total: usize = inner.reverse_adjacency.values().map(|v| v.len()).sum();
3076 Ok(total as f64 / entity_count as f64)
3077 }
3078
3079 pub fn entity_count_by_label_prefix(&self, prefix: &str) -> Result<usize, AgentRuntimeError> {
3083 let inner = recover_lock(
3084 self.inner.lock(),
3085 "GraphStore::entity_count_by_label_prefix",
3086 );
3087 Ok(inner.entities.values().filter(|e| e.label.starts_with(prefix)).count())
3088 }
3089
3090 pub fn relationship_weight_sum(&self) -> Result<f32, AgentRuntimeError> {
3094 let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_weight_sum");
3095 Ok(inner
3096 .adjacency
3097 .values()
3098 .flat_map(|rels| rels.iter())
3099 .map(|r| r.weight)
3100 .sum())
3101 }
3102
3103 pub fn label_frequency(&self) -> Result<std::collections::HashMap<String, usize>, AgentRuntimeError> {
3107 let inner = recover_lock(self.inner.lock(), "GraphStore::label_frequency");
3108 let mut freq: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
3109 for entity in inner.entities.values() {
3110 *freq.entry(entity.label.clone()).or_insert(0) += 1;
3111 }
3112 Ok(freq)
3113 }
3114
3115 pub fn entities_sorted_by_id(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
3121 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_sorted_by_id");
3122 let mut entities: Vec<Entity> = inner.entities.values().cloned().collect();
3123 entities.sort_unstable_by(|a, b| a.id.cmp(&b.id));
3124 Ok(entities)
3125 }
3126
3127 pub fn entities_with_label_containing(&self, substr: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
3133 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_label_containing");
3134 Ok(inner.entities.values().filter(|e| e.label.contains(substr)).cloned().collect())
3135 }
3136
3137 pub fn entity_neighbor_count(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
3141 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_neighbor_count");
3142 Ok(inner.adjacency.get(entity_id).map_or(0, |rels| rels.len()))
3143 }
3144
3145 pub fn entity_labels_sorted(&self) -> Result<Vec<String>, AgentRuntimeError> {
3150 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_labels_sorted");
3151 let mut labels: Vec<String> = inner
3152 .entities
3153 .values()
3154 .map(|e| e.label.clone())
3155 .collect::<std::collections::HashSet<_>>()
3156 .into_iter()
3157 .collect();
3158 labels.sort_unstable();
3159 Ok(labels)
3160 }
3161
3162 pub fn relationship_type_count(&self) -> Result<usize, AgentRuntimeError> {
3167 let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_type_count");
3168 let kinds: std::collections::HashSet<&str> = inner
3169 .adjacency
3170 .values()
3171 .flat_map(|rels| rels.iter().map(|r| r.kind.as_str()))
3172 .collect();
3173 Ok(kinds.len())
3174 }
3175
3176 pub fn entities_with_exact_label(&self, label: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
3183 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_exact_label");
3184 Ok(inner.entities.values().filter(|e| e.label == label).cloned().collect())
3185 }
3186
3187 pub fn entity_ids_sorted(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
3191 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_ids_sorted");
3192 let mut ids: Vec<EntityId> = inner.entities.keys().cloned().collect();
3193 ids.sort_by(|a, b| a.0.cmp(&b.0));
3194 Ok(ids)
3195 }
3196
3197 pub fn relationship_count_for(
3201 &self,
3202 entity_id: &EntityId,
3203 ) -> Result<usize, AgentRuntimeError> {
3204 let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_count_for");
3205 Ok(inner
3206 .adjacency
3207 .get(entity_id)
3208 .map_or(0, |rels| rels.len()))
3209 }
3210
3211 pub fn entity_pair_has_relationship(
3215 &self,
3216 from: &EntityId,
3217 to: &EntityId,
3218 ) -> Result<bool, AgentRuntimeError> {
3219 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_pair_has_relationship");
3220 let to_id = to.clone();
3221 Ok(inner
3222 .adjacency
3223 .get(from)
3224 .map_or(false, |rels| rels.iter().any(|r| r.to == to_id)))
3225 }
3226
3227 pub fn nodes_reachable_from(
3232 &self,
3233 start: &EntityId,
3234 ) -> Result<Vec<EntityId>, AgentRuntimeError> {
3235 let inner = recover_lock(self.inner.lock(), "GraphStore::nodes_reachable_from");
3236 let mut visited: std::collections::HashSet<EntityId> = std::collections::HashSet::new();
3237 let mut queue: std::collections::VecDeque<EntityId> = std::collections::VecDeque::new();
3238 queue.push_back(start.clone());
3239 while let Some(current) = queue.pop_front() {
3240 if !visited.insert(current.clone()) {
3241 continue;
3242 }
3243 if let Some(rels) = inner.adjacency.get(¤t) {
3244 for r in rels {
3245 if !visited.contains(&r.to) {
3246 queue.push_back(r.to.clone());
3247 }
3248 }
3249 }
3250 }
3251 visited.remove(start);
3252 let mut result: Vec<EntityId> = visited.into_iter().collect();
3253 result.sort_by(|a, b| a.0.cmp(&b.0));
3254 Ok(result)
3255 }
3256
3257 pub fn average_weight(&self) -> Result<f64, AgentRuntimeError> {
3261 let inner = recover_lock(self.inner.lock(), "GraphStore::average_weight");
3262 let all: Vec<f64> = inner
3263 .adjacency
3264 .values()
3265 .flat_map(|rels| rels.iter().map(|r| r.weight as f64))
3266 .collect();
3267 if all.is_empty() {
3268 return Ok(0.0);
3269 }
3270 Ok(all.iter().sum::<f64>() / all.len() as f64)
3271 }
3272
3273 pub fn max_weight(&self) -> Result<Option<f64>, AgentRuntimeError> {
3276 let inner = recover_lock(self.inner.lock(), "GraphStore::max_weight");
3277 Ok(inner
3278 .adjacency
3279 .values()
3280 .flat_map(|rels| rels.iter().map(|r| r.weight as f64))
3281 .reduce(f64::max))
3282 }
3283
3284 pub fn edge_weight_between(
3287 &self,
3288 from: &EntityId,
3289 to: &EntityId,
3290 ) -> Result<Option<f64>, AgentRuntimeError> {
3291 let inner = recover_lock(self.inner.lock(), "GraphStore::edge_weight_between");
3292 let to_id = to.clone();
3293 Ok(inner
3294 .adjacency
3295 .get(from)
3296 .and_then(|rels| rels.iter().find(|r| r.to == to_id))
3297 .map(|r| r.weight as f64))
3298 }
3299
3300 pub fn total_relationship_weight(&self) -> Result<f64, AgentRuntimeError> {
3304 let inner = recover_lock(self.inner.lock(), "GraphStore::total_relationship_weight");
3305 Ok(inner
3306 .adjacency
3307 .values()
3308 .flat_map(|rels| rels.iter().map(|r| r.weight as f64))
3309 .sum())
3310 }
3311
3312 pub fn avg_weight_for_kind(&self, kind: &str) -> Result<f64, AgentRuntimeError> {
3316 let inner = recover_lock(self.inner.lock(), "GraphStore::avg_weight_for_kind");
3317 let weights: Vec<f64> = inner
3318 .adjacency
3319 .values()
3320 .flat_map(|rels| {
3321 rels.iter()
3322 .filter(|r| r.kind.as_str() == kind)
3323 .map(|r| r.weight as f64)
3324 })
3325 .collect();
3326 if weights.is_empty() {
3327 return Ok(0.0);
3328 }
3329 Ok(weights.iter().sum::<f64>() / weights.len() as f64)
3330 }
3331
3332 pub fn entity_degree_ratio(&self, entity_id: &EntityId) -> Result<f64, AgentRuntimeError> {
3337 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_degree_ratio");
3338 let total = inner.entities.len();
3339 if total == 0 {
3340 return Ok(0.0);
3341 }
3342 let out_deg = inner
3343 .adjacency
3344 .get(entity_id)
3345 .map_or(0, |rels| rels.len());
3346 Ok(out_deg as f64 / total as f64)
3347 }
3348
3349 pub fn nodes_with_no_outgoing(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
3354 let inner = recover_lock(self.inner.lock(), "GraphStore::nodes_with_no_outgoing");
3355 Ok(inner
3356 .entities
3357 .values()
3358 .filter(|e| {
3359 inner
3360 .adjacency
3361 .get(&e.id)
3362 .map_or(true, |rels| rels.is_empty())
3363 })
3364 .cloned()
3365 .collect())
3366 }
3367
3368 pub fn in_degree_of(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
3373 let inner = recover_lock(self.inner.lock(), "GraphStore::in_degree_of");
3374 let count = inner
3375 .adjacency
3376 .values()
3377 .flat_map(|rels| rels.iter())
3378 .filter(|r| &r.to == id)
3379 .count();
3380 Ok(count)
3381 }
3382
3383 pub fn total_weight_for_kind(&self, kind: &str) -> Result<f64, AgentRuntimeError> {
3388 let inner = recover_lock(self.inner.lock(), "GraphStore::total_weight_for_kind");
3389 let sum: f64 = inner
3390 .adjacency
3391 .values()
3392 .flat_map(|rels| rels.iter())
3393 .filter(|r| r.kind == kind)
3394 .map(|r| r.weight as f64)
3395 .sum();
3396 Ok(sum)
3397 }
3398
3399 pub fn entity_ids_with_label(&self, label: &str) -> Result<Vec<EntityId>, AgentRuntimeError> {
3403 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_ids_with_label");
3404 Ok(inner
3405 .entities
3406 .values()
3407 .filter(|e| e.label == label)
3408 .map(|e| e.id.clone())
3409 .collect())
3410 }
3411
3412 pub fn labels_unique_count(&self) -> Result<usize, AgentRuntimeError> {
3414 let inner = recover_lock(self.inner.lock(), "GraphStore::labels_unique_count");
3415 let labels: std::collections::HashSet<&str> =
3416 inner.entities.values().map(|e| e.label.as_str()).collect();
3417 Ok(labels.len())
3418 }
3419
3420 pub fn entities_with_property_key(
3423 &self,
3424 property_key: &str,
3425 ) -> Result<Vec<Entity>, AgentRuntimeError> {
3426 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_property_key");
3427 Ok(inner
3428 .entities
3429 .values()
3430 .filter(|e| e.properties.contains_key(property_key))
3431 .cloned()
3432 .collect())
3433 }
3434
3435 pub fn entity_properties_count(&self, property_key: &str) -> Result<usize, AgentRuntimeError> {
3437 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_properties_count");
3438 Ok(inner
3439 .entities
3440 .values()
3441 .filter(|e| e.properties.contains_key(property_key))
3442 .count())
3443 }
3444
3445 pub fn edges_to(&self, id: &EntityId) -> Result<Vec<Relationship>, AgentRuntimeError> {
3447 let inner = recover_lock(self.inner.lock(), "GraphStore::edges_to");
3448 Ok(inner
3449 .adjacency
3450 .values()
3451 .flat_map(|rels| rels.iter())
3452 .filter(|r| &r.to == id)
3453 .cloned()
3454 .collect())
3455 }
3456
3457 pub fn entity_has_property_value(
3459 &self,
3460 id: &EntityId,
3461 key: &str,
3462 value: &str,
3463 ) -> Result<bool, AgentRuntimeError> {
3464 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_has_property_value");
3465 Ok(inner
3466 .entities
3467 .get(id)
3468 .and_then(|e| e.properties.get(key))
3469 .map_or(false, |v| v == value))
3470 }
3471
3472 pub fn relationships_from(&self, id: &EntityId) -> Result<Vec<Relationship>, AgentRuntimeError> {
3477 let inner = recover_lock(self.inner.lock(), "GraphStore::relationships_from");
3478 Ok(inner
3479 .adjacency
3480 .get(id)
3481 .map(|rels| rels.clone())
3482 .unwrap_or_default())
3483 }
3484
3485 pub fn entities_with_min_out_degree(
3490 &self,
3491 min_degree: usize,
3492 ) -> Result<Vec<Entity>, AgentRuntimeError> {
3493 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_min_out_degree");
3494 let entities: Vec<Entity> = inner
3495 .entities
3496 .values()
3497 .filter(|e| {
3498 inner
3499 .adjacency
3500 .get(&e.id)
3501 .map_or(0, |rels| rels.len())
3502 >= min_degree
3503 })
3504 .cloned()
3505 .collect();
3506 Ok(entities)
3507 }
3508
3509 pub fn entities_with_min_in_degree(
3514 &self,
3515 min_degree: usize,
3516 ) -> Result<Vec<Entity>, AgentRuntimeError> {
3517 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_min_in_degree");
3518 let mut in_degree: std::collections::HashMap<&EntityId, usize> =
3520 std::collections::HashMap::new();
3521 for rels in inner.adjacency.values() {
3522 for rel in rels {
3523 *in_degree.entry(&rel.to).or_insert(0) += 1;
3524 }
3525 }
3526 let entities: Vec<Entity> = inner
3527 .entities
3528 .values()
3529 .filter(|e| in_degree.get(&e.id).copied().unwrap_or(0) >= min_degree)
3530 .cloned()
3531 .collect();
3532 Ok(entities)
3533 }
3534
3535 pub fn total_property_count(&self) -> Result<usize, AgentRuntimeError> {
3540 let inner = recover_lock(self.inner.lock(), "GraphStore::total_property_count");
3541 Ok(inner.entities.values().map(|e| e.property_count()).sum())
3542 }
3543
3544 pub fn entities_with_no_properties(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
3548 let inner =
3549 recover_lock(self.inner.lock(), "GraphStore::entities_with_no_properties");
3550 Ok(inner
3551 .entities
3552 .values()
3553 .filter(|e| e.properties_is_empty())
3554 .cloned()
3555 .collect())
3556 }
3557
3558 pub fn weight_above_threshold_ratio(
3563 &self,
3564 threshold: f32,
3565 ) -> Result<f64, AgentRuntimeError> {
3566 let inner =
3567 recover_lock(self.inner.lock(), "GraphStore::weight_above_threshold_ratio");
3568 let all: Vec<f32> = inner
3569 .adjacency
3570 .values()
3571 .flat_map(|rels| rels.iter().map(|r| r.weight))
3572 .collect();
3573 if all.is_empty() {
3574 return Ok(0.0);
3575 }
3576 let above = all.iter().filter(|&&w| w > threshold).count();
3577 Ok(above as f64 / all.len() as f64)
3578 }
3579
3580 pub fn entities_sorted_by_out_degree(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
3585 let inner =
3586 recover_lock(self.inner.lock(), "GraphStore::entities_sorted_by_out_degree");
3587 let mut pairs: Vec<(Entity, usize)> = inner
3588 .entities
3589 .values()
3590 .map(|e| {
3591 let degree = inner
3592 .adjacency
3593 .get(&e.id)
3594 .map_or(0, |rels| rels.len());
3595 (e.clone(), degree)
3596 })
3597 .collect();
3598 pairs.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.id.as_str().cmp(b.0.id.as_str())));
3599 Ok(pairs.into_iter().map(|(e, _)| e).collect())
3600 }
3601
3602 pub fn entity_by_label(&self, label: &str) -> Result<Option<Entity>, AgentRuntimeError> {
3607 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_by_label");
3608 Ok(inner.entities.values().find(|e| e.label == label).cloned())
3609 }
3610
3611 pub fn distinct_relationship_kind_count(&self) -> Result<usize, AgentRuntimeError> {
3615 let inner = recover_lock(
3616 self.inner.lock(),
3617 "GraphStore::distinct_relationship_kind_count",
3618 );
3619 let kinds: std::collections::HashSet<&str> = inner
3620 .adjacency
3621 .values()
3622 .flat_map(|rels| rels.iter())
3623 .map(|r| r.kind.as_str())
3624 .collect();
3625 Ok(kinds.len())
3626 }
3627
3628 pub fn has_entity_with_label(&self, label: &str) -> Result<bool, AgentRuntimeError> {
3633 let inner = recover_lock(self.inner.lock(), "GraphStore::has_entity_with_label");
3634 Ok(inner.entities.values().any(|e| e.label == label))
3635 }
3636
3637 pub fn min_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
3640 let inner = recover_lock(self.inner.lock(), "GraphStore::min_weight");
3641 let min = inner
3642 .adjacency
3643 .values()
3644 .flat_map(|rels| rels.iter())
3645 .map(|r| r.weight)
3646 .reduce(f32::min);
3647 Ok(min)
3648 }
3649
3650 pub fn relationships_of_kind_count(&self, kind: &str) -> Result<usize, AgentRuntimeError> {
3656 let inner = recover_lock(
3657 self.inner.lock(),
3658 "GraphStore::relationships_of_kind_count",
3659 );
3660 let count = inner
3661 .adjacency
3662 .values()
3663 .flat_map(|rels| rels.iter())
3664 .filter(|r| r.kind == kind)
3665 .count();
3666 Ok(count)
3667 }
3668
3669 pub fn relationship_count_for_entity(
3673 &self,
3674 entity_id: &EntityId,
3675 ) -> Result<usize, AgentRuntimeError> {
3676 let inner = recover_lock(
3677 self.inner.lock(),
3678 "GraphStore::relationship_count_for_entity",
3679 );
3680 Ok(inner
3681 .adjacency
3682 .get(entity_id)
3683 .map_or(0, |rels| rels.len()))
3684 }
3685
3686 pub fn entities_by_label(
3690 &self,
3691 label: &str,
3692 ) -> Result<Vec<EntityId>, AgentRuntimeError> {
3693 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_by_label");
3694 Ok(inner
3695 .entities
3696 .values()
3697 .filter(|e| e.label == label)
3698 .map(|e| e.id.clone())
3699 .collect())
3700 }
3701
3702 pub fn edge_count_between(
3706 &self,
3707 from_id: &EntityId,
3708 to_id: &EntityId,
3709 ) -> Result<usize, AgentRuntimeError> {
3710 let inner = recover_lock(self.inner.lock(), "GraphStore::edge_count_between");
3711 Ok(inner
3712 .adjacency
3713 .get(from_id)
3714 .map_or(0, |rels| rels.iter().filter(|r| &r.to == to_id).count()))
3715 }
3716
3717 pub fn is_connected(
3723 &self,
3724 from_id: &EntityId,
3725 to_id: &EntityId,
3726 ) -> Result<bool, AgentRuntimeError> {
3727 let inner = recover_lock(self.inner.lock(), "GraphStore::is_connected");
3728 Ok(inner
3729 .adjacency
3730 .get(from_id)
3731 .map_or(false, |rels| rels.iter().any(|r| &r.to == to_id)))
3732 }
3733
3734 pub fn graph_is_empty(&self) -> Result<bool, AgentRuntimeError> {
3736 let inner = recover_lock(self.inner.lock(), "GraphStore::graph_is_empty");
3737 Ok(inner.entities.is_empty() && inner.adjacency.is_empty())
3738 }
3739
3740 pub fn unique_relationship_kinds(&self) -> Result<Vec<String>, AgentRuntimeError> {
3745 let inner =
3746 recover_lock(self.inner.lock(), "GraphStore::unique_relationship_kinds");
3747 let mut kinds: Vec<String> = inner
3748 .adjacency
3749 .values()
3750 .flat_map(|rels| rels.iter())
3751 .map(|r| r.kind.clone())
3752 .collect::<std::collections::HashSet<_>>()
3753 .into_iter()
3754 .collect();
3755 kinds.sort_unstable();
3756 Ok(kinds)
3757 }
3758
3759 pub fn has_any_relationships(&self) -> Result<bool, AgentRuntimeError> {
3761 let inner = recover_lock(self.inner.lock(), "GraphStore::has_any_relationships");
3762 Ok(inner.adjacency.values().any(|rels| !rels.is_empty()))
3763 }
3764
3765 pub fn avg_edge_weight(&self) -> Result<f64, AgentRuntimeError> {
3769 let inner = recover_lock(self.inner.lock(), "GraphStore::avg_edge_weight");
3770 let rels: Vec<&crate::graph::Relationship> = inner
3771 .adjacency
3772 .values()
3773 .flat_map(|v| v.iter())
3774 .collect();
3775 if rels.is_empty() {
3776 return Ok(0.0);
3777 }
3778 let total: f64 = rels.iter().map(|r| r.weight as f64).sum();
3779 Ok(total / rels.len() as f64)
3780 }
3781
3782 pub fn entities_with_incoming(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
3790 let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_incoming");
3791 let mut has_in: std::collections::HashSet<&EntityId> =
3792 std::collections::HashSet::new();
3793 for rels in inner.adjacency.values() {
3794 for r in rels {
3795 has_in.insert(&r.to);
3796 }
3797 }
3798 Ok(inner
3799 .entities
3800 .values()
3801 .filter(|e| has_in.contains(&e.id))
3802 .cloned()
3803 .collect())
3804 }
3805
3806 pub fn entities_with_no_relationships(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
3811 let inner = recover_lock(
3812 self.inner.lock(),
3813 "GraphStore::entities_with_no_relationships",
3814 );
3815 Ok(inner
3816 .entities
3817 .values()
3818 .filter(|e| {
3819 inner
3820 .adjacency
3821 .get(&e.id)
3822 .map_or(true, |rels| rels.is_empty())
3823 })
3824 .cloned()
3825 .collect())
3826 }
3827
3828 pub fn total_edge_weight(&self) -> Result<f64, AgentRuntimeError> {
3832 let inner = recover_lock(self.inner.lock(), "GraphStore::total_edge_weight");
3833 Ok(inner
3834 .adjacency
3835 .values()
3836 .flat_map(|rels| rels.iter())
3837 .map(|r| r.weight as f64)
3838 .sum())
3839 }
3840
3841 pub fn entity_with_max_out_degree(&self) -> Result<Option<Entity>, AgentRuntimeError> {
3846 let inner = recover_lock(self.inner.lock(), "GraphStore::entity_with_max_out_degree");
3847 Ok(inner
3848 .entities
3849 .values()
3850 .max_by_key(|e| {
3851 inner
3852 .adjacency
3853 .get(&e.id)
3854 .map_or(0, |rels| rels.len())
3855 })
3856 .cloned())
3857 }
3858
3859 pub fn self_loops(&self) -> Result<Vec<Relationship>, AgentRuntimeError> {
3864 let inner = recover_lock(self.inner.lock(), "GraphStore::self_loops");
3865 let loops: Vec<Relationship> = inner
3866 .adjacency
3867 .iter()
3868 .flat_map(|(from, rels)| {
3869 let from = from.clone();
3870 rels.iter()
3871 .filter(move |r| r.to == from)
3872 .cloned()
3873 .collect::<Vec<_>>()
3874 })
3875 .collect();
3876 Ok(loops)
3877 }
3878
3879 pub fn entities_with_label_and_property(
3884 &self,
3885 label: &str,
3886 key: &str,
3887 ) -> Result<Vec<Entity>, AgentRuntimeError> {
3888 let inner = recover_lock(
3889 self.inner.lock(),
3890 "GraphStore::entities_with_label_and_property",
3891 );
3892 Ok(inner
3893 .entities
3894 .values()
3895 .filter(|e| e.label == label && e.properties.contains_key(key))
3896 .cloned()
3897 .collect())
3898 }
3899
3900 pub fn entity_has_outgoing_edge(
3906 &self,
3907 id: &EntityId,
3908 ) -> Result<bool, AgentRuntimeError> {
3909 let inner =
3910 recover_lock(self.inner.lock(), "GraphStore::entity_has_outgoing_edge");
3911 Ok(inner
3912 .adjacency
3913 .get(id)
3914 .map_or(false, |rels| !rels.is_empty()))
3915 }
3916
3917 pub fn count_nodes_with_self_loop(&self) -> Result<usize, AgentRuntimeError> {
3923 let inner =
3924 recover_lock(self.inner.lock(), "GraphStore::count_nodes_with_self_loop");
3925 let count = inner
3926 .adjacency
3927 .iter()
3928 .filter(|(from, rels)| rels.iter().any(|r| &r.to == *from))
3929 .count();
3930 Ok(count)
3931 }
3932
3933 pub fn cycle_count(&self) -> Result<usize, AgentRuntimeError> {
3936 let inner = recover_lock(self.inner.lock(), "GraphStore::cycle_count");
3937 let count = inner
3938 .adjacency
3939 .iter()
3940 .flat_map(|(from, rels)| rels.iter().map(move |r| (from, r)))
3941 .filter(|(from, r)| &r.to == *from)
3942 .count();
3943 Ok(count)
3944 }
3945
3946 pub fn out_degree_of(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
3951 let inner =
3952 recover_lock(self.inner.lock(), "GraphStore::out_degree_of");
3953 Ok(inner.adjacency.get(id).map_or(0, |rels| rels.len()))
3954 }
3955
3956 pub fn has_relationship_with_kind(
3962 &self,
3963 kind: &str,
3964 ) -> Result<bool, AgentRuntimeError> {
3965 let inner =
3966 recover_lock(self.inner.lock(), "GraphStore::has_relationship_with_kind");
3967 Ok(inner
3968 .adjacency
3969 .values()
3970 .flat_map(|rels| rels.iter())
3971 .any(|r| r.kind == kind))
3972 }
3973
3974 pub fn all_entities_have_properties(&self) -> Result<bool, AgentRuntimeError> {
3979 let inner =
3980 recover_lock(self.inner.lock(), "GraphStore::all_entities_have_properties");
3981 Ok(inner.entities.values().all(|e| !e.properties.is_empty()))
3982 }
3983
3984}
3985
3986impl Default for GraphStore {
3987 fn default() -> Self {
3988 Self::new()
3989 }
3990}
3991
3992#[cfg(test)]
3995mod tests {
3996 use super::*;
3997
3998 fn make_graph() -> GraphStore {
3999 GraphStore::new()
4000 }
4001
4002 fn add(g: &GraphStore, id: &str) {
4003 g.add_entity(Entity::new(id, "Node")).unwrap();
4004 }
4005
4006 fn link(g: &GraphStore, from: &str, to: &str) {
4007 g.add_relationship(Relationship::new(from, to, "CONNECTS", 1.0))
4008 .unwrap();
4009 }
4010
4011 fn link_w(g: &GraphStore, from: &str, to: &str, weight: f32) {
4012 g.add_relationship(Relationship::new(from, to, "CONNECTS", weight))
4013 .unwrap();
4014 }
4015
4016 #[test]
4019 fn test_entity_id_equality() {
4020 assert_eq!(EntityId::new("a"), EntityId::new("a"));
4021 assert_ne!(EntityId::new("a"), EntityId::new("b"));
4022 }
4023
4024 #[test]
4025 fn test_entity_id_display() {
4026 let id = EntityId::new("hello");
4027 assert_eq!(id.to_string(), "hello");
4028 }
4029
4030 #[test]
4033 fn test_entity_new_has_empty_properties() {
4034 let e = Entity::new("e1", "Person");
4035 assert!(e.properties.is_empty());
4036 }
4037
4038 #[test]
4039 fn test_entity_with_properties_stores_props() {
4040 let mut props = HashMap::new();
4041 props.insert("age".into(), Value::Number(42.into()));
4042 let e = Entity::with_properties("e1", "Person", props);
4043 assert!(e.properties.contains_key("age"));
4044 }
4045
4046 #[test]
4049 fn test_graph_add_entity_increments_count() {
4050 let g = make_graph();
4051 add(&g, "a");
4052 assert_eq!(g.entity_count().unwrap(), 1);
4053 }
4054
4055 #[test]
4056 fn test_graph_get_entity_returns_entity() {
4057 let g = make_graph();
4058 g.add_entity(Entity::new("e1", "Person")).unwrap();
4059 let e = g.get_entity(&EntityId::new("e1")).unwrap();
4060 assert_eq!(e.label, "Person");
4061 }
4062
4063 #[test]
4064 fn test_graph_get_entity_missing_returns_error() {
4065 let g = make_graph();
4066 assert!(g.get_entity(&EntityId::new("ghost")).is_err());
4067 }
4068
4069 #[test]
4070 fn test_graph_add_relationship_increments_count() {
4071 let g = make_graph();
4072 add(&g, "a");
4073 add(&g, "b");
4074 link(&g, "a", "b");
4075 assert_eq!(g.relationship_count().unwrap(), 1);
4076 }
4077
4078 #[test]
4079 fn test_graph_add_relationship_missing_source_fails() {
4080 let g = make_graph();
4081 add(&g, "b");
4082 let result = g.add_relationship(Relationship::new("ghost", "b", "X", 1.0));
4083 assert!(result.is_err());
4084 }
4085
4086 #[test]
4087 fn test_graph_add_relationship_missing_target_fails() {
4088 let g = make_graph();
4089 add(&g, "a");
4090 let result = g.add_relationship(Relationship::new("a", "ghost", "X", 1.0));
4091 assert!(result.is_err());
4092 }
4093
4094 #[test]
4095 fn test_graph_remove_entity_removes_relationships() {
4096 let g = make_graph();
4097 add(&g, "a");
4098 add(&g, "b");
4099 link(&g, "a", "b");
4100 g.remove_entity(&EntityId::new("a")).unwrap();
4101 assert_eq!(g.entity_count().unwrap(), 1);
4102 assert_eq!(g.relationship_count().unwrap(), 0);
4103 }
4104
4105 #[test]
4106 fn test_graph_remove_entity_missing_returns_error() {
4107 let g = make_graph();
4108 assert!(g.remove_entity(&EntityId::new("ghost")).is_err());
4109 }
4110
4111 #[test]
4114 fn test_bfs_finds_direct_neighbours() {
4115 let g = make_graph();
4116 add(&g, "a");
4117 add(&g, "b");
4118 add(&g, "c");
4119 link(&g, "a", "b");
4120 link(&g, "a", "c");
4121 let visited = g.bfs(&EntityId::new("a")).unwrap();
4122 assert_eq!(visited.len(), 2);
4123 }
4124
4125 #[test]
4126 fn test_bfs_traverses_chain() {
4127 let g = make_graph();
4128 add(&g, "a");
4129 add(&g, "b");
4130 add(&g, "c");
4131 add(&g, "d");
4132 link(&g, "a", "b");
4133 link(&g, "b", "c");
4134 link(&g, "c", "d");
4135 let visited = g.bfs(&EntityId::new("a")).unwrap();
4136 assert_eq!(visited.len(), 3);
4137 assert_eq!(visited[0], EntityId::new("b"));
4138 }
4139
4140 #[test]
4141 fn test_bfs_handles_isolated_node() {
4142 let g = make_graph();
4143 add(&g, "a");
4144 let visited = g.bfs(&EntityId::new("a")).unwrap();
4145 assert!(visited.is_empty());
4146 }
4147
4148 #[test]
4149 fn test_bfs_missing_start_returns_error() {
4150 let g = make_graph();
4151 assert!(g.bfs(&EntityId::new("ghost")).is_err());
4152 }
4153
4154 #[test]
4157 fn test_dfs_visits_all_reachable_nodes() {
4158 let g = make_graph();
4159 add(&g, "a");
4160 add(&g, "b");
4161 add(&g, "c");
4162 add(&g, "d");
4163 link(&g, "a", "b");
4164 link(&g, "a", "c");
4165 link(&g, "b", "d");
4166 let visited = g.dfs(&EntityId::new("a")).unwrap();
4167 assert_eq!(visited.len(), 3);
4168 }
4169
4170 #[test]
4171 fn test_dfs_handles_isolated_node() {
4172 let g = make_graph();
4173 add(&g, "a");
4174 let visited = g.dfs(&EntityId::new("a")).unwrap();
4175 assert!(visited.is_empty());
4176 }
4177
4178 #[test]
4179 fn test_dfs_missing_start_returns_error() {
4180 let g = make_graph();
4181 assert!(g.dfs(&EntityId::new("ghost")).is_err());
4182 }
4183
4184 #[test]
4187 fn test_shortest_path_direct_connection() {
4188 let g = make_graph();
4189 add(&g, "a");
4190 add(&g, "b");
4191 link(&g, "a", "b");
4192 let path = g
4193 .shortest_path(&EntityId::new("a"), &EntityId::new("b"))
4194 .unwrap();
4195 assert_eq!(path, Some(vec![EntityId::new("a"), EntityId::new("b")]));
4196 }
4197
4198 #[test]
4199 fn test_shortest_path_multi_hop() {
4200 let g = make_graph();
4201 add(&g, "a");
4202 add(&g, "b");
4203 add(&g, "c");
4204 link(&g, "a", "b");
4205 link(&g, "b", "c");
4206 let path = g
4207 .shortest_path(&EntityId::new("a"), &EntityId::new("c"))
4208 .unwrap();
4209 assert_eq!(path.as_ref().map(|p| p.len()), Some(3));
4210 }
4211
4212 #[test]
4213 fn test_shortest_path_returns_none_for_disconnected() {
4214 let g = make_graph();
4215 add(&g, "a");
4216 add(&g, "b");
4217 let path = g
4218 .shortest_path(&EntityId::new("a"), &EntityId::new("b"))
4219 .unwrap();
4220 assert_eq!(path, None);
4221 }
4222
4223 #[test]
4224 fn test_shortest_path_same_node_returns_single_element() {
4225 let g = make_graph();
4226 add(&g, "a");
4227 let path = g
4228 .shortest_path(&EntityId::new("a"), &EntityId::new("a"))
4229 .unwrap();
4230 assert_eq!(path, Some(vec![EntityId::new("a")]));
4231 }
4232
4233 #[test]
4234 fn test_shortest_path_missing_source_returns_error() {
4235 let g = make_graph();
4236 add(&g, "b");
4237 assert!(g
4238 .shortest_path(&EntityId::new("ghost"), &EntityId::new("b"))
4239 .is_err());
4240 }
4241
4242 #[test]
4243 fn test_shortest_path_missing_target_returns_error() {
4244 let g = make_graph();
4245 add(&g, "a");
4246 assert!(g
4247 .shortest_path(&EntityId::new("a"), &EntityId::new("ghost"))
4248 .is_err());
4249 }
4250
4251 #[test]
4254 fn test_transitive_closure_includes_start() {
4255 let g = make_graph();
4256 add(&g, "a");
4257 add(&g, "b");
4258 link(&g, "a", "b");
4259 let closure = g.transitive_closure(&EntityId::new("a")).unwrap();
4260 assert!(closure.contains(&EntityId::new("a")));
4261 assert!(closure.contains(&EntityId::new("b")));
4262 }
4263
4264 #[test]
4265 fn test_transitive_closure_isolated_node_contains_only_self() {
4266 let g = make_graph();
4267 add(&g, "a");
4268 let closure = g.transitive_closure(&EntityId::new("a")).unwrap();
4269 assert_eq!(closure.len(), 1);
4270 }
4271
4272 #[test]
4275 fn test_mem_graph_error_converts_to_runtime_error() {
4276 let e = MemGraphError::EntityNotFound("x".into());
4277 let re: AgentRuntimeError = e.into();
4278 assert!(matches!(re, AgentRuntimeError::Graph(_)));
4279 }
4280
4281 #[test]
4284 fn test_shortest_path_weighted_simple() {
4285 let g = make_graph();
4288 add(&g, "a");
4289 add(&g, "b");
4290 add(&g, "c");
4291 link_w(&g, "a", "b", 1.0);
4292 link_w(&g, "b", "c", 2.0);
4293 g.add_relationship(Relationship::new("a", "c", "DIRECT", 10.0))
4294 .unwrap();
4295
4296 let result = g
4297 .shortest_path_weighted(&EntityId::new("a"), &EntityId::new("c"))
4298 .unwrap();
4299 assert!(result.is_some());
4300 let (path, weight) = result.unwrap();
4301 assert_eq!(
4303 path,
4304 vec![EntityId::new("a"), EntityId::new("b"), EntityId::new("c")]
4305 );
4306 assert!((weight - 3.0).abs() < 1e-5);
4307 }
4308
4309 #[test]
4310 fn test_shortest_path_weighted_returns_none_for_disconnected() {
4311 let g = make_graph();
4312 add(&g, "a");
4313 add(&g, "b");
4314 let result = g
4315 .shortest_path_weighted(&EntityId::new("a"), &EntityId::new("b"))
4316 .unwrap();
4317 assert!(result.is_none());
4318 }
4319
4320 #[test]
4321 fn test_shortest_path_weighted_same_node() {
4322 let g = make_graph();
4323 add(&g, "a");
4324 let result = g
4325 .shortest_path_weighted(&EntityId::new("a"), &EntityId::new("a"))
4326 .unwrap();
4327 assert_eq!(result, Some((vec![EntityId::new("a")], 0.0)));
4328 }
4329
4330 #[test]
4331 fn test_shortest_path_weighted_negative_weight_errors() {
4332 let g = make_graph();
4333 add(&g, "a");
4334 add(&g, "b");
4335 g.add_relationship(Relationship::new("a", "b", "NEG", -1.0))
4336 .unwrap();
4337 let result = g.shortest_path_weighted(&EntityId::new("a"), &EntityId::new("b"));
4338 assert!(result.is_err());
4339 }
4340
4341 #[test]
4344 fn test_degree_centrality_basic() {
4345 let g = make_graph();
4349 add(&g, "a");
4350 add(&g, "b");
4351 add(&g, "c");
4352 add(&g, "d");
4353 link(&g, "a", "b");
4354 link(&g, "a", "c");
4355 link(&g, "a", "d");
4356
4357 let centrality = g.degree_centrality().unwrap();
4358 let a_cent = *centrality.get(&EntityId::new("a")).unwrap();
4359 let b_cent = *centrality.get(&EntityId::new("b")).unwrap();
4360
4361 assert!((a_cent - 1.0).abs() < 1e-5, "a centrality was {a_cent}");
4362 assert!(
4363 (b_cent - 1.0 / 3.0).abs() < 1e-5,
4364 "b centrality was {b_cent}"
4365 );
4366 }
4367
4368 #[test]
4371 fn test_betweenness_centrality_chain() {
4372 let g = make_graph();
4376 add(&g, "a");
4377 add(&g, "b");
4378 add(&g, "c");
4379 add(&g, "d");
4380 link(&g, "a", "b");
4381 link(&g, "b", "c");
4382 link(&g, "c", "d");
4383
4384 let centrality = g.betweenness_centrality().unwrap();
4385 let a_cent = *centrality.get(&EntityId::new("a")).unwrap();
4386 let b_cent = *centrality.get(&EntityId::new("b")).unwrap();
4387 let c_cent = *centrality.get(&EntityId::new("c")).unwrap();
4388 let d_cent = *centrality.get(&EntityId::new("d")).unwrap();
4389
4390 assert!((a_cent).abs() < 1e-5, "expected a_cent ~ 0, got {a_cent}");
4392 assert!(b_cent > 0.0, "expected b_cent > 0, got {b_cent}");
4393 assert!(c_cent > 0.0, "expected c_cent > 0, got {c_cent}");
4394 assert!((d_cent).abs() < 1e-5, "expected d_cent ~ 0, got {d_cent}");
4395 }
4396
4397 #[test]
4400 fn test_label_propagation_communities_two_clusters() {
4401 let g = make_graph();
4405 for id in &["a", "b", "c", "x", "y", "z"] {
4406 add(&g, id);
4407 }
4408 link(&g, "a", "b");
4410 link(&g, "b", "a");
4411 link(&g, "b", "c");
4412 link(&g, "c", "b");
4413 link(&g, "a", "c");
4414 link(&g, "c", "a");
4415 link(&g, "x", "y");
4417 link(&g, "y", "x");
4418 link(&g, "y", "z");
4419 link(&g, "z", "y");
4420 link(&g, "x", "z");
4421 link(&g, "z", "x");
4422
4423 let communities = g.label_propagation_communities(100).unwrap();
4424
4425 let label_a = communities[&EntityId::new("a")];
4426 let label_b = communities[&EntityId::new("b")];
4427 let label_c = communities[&EntityId::new("c")];
4428 let label_x = communities[&EntityId::new("x")];
4429 let label_y = communities[&EntityId::new("y")];
4430 let label_z = communities[&EntityId::new("z")];
4431
4432 assert_eq!(label_a, label_b, "a and b should be in same community");
4435 assert_eq!(label_b, label_c, "b and c should be in same community");
4436 assert_eq!(label_x, label_y, "x and y should be in same community");
4437 assert_eq!(label_y, label_z, "y and z should be in same community");
4438 assert_ne!(
4439 label_a, label_x,
4440 "cluster 1 and cluster 2 should be different communities"
4441 );
4442 }
4443
4444 #[test]
4447 fn test_subgraph_extracts_correct_nodes_and_edges() {
4448 let g = make_graph();
4451 add(&g, "a");
4452 add(&g, "b");
4453 add(&g, "c");
4454 add(&g, "d");
4455 link(&g, "a", "b");
4456 link(&g, "b", "c");
4457 link(&g, "c", "d");
4458
4459 let sub = g
4460 .subgraph(&[EntityId::new("a"), EntityId::new("b"), EntityId::new("c")])
4461 .unwrap();
4462
4463 assert_eq!(sub.entity_count().unwrap(), 3);
4464 assert_eq!(sub.relationship_count().unwrap(), 2);
4465
4466 assert!(sub.get_entity(&EntityId::new("d")).is_err());
4468
4469 let path = sub
4471 .shortest_path(&EntityId::new("a"), &EntityId::new("c"))
4472 .unwrap();
4473 assert!(path.is_some());
4474 assert_eq!(path.unwrap().len(), 3);
4475 }
4476
4477 #[test]
4480 fn test_detect_cycles_dag_returns_false() {
4481 let g = make_graph();
4482 add(&g, "a");
4483 add(&g, "b");
4484 add(&g, "c");
4485 link(&g, "a", "b");
4486 link(&g, "b", "c");
4487 assert_eq!(g.detect_cycles().unwrap(), false);
4488 }
4489
4490 #[test]
4491 fn test_detect_cycles_self_loop_returns_true() {
4492 let g = make_graph();
4493 add(&g, "a");
4494 g.add_relationship(Relationship::new("a", "a", "SELF", 1.0))
4496 .unwrap();
4497 assert_eq!(g.detect_cycles().unwrap(), true);
4498 }
4499
4500 #[test]
4501 fn test_detect_cycles_simple_cycle_returns_true() {
4502 let g = make_graph();
4503 add(&g, "a");
4504 add(&g, "b");
4505 link(&g, "a", "b");
4506 g.add_relationship(Relationship::new("b", "a", "BACK", 1.0))
4507 .unwrap();
4508 assert_eq!(g.detect_cycles().unwrap(), true);
4509 }
4510
4511 #[test]
4512 fn test_detect_cycles_empty_graph_returns_false() {
4513 let g = make_graph();
4514 assert_eq!(g.detect_cycles().unwrap(), false);
4515 }
4516
4517 #[test]
4518 fn test_detect_cycles_result_is_cached() {
4519 let g = make_graph();
4520 add(&g, "x");
4521 add(&g, "y");
4522 link(&g, "x", "y");
4523 let r1 = g.detect_cycles().unwrap();
4525 let r2 = g.detect_cycles().unwrap();
4527 assert_eq!(r1, r2);
4528 }
4529
4530 #[test]
4531 fn test_detect_cycles_cache_invalidated_on_mutation() {
4532 let g = make_graph();
4533 add(&g, "a");
4534 add(&g, "b");
4535 link(&g, "a", "b");
4536 assert_eq!(g.detect_cycles().unwrap(), false);
4537
4538 g.add_relationship(Relationship::new("b", "a", "BACK", 1.0))
4540 .unwrap();
4541 assert_eq!(
4542 g.detect_cycles().unwrap(),
4543 true,
4544 "cache should be invalidated after adding a back edge"
4545 );
4546 }
4547
4548 #[test]
4551 fn test_bfs_bounded_respects_max_depth() {
4552 let g = make_graph();
4554 add(&g, "a");
4555 add(&g, "b");
4556 add(&g, "c");
4557 add(&g, "d");
4558 link(&g, "a", "b");
4559 link(&g, "b", "c");
4560 link(&g, "c", "d");
4561
4562 let visited = g.bfs_bounded("a", 1, 100).unwrap();
4564 assert!(visited.contains(&EntityId::new("a")));
4565 assert!(visited.contains(&EntityId::new("b")));
4566 assert!(!visited.contains(&EntityId::new("c")), "c is at depth 2, should not be visited");
4567 }
4568
4569 #[test]
4572 fn test_path_exists_returns_true() {
4573 let g = make_graph();
4574 add(&g, "a");
4575 add(&g, "b");
4576 add(&g, "c");
4577 link(&g, "a", "b");
4578 link(&g, "b", "c");
4579 assert_eq!(g.path_exists("a", "c").unwrap(), true);
4580 }
4581
4582 #[test]
4583 fn test_path_exists_returns_false() {
4584 let g = make_graph();
4585 add(&g, "a");
4586 add(&g, "b");
4587 assert_eq!(g.path_exists("a", "b").unwrap(), false);
4588 }
4589
4590 #[test]
4593 fn test_entity_id_as_str() {
4594 let id = EntityId::new("my-entity");
4595 assert_eq!(id.as_str(), "my-entity");
4596 }
4597
4598 #[test]
4601 fn test_entity_id_as_ref_str() {
4602 let id = EntityId::new("asref-test");
4603 let s: &str = id.as_ref();
4604 assert_eq!(s, "asref-test");
4605 }
4606
4607 #[test]
4608 fn test_dfs_bounded_respects_max_nodes() {
4609 let g = make_graph();
4611 add(&g, "a");
4612 add(&g, "b");
4613 add(&g, "c");
4614 add(&g, "d");
4615 link(&g, "a", "b");
4616 link(&g, "b", "c");
4617 link(&g, "c", "d");
4618
4619 let visited = g.dfs_bounded("a", 100, 2).unwrap();
4621 assert_eq!(visited.len(), 2, "should stop at 2 nodes");
4622 }
4623
4624 #[test]
4627 fn test_entity_exists_and_relationship_exists() {
4628 let g = GraphStore::new();
4629 let a = EntityId::new("a");
4630 let b = EntityId::new("b");
4631 assert!(!g.entity_exists(&a).unwrap());
4632 g.add_entity(Entity::new("a", "Node")).unwrap();
4633 assert!(g.entity_exists(&a).unwrap());
4634 assert!(!g.relationship_exists(&a, &b, "knows").unwrap());
4635 g.add_entity(Entity::new("b", "Node")).unwrap();
4636 g.add_relationship(Relationship::new("a", "b", "knows", 1.0)).unwrap();
4637 assert!(g.relationship_exists(&a, &b, "knows").unwrap());
4638 assert!(!g.relationship_exists(&a, &b, "likes").unwrap());
4639 }
4640
4641 #[test]
4642 fn test_get_relationships_for_returns_outgoing() {
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 let a = EntityId::new("a");
4648 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4649 g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
4650 let rels = g.get_relationships_for(&a).unwrap();
4651 assert_eq!(rels.len(), 2);
4652 }
4653
4654 #[test]
4655 fn test_relationships_between_finds_both_directions() {
4656 let g = GraphStore::new();
4657 g.add_entity(Entity::new("x", "N")).unwrap();
4658 g.add_entity(Entity::new("y", "N")).unwrap();
4659 let x = EntityId::new("x");
4660 let y = EntityId::new("y");
4661 g.add_relationship(Relationship::new("x", "y", "follows", 1.0)).unwrap();
4662 g.add_relationship(Relationship::new("y", "x", "blocks", 1.0)).unwrap();
4663 let rels = g.relationships_between(&x, &y).unwrap();
4664 assert_eq!(rels.len(), 2);
4665 }
4666
4667 #[test]
4668 fn test_find_entities_by_property() {
4669 let g = GraphStore::new();
4670 g.add_entity(Entity::new("a", "Person").with_property("age", serde_json::json!(30))).unwrap();
4671 g.add_entity(Entity::new("b", "Person").with_property("age", serde_json::json!(25))).unwrap();
4672 g.add_entity(Entity::new("c", "Person").with_property("age", serde_json::json!(30))).unwrap();
4673 let found = g.find_entities_by_property("age", &serde_json::json!(30)).unwrap();
4674 assert_eq!(found.len(), 2);
4675 let ids: Vec<_> = found.iter().map(|e| e.id.as_str()).collect();
4676 assert!(ids.contains(&"a") && ids.contains(&"c"));
4677 }
4678
4679 #[test]
4680 fn test_neighbor_entities_returns_entity_objects() {
4681 let g = GraphStore::new();
4682 g.add_entity(Entity::new("root", "R")).unwrap();
4683 g.add_entity(Entity::new("child1", "C")).unwrap();
4684 g.add_entity(Entity::new("child2", "C")).unwrap();
4685 let root = EntityId::new("root");
4686 g.add_relationship(Relationship::new("root", "child1", "has", 1.0)).unwrap();
4687 g.add_relationship(Relationship::new("root", "child2", "has", 1.0)).unwrap();
4688 let neighbors = g.neighbor_entities(&root).unwrap();
4689 assert_eq!(neighbors.len(), 2);
4690 let labels: Vec<_> = neighbors.iter().map(|e| e.label.as_str()).collect();
4691 assert!(labels.iter().all(|l| *l == "C"));
4692 }
4693
4694 #[test]
4695 fn test_remove_all_relationships_for() {
4696 let g = GraphStore::new();
4697 g.add_entity(Entity::new("a", "N")).unwrap();
4698 g.add_entity(Entity::new("b", "N")).unwrap();
4699 let a = EntityId::new("a");
4700 g.add_relationship(Relationship::new("a", "b", "r1", 1.0)).unwrap();
4701 g.add_relationship(Relationship::new("a", "b", "r2", 1.0)).unwrap();
4702 let removed = g.remove_all_relationships_for(&a).unwrap();
4703 assert_eq!(removed, 2);
4704 assert_eq!(g.relationship_count().unwrap(), 0);
4705 }
4706
4707 #[test]
4708 fn test_topological_sort_returns_valid_order() {
4709 let g = GraphStore::new();
4710 for id in &["a", "b", "c", "d"] {
4711 g.add_entity(Entity::new(*id, "N")).unwrap();
4712 }
4713 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
4714 g.add_relationship(Relationship::new("b", "c", "r", 1.0)).unwrap();
4715 g.add_relationship(Relationship::new("c", "d", "r", 1.0)).unwrap();
4716 let order = g.topological_sort().unwrap();
4717 assert_eq!(order.len(), 4);
4718 let pos: std::collections::HashMap<_, _> = order.iter().enumerate().map(|(i, id)| (id.as_str().to_owned(), i)).collect();
4719 assert!(pos["a"] < pos["b"]);
4720 assert!(pos["b"] < pos["c"]);
4721 assert!(pos["c"] < pos["d"]);
4722 }
4723
4724 #[test]
4725 fn test_topological_sort_rejects_cycle() {
4726 let g = GraphStore::new();
4727 g.add_entity(Entity::new("x", "N")).unwrap();
4728 g.add_entity(Entity::new("y", "N")).unwrap();
4729 g.add_relationship(Relationship::new("x", "y", "r", 1.0)).unwrap();
4730 g.add_relationship(Relationship::new("y", "x", "r", 1.0)).unwrap();
4731 assert!(g.topological_sort().is_err());
4732 }
4733
4734 #[test]
4735 fn test_entity_count_by_label() {
4736 let g = GraphStore::new();
4737 g.add_entity(Entity::new("a", "Person")).unwrap();
4738 g.add_entity(Entity::new("b", "Person")).unwrap();
4739 g.add_entity(Entity::new("c", "Organization")).unwrap();
4740 assert_eq!(g.entity_count_by_label("Person").unwrap(), 2);
4741 assert_eq!(g.entity_count_by_label("Organization").unwrap(), 1);
4742 assert_eq!(g.entity_count_by_label("Unknown").unwrap(), 0);
4743 }
4744
4745 #[test]
4746 fn test_update_entity_label_and_property() {
4747 let g = GraphStore::new();
4748 let id = EntityId::new("e1");
4749 g.add_entity(Entity::new("e1", "Old")).unwrap();
4750 assert!(g.update_entity_label(&id, "New").unwrap());
4751 assert_eq!(g.get_entity(&id).unwrap().label, "New");
4752 assert!(g.update_entity_property(&id, "key", serde_json::json!("val")).unwrap());
4753 assert_eq!(g.get_entity(&id).unwrap().properties["key"], serde_json::json!("val"));
4754 }
4755
4756 #[test]
4757 fn test_connected_components_single_node() {
4758 let g = GraphStore::new();
4759 g.add_entity(Entity::new("a", "A")).unwrap();
4760 assert_eq!(g.connected_components().unwrap(), 1);
4761 }
4762
4763 #[test]
4764 fn test_connected_components_two_isolated_nodes() {
4765 let g = GraphStore::new();
4766 g.add_entity(Entity::new("a", "A")).unwrap();
4767 g.add_entity(Entity::new("b", "B")).unwrap();
4768 assert_eq!(g.connected_components().unwrap(), 2);
4769 }
4770
4771 #[test]
4772 fn test_connected_components_connected_pair() {
4773 let g = GraphStore::new();
4774 g.add_entity(Entity::new("a", "A")).unwrap();
4775 g.add_entity(Entity::new("b", "B")).unwrap();
4776 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4777 assert_eq!(g.connected_components().unwrap(), 1);
4778 }
4779
4780 #[test]
4781 fn test_connected_components_two_separate_pairs() {
4782 let g = GraphStore::new();
4783 g.add_entity(Entity::new("a", "A")).unwrap();
4784 g.add_entity(Entity::new("b", "B")).unwrap();
4785 g.add_entity(Entity::new("c", "C")).unwrap();
4786 g.add_entity(Entity::new("d", "D")).unwrap();
4787 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4788 g.add_relationship(Relationship::new("c", "d", "link", 1.0)).unwrap();
4789 assert_eq!(g.connected_components().unwrap(), 2);
4790 }
4791
4792 #[test]
4793 fn test_connected_components_empty_graph() {
4794 let g = GraphStore::new();
4795 assert_eq!(g.connected_components().unwrap(), 0);
4796 }
4797
4798 #[test]
4801 fn test_weakly_connected_true_for_empty_graph() {
4802 let g = GraphStore::new();
4803 assert!(g.weakly_connected().unwrap());
4804 }
4805
4806 #[test]
4807 fn test_weakly_connected_true_for_single_node() {
4808 let g = GraphStore::new();
4809 g.add_entity(Entity::new("a", "A")).unwrap();
4810 assert!(g.weakly_connected().unwrap());
4811 }
4812
4813 #[test]
4814 fn test_weakly_connected_true_when_all_nodes_connected() {
4815 let g = GraphStore::new();
4816 g.add_entity(Entity::new("a", "A")).unwrap();
4817 g.add_entity(Entity::new("b", "B")).unwrap();
4818 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4819 assert!(g.weakly_connected().unwrap());
4820 }
4821
4822 #[test]
4823 fn test_weakly_connected_false_when_nodes_isolated() {
4824 let g = GraphStore::new();
4825 g.add_entity(Entity::new("a", "A")).unwrap();
4826 g.add_entity(Entity::new("b", "B")).unwrap();
4827 assert!(!g.weakly_connected().unwrap());
4828 }
4829
4830 #[test]
4831 fn test_isolates_returns_nodes_with_no_edges() {
4832 let g = GraphStore::new();
4833 g.add_entity(Entity::new("a", "A")).unwrap();
4834 g.add_entity(Entity::new("b", "B")).unwrap();
4835 g.add_entity(Entity::new("c", "C")).unwrap();
4836 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4837 let iso = g.isolates().unwrap();
4838 let mut ids: Vec<String> = iso.iter().map(|e| e.id.as_str().to_string()).collect();
4839 ids.sort();
4840 assert_eq!(ids, vec!["c".to_string()]);
4841 }
4842
4843 #[test]
4844 fn test_isolates_returns_empty_when_all_connected() {
4845 let g = GraphStore::new();
4846 g.add_entity(Entity::new("a", "A")).unwrap();
4847 g.add_entity(Entity::new("b", "B")).unwrap();
4848 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4849 assert!(g.isolates().unwrap().is_empty());
4850 }
4851
4852 #[test]
4853 fn test_isolates_all_isolated() {
4854 let g = GraphStore::new();
4855 g.add_entity(Entity::new("x", "X")).unwrap();
4856 g.add_entity(Entity::new("y", "Y")).unwrap();
4857 let iso = g.isolates().unwrap();
4858 let mut ids: Vec<String> = iso.iter().map(|e| e.id.as_str().to_string()).collect();
4859 ids.sort();
4860 assert_eq!(ids, vec!["x".to_string(), "y".to_string()]);
4861 }
4862
4863 #[test]
4864 fn test_is_dag_on_dag() {
4865 let g = GraphStore::new();
4866 g.add_entity(Entity::new("a", "A")).unwrap();
4867 g.add_entity(Entity::new("b", "B")).unwrap();
4868 g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
4869 assert!(g.is_dag().unwrap());
4870 }
4871
4872 #[test]
4873 fn test_is_dag_on_cyclic_graph() {
4874 let g = GraphStore::new();
4875 g.add_entity(Entity::new("a", "A")).unwrap();
4876 g.add_entity(Entity::new("b", "B")).unwrap();
4877 g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
4878 g.add_relationship(Relationship::new("b", "a", "back", 1.0)).unwrap();
4879 assert!(!g.is_dag().unwrap());
4880 }
4881
4882 #[test]
4883 fn test_in_degree_and_out_degree() {
4884 let g = GraphStore::new();
4885 g.add_entity(Entity::new("a", "A")).unwrap();
4886 g.add_entity(Entity::new("b", "B")).unwrap();
4887 g.add_entity(Entity::new("c", "C")).unwrap();
4888 g.add_relationship(Relationship::new("a", "b", "e1", 1.0)).unwrap();
4889 g.add_relationship(Relationship::new("c", "b", "e2", 1.0)).unwrap();
4890 let a = EntityId::new("a");
4891 let b = EntityId::new("b");
4892 let c = EntityId::new("c");
4893 assert_eq!(g.out_degree(&a).unwrap(), 1);
4894 assert_eq!(g.in_degree(&a).unwrap(), 0);
4895 assert_eq!(g.in_degree(&b).unwrap(), 2);
4896 assert_eq!(g.out_degree(&b).unwrap(), 0);
4897 assert_eq!(g.out_degree(&c).unwrap(), 1);
4898 assert_eq!(g.in_degree(&c).unwrap(), 0);
4899 }
4900
4901 #[test]
4902 fn test_in_degree_missing_entity_returns_zero() {
4903 let g = GraphStore::new();
4904 let id = EntityId::new("ghost");
4905 assert_eq!(g.in_degree(&id).unwrap(), 0);
4906 }
4907
4908 #[test]
4909 fn test_out_degree_missing_entity_returns_zero() {
4910 let g = GraphStore::new();
4911 let id = EntityId::new("ghost");
4912 assert_eq!(g.out_degree(&id).unwrap(), 0);
4913 }
4914
4915 #[test]
4916 fn test_node_count_is_alias_for_entity_count() {
4917 let g = GraphStore::new();
4918 g.add_entity(Entity::new("a", "A")).unwrap();
4919 g.add_entity(Entity::new("b", "B")).unwrap();
4920 assert_eq!(g.node_count().unwrap(), g.entity_count().unwrap());
4921 assert_eq!(g.node_count().unwrap(), 2);
4922 }
4923
4924 #[test]
4925 fn test_edge_count_is_alias_for_relationship_count() {
4926 let g = GraphStore::new();
4927 g.add_entity(Entity::new("a", "A")).unwrap();
4928 g.add_entity(Entity::new("b", "B")).unwrap();
4929 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
4930 assert_eq!(g.edge_count().unwrap(), g.relationship_count().unwrap());
4931 assert_eq!(g.edge_count().unwrap(), 1);
4932 }
4933
4934 #[test]
4935 fn test_source_nodes_returns_nodes_with_no_incoming_edges() {
4936 let g = GraphStore::new();
4937 g.add_entity(Entity::new("root", "Root")).unwrap();
4938 g.add_entity(Entity::new("child", "Child")).unwrap();
4939 g.add_entity(Entity::new("leaf", "Leaf")).unwrap();
4940 g.add_relationship(Relationship::new("root", "child", "e", 1.0)).unwrap();
4941 g.add_relationship(Relationship::new("child", "leaf", "e", 1.0)).unwrap();
4942 let sources = g.source_nodes().unwrap();
4943 assert_eq!(sources.len(), 1);
4944 assert_eq!(sources[0].id.as_str(), "root");
4945 }
4946
4947 #[test]
4948 fn test_sink_nodes_returns_nodes_with_no_outgoing_edges() {
4949 let g = GraphStore::new();
4950 g.add_entity(Entity::new("root", "Root")).unwrap();
4951 g.add_entity(Entity::new("child", "Child")).unwrap();
4952 g.add_entity(Entity::new("leaf", "Leaf")).unwrap();
4953 g.add_relationship(Relationship::new("root", "child", "e", 1.0)).unwrap();
4954 g.add_relationship(Relationship::new("child", "leaf", "e", 1.0)).unwrap();
4955 let sinks = g.sink_nodes().unwrap();
4956 assert_eq!(sinks.len(), 1);
4957 assert_eq!(sinks[0].id.as_str(), "leaf");
4958 }
4959
4960 #[test]
4961 fn test_source_and_sink_empty_on_isolated_node() {
4962 let g = GraphStore::new();
4964 g.add_entity(Entity::new("solo", "Solo")).unwrap();
4965 assert_eq!(g.source_nodes().unwrap().len(), 1);
4966 assert_eq!(g.sink_nodes().unwrap().len(), 1);
4967 }
4968
4969 #[test]
4970 fn test_reverse_flips_all_edges() {
4971 let g = GraphStore::new();
4972 g.add_entity(Entity::new("a", "A")).unwrap();
4973 g.add_entity(Entity::new("b", "B")).unwrap();
4974 g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
4975 let rev = g.reverse().unwrap();
4976 assert_eq!(rev.entity_count().unwrap(), 2);
4978 assert_eq!(rev.relationship_count().unwrap(), 1);
4979 let b_id = EntityId::new("b");
4980 let a_id = EntityId::new("a");
4981 assert!(rev.relationship_exists(&b_id, &a_id, "edge").unwrap());
4982 }
4983
4984 #[test]
4985 fn test_reverse_empty_graph_stays_empty() {
4986 let g = GraphStore::new();
4987 let rev = g.reverse().unwrap();
4988 assert_eq!(rev.entity_count().unwrap(), 0);
4989 }
4990
4991 #[test]
4992 fn test_common_neighbors_finds_shared_targets() {
4993 let g = GraphStore::new();
4994 g.add_entity(Entity::new("a", "A")).unwrap();
4995 g.add_entity(Entity::new("b", "B")).unwrap();
4996 g.add_entity(Entity::new("shared", "S")).unwrap();
4997 g.add_entity(Entity::new("only_a", "OA")).unwrap();
4998 g.add_relationship(Relationship::new("a", "shared", "e", 1.0)).unwrap();
4999 g.add_relationship(Relationship::new("b", "shared", "e", 1.0)).unwrap();
5000 g.add_relationship(Relationship::new("a", "only_a", "e", 1.0)).unwrap();
5001 let a_id = EntityId::new("a");
5002 let b_id = EntityId::new("b");
5003 let common = g.common_neighbors(&a_id, &b_id).unwrap();
5004 assert_eq!(common.len(), 1);
5005 assert_eq!(common[0].id.as_str(), "shared");
5006 }
5007
5008 #[test]
5009 fn test_common_neighbors_empty_when_none_shared() {
5010 let g = GraphStore::new();
5011 g.add_entity(Entity::new("a", "A")).unwrap();
5012 g.add_entity(Entity::new("b", "B")).unwrap();
5013 g.add_entity(Entity::new("x", "X")).unwrap();
5014 g.add_entity(Entity::new("y", "Y")).unwrap();
5015 g.add_relationship(Relationship::new("a", "x", "e", 1.0)).unwrap();
5016 g.add_relationship(Relationship::new("b", "y", "e", 1.0)).unwrap();
5017 let a_id = EntityId::new("a");
5018 let b_id = EntityId::new("b");
5019 assert!(g.common_neighbors(&a_id, &b_id).unwrap().is_empty());
5020 }
5021
5022 #[test]
5025 fn test_graph_is_empty_initially() {
5026 let g = GraphStore::new();
5027 assert!(g.is_empty().unwrap());
5028 }
5029
5030 #[test]
5031 fn test_graph_is_empty_false_after_add() {
5032 let g = GraphStore::new();
5033 g.add_entity(Entity::new("a", "A")).unwrap();
5034 assert!(!g.is_empty().unwrap());
5035 }
5036
5037 #[test]
5038 fn test_graph_entity_ids_returns_all_ids() {
5039 let g = GraphStore::new();
5040 g.add_entity(Entity::new("x", "X")).unwrap();
5041 g.add_entity(Entity::new("y", "Y")).unwrap();
5042 let ids = g.entity_ids().unwrap();
5043 assert_eq!(ids.len(), 2);
5044 assert!(ids.iter().any(|id| id.0 == "x"));
5045 assert!(ids.iter().any(|id| id.0 == "y"));
5046 }
5047
5048 #[test]
5049 fn test_graph_clear_removes_entities_and_relationships() {
5050 let g = GraphStore::new();
5051 g.add_entity(Entity::new("a", "A")).unwrap();
5052 g.add_entity(Entity::new("b", "B")).unwrap();
5053 g.add_relationship(Relationship::new("a", "b", "links", 1.0))
5054 .unwrap();
5055 g.clear().unwrap();
5056 assert_eq!(g.entity_count().unwrap(), 0);
5057 assert_eq!(g.relationship_count().unwrap(), 0);
5058 assert!(g.is_empty().unwrap());
5059 }
5060
5061 #[test]
5064 fn test_weight_of_returns_edge_weight() {
5065 let g = make_graph();
5066 add(&g, "x"); add(&g, "y");
5067 link_w(&g, "x", "y", 3.5);
5068 let w = g.weight_of(&EntityId::new("x"), &EntityId::new("y")).unwrap();
5069 assert!(w.is_some());
5070 assert!((w.unwrap() - 3.5).abs() < 1e-6);
5071 }
5072
5073 #[test]
5074 fn test_weight_of_absent_edge_returns_none() {
5075 let g = make_graph();
5076 add(&g, "a"); add(&g, "b");
5077 let w = g.weight_of(&EntityId::new("a"), &EntityId::new("b")).unwrap();
5078 assert!(w.is_none());
5079 }
5080
5081 #[test]
5082 fn test_neighbors_in_returns_predecessors() {
5083 let g = make_graph();
5084 add(&g, "a"); add(&g, "b"); add(&g, "c");
5085 link(&g, "a", "c"); link(&g, "b", "c");
5086 let mut preds: Vec<String> = g
5087 .neighbors_in(&EntityId::new("c"))
5088 .unwrap()
5089 .into_iter()
5090 .map(|id| id.as_str().to_string())
5091 .collect();
5092 preds.sort();
5093 assert_eq!(preds, vec!["a", "b"]);
5094 }
5095
5096 #[test]
5097 fn test_neighbors_in_empty_for_node_with_no_incoming() {
5098 let g = make_graph();
5099 add(&g, "isolated");
5100 let preds = g.neighbors_in(&EntityId::new("isolated")).unwrap();
5101 assert!(preds.is_empty());
5102 }
5103
5104 #[test]
5105 fn test_path_exists_reachable() {
5106 let g = make_graph();
5107 add(&g, "s"); add(&g, "m"); add(&g, "t");
5108 link(&g, "s", "m"); link(&g, "m", "t");
5109 assert!(g.path_exists("s", "t").unwrap());
5110 }
5111
5112 #[test]
5113 fn test_path_exists_unreachable() {
5114 let g = make_graph();
5115 add(&g, "a"); add(&g, "b");
5116 assert!(!g.path_exists("a", "b").unwrap());
5117 }
5118
5119 #[test]
5122 fn test_neighbor_ids_returns_direct_successors() {
5123 let g = make_graph();
5124 add(&g, "src"); add(&g, "dst1"); add(&g, "dst2");
5125 link(&g, "src", "dst1"); link(&g, "src", "dst2");
5126 let mut ids = g.neighbor_ids(&EntityId::new("src")).unwrap();
5127 ids.sort_by_key(|id| id.0.clone());
5128 assert_eq!(ids, vec![EntityId::new("dst1"), EntityId::new("dst2")]);
5129 }
5130
5131 #[test]
5132 fn test_neighbor_ids_empty_for_isolated_node() {
5133 let g = make_graph();
5134 add(&g, "isolated");
5135 let ids = g.neighbor_ids(&EntityId::new("isolated")).unwrap();
5136 assert!(ids.is_empty());
5137 }
5138
5139 #[test]
5142 fn test_density_zero_for_empty_graph() {
5143 let g = make_graph();
5144 assert_eq!(g.density().unwrap(), 0.0);
5145 }
5146
5147 #[test]
5148 fn test_density_zero_for_single_node() {
5149 let g = make_graph();
5150 add(&g, "solo");
5151 assert_eq!(g.density().unwrap(), 0.0);
5152 }
5153
5154 #[test]
5155 fn test_density_one_for_complete_directed_graph() {
5156 let g = make_graph();
5158 add(&g, "a"); add(&g, "b");
5159 link(&g, "a", "b"); link(&g, "b", "a");
5160 assert!((g.density().unwrap() - 1.0).abs() < 1e-9);
5161 }
5162
5163 #[test]
5164 fn test_density_partial() {
5165 let g = make_graph();
5167 add(&g, "a"); add(&g, "b"); add(&g, "c");
5168 link(&g, "a", "b");
5169 let d = g.density().unwrap();
5170 assert!((d - 1.0/6.0).abs() < 1e-9);
5171 }
5172
5173 #[test]
5174 fn test_all_entities_returns_all_nodes() {
5175 let g = make_graph();
5176 add(&g, "x"); add(&g, "y"); add(&g, "z");
5177 assert_eq!(g.all_entities().unwrap().len(), 3);
5178 }
5179
5180 #[test]
5181 fn test_all_relationships_returns_all_edges() {
5182 let g = make_graph();
5183 add(&g, "a"); add(&g, "b"); add(&g, "c");
5184 link(&g, "a", "b"); link(&g, "b", "c");
5185 assert_eq!(g.all_relationships().unwrap().len(), 2);
5186 }
5187
5188 #[test]
5189 fn test_find_entities_by_label_returns_matches() {
5190 let g = make_graph();
5191 g.add_entity(Entity::new("n1", "Person")).unwrap();
5192 g.add_entity(Entity::new("n2", "Person")).unwrap();
5193 g.add_entity(Entity::new("n3", "Car")).unwrap();
5194 let people = g.find_entities_by_label("Person").unwrap();
5195 assert_eq!(people.len(), 2);
5196 }
5197
5198 #[test]
5199 fn test_bfs_bounded_limits_depth() {
5200 let g = make_graph();
5202 add(&g, "a"); add(&g, "b"); add(&g, "c"); add(&g, "d");
5203 link(&g, "a", "b"); link(&g, "b", "c"); link(&g, "c", "d");
5204 let visited = g.bfs_bounded("a", 2, 100).unwrap();
5205 assert!(visited.contains(&EntityId::new("a")));
5206 assert!(visited.contains(&EntityId::new("b")));
5207 assert!(visited.contains(&EntityId::new("c")));
5208 assert!(!visited.contains(&EntityId::new("d")));
5209 }
5210
5211 #[test]
5214 fn test_avg_degree_zero_for_empty_graph() {
5215 let g = make_graph();
5216 assert_eq!(g.avg_degree().unwrap(), 0.0);
5217 }
5218
5219 #[test]
5220 fn test_avg_degree_correct_value() {
5221 let g = make_graph();
5223 add(&g, "a"); add(&g, "b"); add(&g, "c");
5224 link(&g, "a", "b"); link(&g, "a", "c");
5225 let d = g.avg_degree().unwrap();
5226 assert!((d - 2.0/3.0).abs() < 1e-9);
5227 }
5228
5229 #[test]
5232 fn test_total_weight_zero_for_empty_graph() {
5233 let g = make_graph();
5234 assert_eq!(g.total_weight().unwrap(), 0.0);
5235 }
5236
5237 #[test]
5238 fn test_total_weight_sums_all_edges() {
5239 let g = make_graph();
5240 add(&g, "a"); add(&g, "b"); add(&g, "c");
5241 link_w(&g, "a", "b", 2.0); link_w(&g, "b", "c", 3.5);
5242 assert!((g.total_weight().unwrap() - 5.5).abs() < 1e-6);
5243 }
5244
5245 #[test]
5246 fn test_max_edge_weight_none_for_empty() {
5247 let g = make_graph();
5248 assert!(g.max_edge_weight().unwrap().is_none());
5249 }
5250
5251 #[test]
5252 fn test_max_edge_weight_returns_largest() {
5253 let g = make_graph();
5254 add(&g, "a"); add(&g, "b"); add(&g, "c");
5255 link_w(&g, "a", "b", 1.0); link_w(&g, "a", "c", 9.5);
5256 assert!((g.max_edge_weight().unwrap().unwrap() - 9.5).abs() < 1e-6);
5257 }
5258
5259 #[test]
5260 fn test_min_edge_weight_returns_smallest() {
5261 let g = make_graph();
5262 add(&g, "a"); add(&g, "b"); add(&g, "c");
5263 link_w(&g, "a", "b", 1.0); link_w(&g, "a", "c", 9.5);
5264 assert!((g.min_edge_weight().unwrap().unwrap() - 1.0).abs() < 1e-6);
5265 }
5266
5267 #[test]
5270 fn test_max_out_degree_entity_returns_node_with_most_edges() {
5271 let g = make_graph();
5272 add(&g, "hub"); add(&g, "a"); add(&g, "b"); add(&g, "leaf");
5273 link(&g, "hub", "a"); link(&g, "hub", "b"); link(&g, "a", "leaf");
5274 let best = g.max_out_degree_entity().unwrap().unwrap();
5275 assert_eq!(best.id, EntityId::new("hub"));
5276 }
5277
5278 #[test]
5279 fn test_max_out_degree_entity_none_for_empty_graph() {
5280 let g = make_graph();
5281 assert!(g.max_out_degree_entity().unwrap().is_none());
5282 }
5283
5284 #[test]
5285 fn test_leaf_nodes_returns_nodes_with_no_outgoing_edges() {
5286 let g = make_graph();
5287 add(&g, "root"); add(&g, "mid"); add(&g, "leaf1"); add(&g, "leaf2");
5288 link(&g, "root", "mid"); link(&g, "mid", "leaf1"); link(&g, "mid", "leaf2");
5289 let mut leaf_ids: Vec<String> = g
5290 .leaf_nodes()
5291 .unwrap()
5292 .into_iter()
5293 .map(|e| e.id.0.clone())
5294 .collect();
5295 leaf_ids.sort();
5296 assert_eq!(leaf_ids, vec!["leaf1", "leaf2"]);
5297 }
5298
5299 #[test]
5300 fn test_leaf_nodes_all_are_leaves_with_no_edges() {
5301 let g = make_graph();
5302 add(&g, "a"); add(&g, "b");
5303 assert_eq!(g.leaf_nodes().unwrap().len(), 2);
5304 }
5305
5306 #[test]
5309 fn test_top_n_by_out_degree_returns_descending() {
5310 let g = make_graph();
5311 add(&g, "hub"); add(&g, "mid"); add(&g, "tip"); add(&g, "leaf");
5312 link(&g, "hub", "mid"); link(&g, "hub", "tip"); link(&g, "hub", "leaf");
5313 link(&g, "mid", "leaf");
5314 let top2 = g.top_n_by_out_degree(2).unwrap();
5315 assert_eq!(top2.len(), 2);
5316 assert_eq!(top2[0].id, EntityId::new("hub"));
5317 }
5318
5319 #[test]
5320 fn test_top_n_by_out_degree_zero_returns_empty() {
5321 let g = make_graph();
5322 add(&g, "x");
5323 assert!(g.top_n_by_out_degree(0).unwrap().is_empty());
5324 }
5325
5326 #[test]
5327 fn test_remove_entity_and_edges_removes_node_and_incident_edges() {
5328 let g = make_graph();
5329 add(&g, "a"); add(&g, "b"); add(&g, "c");
5330 link(&g, "a", "b"); link(&g, "b", "c");
5331 g.remove_entity_and_edges(&EntityId::new("b")).unwrap();
5332 assert_eq!(g.entity_count().unwrap(), 2);
5333 assert_eq!(g.relationship_count().unwrap(), 0);
5334 }
5335
5336 #[test]
5337 fn test_remove_entity_and_edges_errors_for_unknown_id() {
5338 let g = make_graph();
5339 let result = g.remove_entity_and_edges(&EntityId::new("ghost"));
5340 assert!(result.is_err());
5341 }
5342
5343 #[test]
5346 fn test_relationship_kinds_returns_sorted_distinct_kinds() {
5347 let g = make_graph();
5348 add(&g, "a"); add(&g, "b"); add(&g, "c");
5349 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
5350 g.add_relationship(Relationship::new("b", "c", "LIKES", 1.0)).unwrap();
5351 g.add_relationship(Relationship::new("a", "c", "KNOWS", 1.0)).unwrap();
5352 let kinds = g.relationship_kinds().unwrap();
5353 assert_eq!(kinds, vec!["KNOWS", "LIKES"]);
5354 }
5355
5356 #[test]
5357 fn test_relationship_kinds_empty_graph_returns_empty() {
5358 let g = make_graph();
5359 assert!(g.relationship_kinds().unwrap().is_empty());
5360 }
5361
5362 #[test]
5363 fn test_graph_density_zero_for_empty() {
5364 let g = make_graph();
5365 assert_eq!(g.graph_density().unwrap(), 0.0);
5366 }
5367
5368 #[test]
5369 fn test_graph_density_correct_for_partial_graph() {
5370 let g = make_graph();
5371 add(&g, "a"); add(&g, "b"); add(&g, "c");
5372 link(&g, "a", "b");
5374 let d = g.graph_density().unwrap();
5375 assert!((d - 1.0 / 6.0).abs() < 1e-9);
5376 }
5377
5378 #[test]
5379 fn test_entity_id_try_new_rejects_empty() {
5380 let result = EntityId::try_new("");
5381 assert!(result.is_err());
5382 }
5383
5384 #[test]
5385 fn test_entity_id_try_new_accepts_nonempty() {
5386 let id = EntityId::try_new("valid").unwrap();
5387 assert_eq!(id.as_str(), "valid");
5388 }
5389
5390 #[test]
5393 fn test_hub_nodes_returns_nodes_meeting_threshold() {
5394 let g = make_graph();
5395 add(&g, "hub"); add(&g, "mid"); add(&g, "leaf");
5396 link(&g, "hub", "mid"); link(&g, "hub", "leaf"); link(&g, "mid", "leaf");
5397 let hubs = g.hub_nodes(2).unwrap();
5398 assert_eq!(hubs.len(), 1);
5399 assert_eq!(hubs[0].id, EntityId::new("hub"));
5400 }
5401
5402 #[test]
5403 fn test_hub_nodes_threshold_zero_returns_all() {
5404 let g = make_graph();
5405 add(&g, "a"); add(&g, "b");
5406 assert_eq!(g.hub_nodes(0).unwrap().len(), 2);
5407 }
5408
5409 #[test]
5410 fn test_incident_relationships_includes_outgoing_and_incoming() {
5411 let g = make_graph();
5412 add(&g, "a"); add(&g, "b"); add(&g, "c");
5413 link(&g, "a", "b"); link(&g, "c", "b");
5414 let rels = g.incident_relationships(&EntityId::new("b")).unwrap();
5415 assert_eq!(rels.len(), 2);
5416 }
5417
5418 #[test]
5419 fn test_incident_relationships_empty_for_isolated_node() {
5420 let g = make_graph();
5421 add(&g, "iso");
5422 assert!(g.incident_relationships(&EntityId::new("iso")).unwrap().is_empty());
5423 }
5424
5425 #[test]
5428 fn test_average_out_degree_empty_graph_is_zero() {
5429 let g = GraphStore::new();
5430 assert!((g.average_out_degree().unwrap() - 0.0).abs() < 1e-9);
5431 }
5432
5433 #[test]
5434 fn test_average_out_degree_two_nodes_one_edge() {
5435 let g = GraphStore::new();
5436 g.add_entity(Entity::new("a", "A")).unwrap();
5437 g.add_entity(Entity::new("b", "B")).unwrap();
5438 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
5439 assert!((g.average_out_degree().unwrap() - 0.5).abs() < 1e-9);
5441 }
5442
5443 #[test]
5444 fn test_in_degree_for_counts_incoming_edges() {
5445 let g = GraphStore::new();
5446 g.add_entity(Entity::new("a", "A")).unwrap();
5447 g.add_entity(Entity::new("b", "B")).unwrap();
5448 g.add_entity(Entity::new("c", "C")).unwrap();
5449 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
5450 g.add_relationship(Relationship::new("c", "b", "r", 1.0)).unwrap();
5451 assert_eq!(g.in_degree_for(&EntityId::new("b")).unwrap(), 2);
5452 assert_eq!(g.in_degree_for(&EntityId::new("a")).unwrap(), 0);
5453 }
5454
5455 #[test]
5456 fn test_in_degree_for_returns_zero_for_unknown_entity() {
5457 let g = GraphStore::new();
5458 assert_eq!(g.in_degree_for(&EntityId::new("ghost")).unwrap(), 0);
5459 }
5460
5461 #[test]
5464 fn test_entity_id_is_empty_false_for_nonempty() {
5465 let id = EntityId::new("node-1");
5466 assert!(!id.is_empty());
5467 }
5468
5469 #[test]
5470 fn test_entity_id_starts_with_matches_prefix() {
5471 let id = EntityId::new("concept-42");
5472 assert!(id.starts_with("concept-"));
5473 assert!(!id.starts_with("entity-"));
5474 }
5475
5476 #[test]
5477 fn test_entity_id_starts_with_empty_always_true() {
5478 let id = EntityId::new("anything");
5479 assert!(id.starts_with(""));
5480 }
5481
5482 #[test]
5483 fn test_entity_has_property_returns_true_when_present() {
5484 let e = Entity::new("e", "Node")
5485 .with_property("color", serde_json::json!("blue"));
5486 assert!(e.has_property("color"));
5487 assert!(!e.has_property("size"));
5488 }
5489
5490 #[test]
5491 fn test_entity_has_property_false_when_no_properties() {
5492 let e = Entity::new("e", "Node");
5493 assert!(!e.has_property("any"));
5494 }
5495
5496 #[test]
5497 fn test_entity_labels_returns_distinct_sorted_labels() {
5498 let g = make_graph();
5499 g.add_entity(Entity::new("a", "Person")).unwrap();
5500 g.add_entity(Entity::new("b", "Concept")).unwrap();
5501 g.add_entity(Entity::new("c", "Person")).unwrap();
5502 let labels = g.entity_labels().unwrap();
5503 assert_eq!(labels, vec!["Concept", "Person"]);
5504 }
5505
5506 #[test]
5507 fn test_entity_labels_empty_for_empty_graph() {
5508 let g = make_graph();
5509 assert!(g.entity_labels().unwrap().is_empty());
5510 }
5511
5512 #[test]
5515 fn test_out_degree_for_returns_outgoing_edge_count() {
5516 let g = GraphStore::new();
5517 g.add_entity(Entity::new("a", "A")).unwrap();
5518 g.add_entity(Entity::new("b", "B")).unwrap();
5519 g.add_entity(Entity::new("c", "C")).unwrap();
5520 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
5521 g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
5522 assert_eq!(g.out_degree_for(&EntityId::new("a")).unwrap(), 2);
5523 assert_eq!(g.out_degree_for(&EntityId::new("b")).unwrap(), 0);
5524 }
5525
5526 #[test]
5527 fn test_out_degree_for_returns_zero_for_unknown_entity() {
5528 let g = GraphStore::new();
5529 assert_eq!(g.out_degree_for(&EntityId::new("ghost")).unwrap(), 0);
5530 }
5531
5532 #[test]
5533 fn test_predecessors_returns_nodes_with_incoming_edges() {
5534 let g = GraphStore::new();
5535 g.add_entity(Entity::new("a", "A")).unwrap();
5536 g.add_entity(Entity::new("b", "B")).unwrap();
5537 g.add_entity(Entity::new("c", "C")).unwrap();
5538 g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
5539 g.add_relationship(Relationship::new("b", "c", "r", 1.0)).unwrap();
5540 let mut preds: Vec<String> = g
5541 .predecessors(&EntityId::new("c"))
5542 .unwrap()
5543 .iter()
5544 .map(|e| e.id.as_str().to_string())
5545 .collect();
5546 preds.sort();
5547 assert_eq!(preds, vec!["a", "b"]);
5548 }
5549
5550 #[test]
5551 fn test_predecessors_empty_for_source_node() {
5552 let g = GraphStore::new();
5553 g.add_entity(Entity::new("root", "Root")).unwrap();
5554 g.add_entity(Entity::new("child", "Child")).unwrap();
5555 g.add_relationship(Relationship::new("root", "child", "r", 1.0)).unwrap();
5556 assert!(g.predecessors(&EntityId::new("root")).unwrap().is_empty());
5557 }
5558
5559 #[test]
5560 fn test_is_source_true_for_node_with_no_incoming_edges() {
5561 let g = GraphStore::new();
5562 g.add_entity(Entity::new("src", "Src")).unwrap();
5563 g.add_entity(Entity::new("dst", "Dst")).unwrap();
5564 g.add_relationship(Relationship::new("src", "dst", "r", 1.0)).unwrap();
5565 assert!(g.is_source(&EntityId::new("src")).unwrap());
5566 assert!(!g.is_source(&EntityId::new("dst")).unwrap());
5567 }
5568
5569 #[test]
5572 fn test_relationship_is_self_loop_true_when_from_equals_to() {
5573 let r = Relationship::new("a", "a", "self", 1.0);
5574 assert!(r.is_self_loop());
5575 }
5576
5577 #[test]
5578 fn test_relationship_is_self_loop_false_for_normal_edge() {
5579 let r = Relationship::new("a", "b", "edge", 1.0);
5580 assert!(!r.is_self_loop());
5581 }
5582
5583 #[test]
5584 fn test_relationship_reversed_swaps_endpoints() {
5585 let r = Relationship::new("from", "to", "knows", 0.5);
5586 let rev = r.reversed();
5587 assert_eq!(rev.from.as_str(), "to");
5588 assert_eq!(rev.to.as_str(), "from");
5589 assert_eq!(rev.kind, "knows");
5590 assert!((rev.weight - 0.5).abs() < 1e-6);
5591 }
5592
5593 #[test]
5594 fn test_find_entities_by_labels_returns_matching() {
5595 let g = make_graph();
5596 g.add_entity(Entity::new("p1", "Person")).unwrap();
5597 g.add_entity(Entity::new("p2", "Person")).unwrap();
5598 g.add_entity(Entity::new("c1", "Concept")).unwrap();
5599 let results = g.find_entities_by_labels(&["Person"]).unwrap();
5600 assert_eq!(results.len(), 2);
5601 assert!(results.iter().all(|e| e.label == "Person"));
5602 }
5603
5604 #[test]
5605 fn test_find_entities_by_labels_empty_when_no_match() {
5606 let g = make_graph();
5607 g.add_entity(Entity::new("n1", "Node")).unwrap();
5608 let results = g.find_entities_by_labels(&["Missing"]).unwrap();
5609 assert!(results.is_empty());
5610 }
5611
5612 #[test]
5613 fn test_remove_isolated_removes_nodes_without_edges() {
5614 let g = make_graph();
5615 g.add_entity(Entity::new("connected", "N")).unwrap();
5616 g.add_entity(Entity::new("isolated", "N")).unwrap();
5617 g.add_entity(Entity::new("other", "N")).unwrap();
5618 g.add_relationship(Relationship::new("connected", "other", "r", 1.0)).unwrap();
5619 let removed = g.remove_isolated().unwrap();
5620 assert_eq!(removed, 1);
5621 assert!(g.get_entity(&EntityId::new("isolated")).is_err());
5622 assert!(g.get_entity(&EntityId::new("connected")).is_ok());
5623 }
5624
5625 #[test]
5626 fn test_remove_isolated_zero_when_all_connected() {
5627 let g = make_graph();
5628 g.add_entity(Entity::new("a", "N")).unwrap();
5629 g.add_entity(Entity::new("b", "N")).unwrap();
5630 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
5631 assert_eq!(g.remove_isolated().unwrap(), 0);
5632 }
5633
5634 #[test]
5637 fn test_successors_returns_direct_out_neighbors() {
5638 let g = GraphStore::new();
5639 g.add_entity(Entity::new("a", "A")).unwrap();
5640 g.add_entity(Entity::new("b", "B")).unwrap();
5641 g.add_entity(Entity::new("c", "C")).unwrap();
5642 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
5643 g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
5644 let mut ids: Vec<String> = g
5645 .successors(&EntityId::new("a"))
5646 .unwrap()
5647 .iter()
5648 .map(|e| e.id.as_str().to_string())
5649 .collect();
5650 ids.sort();
5651 assert_eq!(ids, vec!["b", "c"]);
5652 }
5653
5654 #[test]
5655 fn test_successors_empty_for_sink_node() {
5656 let g = GraphStore::new();
5657 g.add_entity(Entity::new("leaf", "L")).unwrap();
5658 assert!(g.successors(&EntityId::new("leaf")).unwrap().is_empty());
5659 }
5660
5661 #[test]
5662 fn test_is_sink_true_for_node_with_no_outgoing_edges() {
5663 let g = GraphStore::new();
5664 g.add_entity(Entity::new("a", "A")).unwrap();
5665 g.add_entity(Entity::new("b", "B")).unwrap();
5666 g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
5667 assert!(!g.is_sink(&EntityId::new("a")).unwrap());
5668 assert!(g.is_sink(&EntityId::new("b")).unwrap());
5669 }
5670
5671 #[test]
5672 fn test_is_sink_true_for_unknown_entity() {
5673 let g = GraphStore::new();
5674 assert!(g.is_sink(&EntityId::new("ghost")).unwrap());
5675 }
5676
5677 #[test]
5680 fn test_reachable_from_returns_all_downstream_nodes() {
5681 let g = GraphStore::new();
5682 g.add_entity(Entity::new("a", "N")).unwrap();
5683 g.add_entity(Entity::new("b", "N")).unwrap();
5684 g.add_entity(Entity::new("c", "N")).unwrap();
5685 g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
5686 g.add_relationship(Relationship::new("b", "c", "edge", 1.0)).unwrap();
5687 let reachable = g.reachable_from(&EntityId::new("a")).unwrap();
5688 assert!(reachable.contains(&EntityId::new("b")));
5689 assert!(reachable.contains(&EntityId::new("c")));
5690 assert!(!reachable.contains(&EntityId::new("a")));
5691 }
5692
5693 #[test]
5694 fn test_reachable_from_empty_for_sink_node() {
5695 let g = GraphStore::new();
5696 g.add_entity(Entity::new("sink", "N")).unwrap();
5697 let reachable = g.reachable_from(&EntityId::new("sink")).unwrap();
5698 assert!(reachable.is_empty());
5699 }
5700
5701 #[test]
5702 fn test_reachable_from_empty_for_unknown_node() {
5703 let g = GraphStore::new();
5704 let reachable = g.reachable_from(&EntityId::new("ghost")).unwrap();
5705 assert!(reachable.is_empty());
5706 }
5707
5708 #[test]
5709 fn test_contains_cycle_false_for_dag() {
5710 let g = GraphStore::new();
5711 g.add_entity(Entity::new("a", "N")).unwrap();
5712 g.add_entity(Entity::new("b", "N")).unwrap();
5713 g.add_entity(Entity::new("c", "N")).unwrap();
5714 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
5715 g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
5716 assert!(!g.contains_cycle().unwrap());
5717 }
5718
5719 #[test]
5720 fn test_contains_cycle_true_for_cyclic_graph() {
5721 let g = GraphStore::new();
5722 g.add_entity(Entity::new("x", "N")).unwrap();
5723 g.add_entity(Entity::new("y", "N")).unwrap();
5724 g.add_relationship(Relationship::new("x", "y", "e", 1.0)).unwrap();
5725 g.add_relationship(Relationship::new("y", "x", "e", 1.0)).unwrap();
5726 assert!(g.contains_cycle().unwrap());
5727 }
5728
5729 #[test]
5730 fn test_contains_cycle_false_for_empty_graph() {
5731 let g = GraphStore::new();
5732 assert!(!g.contains_cycle().unwrap());
5733 }
5734
5735 #[test]
5738 fn test_is_acyclic_true_for_dag() {
5739 let g = GraphStore::new();
5740 g.add_entity(Entity::new("a", "N")).unwrap();
5741 g.add_entity(Entity::new("b", "N")).unwrap();
5742 g.add_entity(Entity::new("c", "N")).unwrap();
5743 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
5744 g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
5745 assert!(g.is_acyclic().unwrap());
5746 }
5747
5748 #[test]
5749 fn test_is_acyclic_false_for_cyclic_graph() {
5750 let g = GraphStore::new();
5751 g.add_entity(Entity::new("x", "N")).unwrap();
5752 g.add_entity(Entity::new("y", "N")).unwrap();
5753 g.add_relationship(Relationship::new("x", "y", "e", 1.0)).unwrap();
5754 g.add_relationship(Relationship::new("y", "x", "e", 1.0)).unwrap();
5755 assert!(!g.is_acyclic().unwrap());
5756 }
5757
5758 #[test]
5759 fn test_is_acyclic_true_for_empty_graph() {
5760 let g = GraphStore::new();
5761 assert!(g.is_acyclic().unwrap());
5762 }
5763
5764 #[test]
5767 fn test_count_relationships_by_kind_returns_correct_count() {
5768 let g = make_graph();
5769 g.add_entity(Entity::new("a", "N")).unwrap();
5770 g.add_entity(Entity::new("b", "N")).unwrap();
5771 g.add_entity(Entity::new("c", "N")).unwrap();
5772 g.add_relationship(Relationship::new("a", "b", "knows", 1.0)).unwrap();
5773 g.add_relationship(Relationship::new("b", "c", "knows", 1.0)).unwrap();
5774 g.add_relationship(Relationship::new("a", "c", "likes", 0.5)).unwrap();
5775 assert_eq!(g.count_relationships_by_kind("knows").unwrap(), 2);
5776 assert_eq!(g.count_relationships_by_kind("likes").unwrap(), 1);
5777 assert_eq!(g.count_relationships_by_kind("absent").unwrap(), 0);
5778 }
5779
5780 #[test]
5781 fn test_merge_imports_entities_and_relationships() {
5782 let g1 = make_graph();
5783 g1.add_entity(Entity::new("a", "N")).unwrap();
5784 g1.add_entity(Entity::new("b", "N")).unwrap();
5785 g1.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
5786
5787 let g2 = make_graph();
5788 g2.add_entity(Entity::new("c", "N")).unwrap();
5789 g2.add_entity(Entity::new("a", "N")).unwrap(); g2.add_relationship(Relationship::new("c", "a", "s", 0.5)).unwrap();
5791
5792 g1.merge(&g2).unwrap();
5793 assert_eq!(g1.entity_count().unwrap(), 3); assert_eq!(g1.relationship_count().unwrap(), 2); }
5796
5797 #[test]
5798 fn test_top_nodes_by_in_degree_returns_sinks() {
5799 let g = make_graph();
5800 g.add_entity(Entity::new("hub", "N")).unwrap();
5801 g.add_entity(Entity::new("src1", "N")).unwrap();
5802 g.add_entity(Entity::new("src2", "N")).unwrap();
5803 g.add_relationship(Relationship::new("src1", "hub", "r", 1.0)).unwrap();
5804 g.add_relationship(Relationship::new("src2", "hub", "r", 1.0)).unwrap();
5805 let top = g.top_nodes_by_in_degree(1).unwrap();
5806 assert_eq!(top.len(), 1);
5807 assert_eq!(top[0].id.as_str(), "hub");
5808 }
5809
5810 #[test]
5811 fn test_top_nodes_by_out_degree_returns_sources() {
5812 let g = make_graph();
5813 g.add_entity(Entity::new("src", "N")).unwrap();
5814 g.add_entity(Entity::new("a", "N")).unwrap();
5815 g.add_entity(Entity::new("b", "N")).unwrap();
5816 g.add_relationship(Relationship::new("src", "a", "r", 1.0)).unwrap();
5817 g.add_relationship(Relationship::new("src", "b", "r", 1.0)).unwrap();
5818 let top = g.top_nodes_by_out_degree(1).unwrap();
5819 assert_eq!(top.len(), 1);
5820 assert_eq!(top[0].id.as_str(), "src");
5821 }
5822
5823 #[test]
5826 fn test_entity_property_value_returns_value() {
5827 let e = Entity::new("n1", "Node")
5828 .with_property("age", serde_json::Value::Number(42.into()));
5829 let val = e.property_value("age");
5830 assert!(val.is_some());
5831 assert_eq!(val.unwrap(), &serde_json::Value::Number(42.into()));
5832 }
5833
5834 #[test]
5835 fn test_entity_property_value_missing_returns_none() {
5836 let e = Entity::new("n1", "Node");
5837 assert!(e.property_value("missing").is_none());
5838 }
5839
5840 #[test]
5841 fn test_find_relationships_by_kind_returns_matching() {
5842 let g = GraphStore::new();
5843 g.add_entity(Entity::new("a", "N")).unwrap();
5844 g.add_entity(Entity::new("b", "N")).unwrap();
5845 g.add_entity(Entity::new("c", "N")).unwrap();
5846 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
5847 g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
5848 g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
5849 let rels = g.find_relationships_by_kind("KNOWS").unwrap();
5850 assert_eq!(rels.len(), 2);
5851 assert!(rels.iter().all(|r| r.kind == "KNOWS"));
5852 }
5853
5854 #[test]
5855 fn test_find_relationships_by_kind_no_match_returns_empty() {
5856 let g = GraphStore::new();
5857 g.add_entity(Entity::new("a", "N")).unwrap();
5858 g.add_entity(Entity::new("b", "N")).unwrap();
5859 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
5860 let rels = g.find_relationships_by_kind("HATES").unwrap();
5861 assert!(rels.is_empty());
5862 }
5863
5864 #[test]
5865 fn test_count_relationships_by_kind_correct() {
5866 let g = GraphStore::new();
5867 g.add_entity(Entity::new("a", "N")).unwrap();
5868 g.add_entity(Entity::new("b", "N")).unwrap();
5869 g.add_entity(Entity::new("c", "N")).unwrap();
5870 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
5871 g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
5872 g.add_relationship(Relationship::new("a", "c", "PART_OF", 1.0)).unwrap();
5873 assert_eq!(g.count_relationships_by_kind("KNOWS").unwrap(), 2);
5874 assert_eq!(g.count_relationships_by_kind("PART_OF").unwrap(), 1);
5875 assert_eq!(g.count_relationships_by_kind("MISSING").unwrap(), 0);
5876 }
5877
5878 #[test]
5881 fn test_max_out_degree_returns_highest_out_degree() {
5882 let g = GraphStore::new();
5883 g.add_entity(Entity::new("a", "N")).unwrap();
5884 g.add_entity(Entity::new("b", "N")).unwrap();
5885 g.add_entity(Entity::new("c", "N")).unwrap();
5886 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
5889 g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
5890 g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
5891 assert_eq!(g.max_out_degree().unwrap(), 2);
5892 }
5893
5894 #[test]
5895 fn test_max_out_degree_zero_for_empty_graph() {
5896 let g = GraphStore::new();
5897 assert_eq!(g.max_out_degree().unwrap(), 0);
5898 }
5899
5900 #[test]
5901 fn test_max_in_degree_returns_highest_in_degree() {
5902 let g = GraphStore::new();
5903 g.add_entity(Entity::new("a", "N")).unwrap();
5904 g.add_entity(Entity::new("b", "N")).unwrap();
5905 g.add_entity(Entity::new("c", "N")).unwrap();
5906 g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
5908 g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
5909 assert_eq!(g.max_in_degree().unwrap(), 2);
5910 }
5911
5912 #[test]
5913 fn test_max_in_degree_zero_for_empty_graph() {
5914 let g = GraphStore::new();
5915 assert_eq!(g.max_in_degree().unwrap(), 0);
5916 }
5917
5918 #[test]
5921 fn test_sum_edge_weights_correct_sum() {
5922 let g = GraphStore::new();
5923 g.add_entity(Entity::new("a", "N")).unwrap();
5924 g.add_entity(Entity::new("b", "N")).unwrap();
5925 g.add_entity(Entity::new("c", "N")).unwrap();
5926 g.add_relationship(Relationship::new("a", "b", "e", 1.5)).unwrap();
5927 g.add_relationship(Relationship::new("b", "c", "e", 2.5)).unwrap();
5928 let total = g.sum_edge_weights().unwrap();
5929 assert!((total - 4.0).abs() < 1e-9);
5930 }
5931
5932 #[test]
5933 fn test_sum_edge_weights_zero_for_empty_graph() {
5934 let g = GraphStore::new();
5935 assert!((g.sum_edge_weights().unwrap() - 0.0).abs() < 1e-9);
5936 }
5937
5938 #[test]
5941 fn test_weight_stats_none_for_empty_graph() {
5942 let g = GraphStore::new();
5943 assert!(g.weight_stats().unwrap().is_none());
5944 }
5945
5946 #[test]
5947 fn test_weight_stats_returns_correct_min_max_mean() {
5948 let g = GraphStore::new();
5949 g.add_entity(Entity::new("a", "N")).unwrap();
5950 g.add_entity(Entity::new("b", "N")).unwrap();
5951 g.add_entity(Entity::new("c", "N")).unwrap();
5952 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
5953 g.add_relationship(Relationship::new("b", "c", "e", 3.0)).unwrap();
5954 let (min, max, mean) = g.weight_stats().unwrap().unwrap();
5955 assert!((min - 1.0).abs() < 1e-9);
5956 assert!((max - 3.0).abs() < 1e-9);
5957 assert!((mean - 2.0).abs() < 1e-9);
5958 }
5959
5960 #[test]
5963 fn test_isolated_nodes_empty_graph_returns_empty_set() {
5964 let g = GraphStore::new();
5965 assert!(g.isolated_nodes().unwrap().is_empty());
5966 }
5967
5968 #[test]
5969 fn test_isolated_nodes_all_connected_returns_empty() {
5970 let g = GraphStore::new();
5971 g.add_entity(Entity::new("a", "N")).unwrap();
5972 g.add_entity(Entity::new("b", "N")).unwrap();
5973 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
5974 assert!(g.isolated_nodes().unwrap().is_empty());
5976 }
5977
5978 #[test]
5979 fn test_isolated_nodes_returns_orphan_entity() {
5980 let g = GraphStore::new();
5981 g.add_entity(Entity::new("a", "N")).unwrap();
5982 g.add_entity(Entity::new("b", "N")).unwrap();
5983 g.add_entity(Entity::new("orphan", "N")).unwrap();
5984 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
5985 let iso = g.isolated_nodes().unwrap();
5986 assert_eq!(iso.len(), 1);
5987 assert!(iso.contains(&EntityId::new("orphan")));
5988 }
5989
5990 #[test]
5993 fn test_reverse_flips_edge_direction() {
5994 let g = GraphStore::new();
5995 g.add_entity(Entity::new("x", "N")).unwrap();
5996 g.add_entity(Entity::new("y", "N")).unwrap();
5997 g.add_relationship(Relationship::new("x", "y", "edge", 1.0)).unwrap();
5998 let rev = g.reverse().unwrap();
5999 let y_id = EntityId::new("y");
6001 let succs = rev.successors(&y_id).unwrap();
6002 assert!(succs.iter().any(|e| e.id == EntityId::new("x")));
6003 }
6004
6005 #[test]
6006 fn test_reverse_empty_graph_produces_empty_reverse() {
6007 let g = GraphStore::new();
6008 let rev = g.reverse().unwrap();
6009 assert!(rev.is_empty().unwrap());
6010 }
6011
6012 #[test]
6013 fn test_max_in_degree_entity_none_for_empty_graph() {
6014 let g = GraphStore::new();
6015 assert!(g.max_in_degree_entity().unwrap().is_none());
6016 }
6017
6018 #[test]
6019 fn test_max_in_degree_entity_returns_node_with_most_incoming() {
6020 let g = GraphStore::new();
6021 g.add_entity(Entity::new("hub", "N")).unwrap();
6022 g.add_entity(Entity::new("a", "N")).unwrap();
6023 g.add_entity(Entity::new("b", "N")).unwrap();
6024 g.add_relationship(Relationship::new("a", "hub", "e", 1.0)).unwrap();
6025 g.add_relationship(Relationship::new("b", "hub", "e", 1.0)).unwrap();
6026 let best = g.max_in_degree_entity().unwrap().unwrap();
6027 assert_eq!(best.id, EntityId::new("hub"));
6028 }
6029
6030 #[test]
6031 fn test_shortest_path_length_none_when_no_path() {
6032 let g = GraphStore::new();
6033 g.add_entity(Entity::new("a", "N")).unwrap();
6034 g.add_entity(Entity::new("b", "N")).unwrap();
6035 let len = g
6036 .shortest_path_length(&EntityId::new("a"), &EntityId::new("b"))
6037 .unwrap();
6038 assert!(len.is_none());
6039 }
6040
6041 #[test]
6042 fn test_shortest_path_length_one_for_direct_edge() {
6043 let g = GraphStore::new();
6044 g.add_entity(Entity::new("a", "N")).unwrap();
6045 g.add_entity(Entity::new("b", "N")).unwrap();
6046 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
6047 let len = g
6048 .shortest_path_length(&EntityId::new("a"), &EntityId::new("b"))
6049 .unwrap();
6050 assert_eq!(len, Some(1));
6051 }
6052
6053 #[test]
6054 fn test_shortest_path_length_two_for_two_hop_path() {
6055 let g = GraphStore::new();
6056 g.add_entity(Entity::new("a", "N")).unwrap();
6057 g.add_entity(Entity::new("b", "N")).unwrap();
6058 g.add_entity(Entity::new("c", "N")).unwrap();
6059 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
6060 g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
6061 let len = g
6062 .shortest_path_length(&EntityId::new("a"), &EntityId::new("c"))
6063 .unwrap();
6064 assert_eq!(len, Some(2));
6065 }
6066
6067 #[test]
6070 fn test_avg_out_degree_zero_for_empty_graph() {
6071 let g = GraphStore::new();
6072 assert!((g.avg_out_degree().unwrap() - 0.0).abs() < 1e-9);
6073 }
6074
6075 #[test]
6076 fn test_avg_out_degree_correct_mean() {
6077 let g = GraphStore::new();
6078 g.add_entity(Entity::new("a", "N")).unwrap();
6079 g.add_entity(Entity::new("b", "N")).unwrap();
6080 g.add_entity(Entity::new("c", "N")).unwrap();
6081 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
6083 g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
6084 let avg = g.avg_out_degree().unwrap();
6086 assert!((avg - 2.0 / 3.0).abs() < 1e-9);
6087 }
6088
6089 #[test]
6090 fn test_avg_in_degree_zero_for_empty_graph() {
6091 let g = GraphStore::new();
6092 assert!((g.avg_in_degree().unwrap() - 0.0).abs() < 1e-9);
6093 }
6094
6095 #[test]
6096 fn test_avg_in_degree_correct_mean() {
6097 let g = GraphStore::new();
6098 g.add_entity(Entity::new("a", "N")).unwrap();
6099 g.add_entity(Entity::new("b", "N")).unwrap();
6100 g.add_entity(Entity::new("c", "N")).unwrap();
6101 g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
6103 g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
6104 let avg = g.avg_in_degree().unwrap();
6106 assert!((avg - 2.0 / 3.0).abs() < 1e-9);
6107 }
6108
6109 #[test]
6112 fn test_graph_store_has_entity_false_when_missing() {
6113 let g = GraphStore::new();
6114 let id = EntityId::new("nonexistent");
6115 assert!(!g.has_entity(&id).unwrap());
6116 }
6117
6118 #[test]
6119 fn test_graph_store_has_entity_true_after_add() {
6120 let g = GraphStore::new();
6121 g.add_entity(Entity::new("node-a", "Person")).unwrap();
6122 let id = EntityId::new("node-a");
6123 assert!(g.has_entity(&id).unwrap());
6124 }
6125
6126 #[test]
6130 fn test_entity_with_property_stores_value() {
6131 let e = Entity::new("e1", "Label")
6132 .with_property("score", serde_json::json!(42));
6133 assert_eq!(e.property_value("score"), Some(&serde_json::json!(42)));
6134 }
6135
6136 #[test]
6137 fn test_entity_with_property_chaining() {
6138 let e = Entity::new("e2", "L")
6139 .with_property("a", serde_json::json!(1))
6140 .with_property("b", serde_json::json!(2));
6141 assert!(e.has_property("a"));
6142 assert!(e.has_property("b"));
6143 }
6144
6145 #[test]
6146 fn test_remove_relationship_succeeds_when_exists() {
6147 let g = GraphStore::new();
6148 g.add_entity(Entity::new("x", "N")).unwrap();
6149 g.add_entity(Entity::new("y", "N")).unwrap();
6150 g.add_relationship(Relationship::new("x", "y", "link", 1.0)).unwrap();
6151 g.remove_relationship(&EntityId::new("x"), &EntityId::new("y"), "link").unwrap();
6152 assert_eq!(g.relationship_count().unwrap(), 0);
6153 }
6154
6155 #[test]
6156 fn test_remove_relationship_errors_when_missing() {
6157 let g = GraphStore::new();
6158 g.add_entity(Entity::new("x", "N")).unwrap();
6159 g.add_entity(Entity::new("y", "N")).unwrap();
6160 let result = g.remove_relationship(&EntityId::new("x"), &EntityId::new("y"), "ghost");
6161 assert!(result.is_err());
6162 }
6163
6164 #[test]
6165 fn test_update_entity_property_returns_true_when_entity_exists() {
6166 let g = GraphStore::new();
6167 g.add_entity(Entity::new("node", "N")).unwrap();
6168 let updated = g
6169 .update_entity_property(&EntityId::new("node"), "color", serde_json::json!("red"))
6170 .unwrap();
6171 assert!(updated);
6172 let entity = g.get_entity(&EntityId::new("node")).unwrap();
6173 assert_eq!(entity.property_value("color"), Some(&serde_json::json!("red")));
6174 }
6175
6176 #[test]
6177 fn test_update_entity_property_returns_false_for_unknown_entity() {
6178 let g = GraphStore::new();
6179 let updated = g
6180 .update_entity_property(&EntityId::new("ghost"), "key", serde_json::json!(1))
6181 .unwrap();
6182 assert!(!updated);
6183 }
6184
6185 #[test]
6188 fn test_graph_store_has_any_entities_false_when_empty() {
6189 let g = GraphStore::new();
6190 assert!(!g.has_any_entities().unwrap());
6191 }
6192
6193 #[test]
6194 fn test_graph_store_has_any_entities_true_after_add() {
6195 let g = GraphStore::new();
6196 g.add_entity(Entity::new("x", "Node")).unwrap();
6197 assert!(g.has_any_entities().unwrap());
6198 }
6199
6200 #[test]
6203 fn test_entity_type_count_zero_for_empty_graph() {
6204 let g = GraphStore::new();
6205 assert_eq!(g.entity_type_count().unwrap(), 0);
6206 }
6207
6208 #[test]
6209 fn test_entity_type_count_counts_distinct_labels() {
6210 let g = GraphStore::new();
6211 g.add_entity(Entity::new("a", "Person")).unwrap();
6212 g.add_entity(Entity::new("b", "Person")).unwrap();
6213 g.add_entity(Entity::new("c", "Concept")).unwrap();
6214 assert_eq!(g.entity_type_count().unwrap(), 2);
6216 }
6217
6218 #[test]
6221 fn test_orphan_count_zero_for_empty_graph() {
6222 let g = GraphStore::new();
6223 assert_eq!(g.orphan_count().unwrap(), 0);
6224 }
6225
6226 #[test]
6227 fn test_orphan_count_all_orphans_with_no_relationships() {
6228 let g = GraphStore::new();
6229 g.add_entity(Entity::new("a", "N")).unwrap();
6230 g.add_entity(Entity::new("b", "N")).unwrap();
6231 assert_eq!(g.orphan_count().unwrap(), 2);
6232 }
6233
6234 #[test]
6235 fn test_orphan_count_excludes_entities_with_edges() {
6236 let g = GraphStore::new();
6237 g.add_entity(Entity::new("a", "N")).unwrap();
6238 g.add_entity(Entity::new("b", "N")).unwrap();
6239 g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
6240 assert_eq!(g.orphan_count().unwrap(), 1);
6242 }
6243
6244 #[test]
6247 fn test_labels_empty_for_empty_graph() {
6248 let g = GraphStore::new();
6249 assert!(g.labels().unwrap().is_empty());
6250 }
6251
6252 #[test]
6253 fn test_labels_returns_distinct_sorted_labels() {
6254 let g = GraphStore::new();
6255 g.add_entity(Entity::new("a", "Concept")).unwrap();
6256 g.add_entity(Entity::new("b", "Person")).unwrap();
6257 g.add_entity(Entity::new("c", "Concept")).unwrap();
6258 assert_eq!(g.labels().unwrap(), vec!["Concept".to_string(), "Person".to_string()]);
6259 }
6260
6261 #[test]
6262 fn test_incoming_count_for_counts_inbound_edges() {
6263 let g = GraphStore::new();
6264 g.add_entity(Entity::new("a", "Node")).unwrap();
6265 g.add_entity(Entity::new("b", "Node")).unwrap();
6266 g.add_entity(Entity::new("c", "Node")).unwrap();
6267 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6268 g.add_relationship(Relationship::new("c", "b", "link", 1.0)).unwrap();
6269 assert_eq!(g.incoming_count_for(&EntityId::new("b")).unwrap(), 2);
6270 assert_eq!(g.incoming_count_for(&EntityId::new("a")).unwrap(), 0);
6271 }
6272
6273 #[test]
6274 fn test_outgoing_count_for_counts_outbound_edges() {
6275 let g = GraphStore::new();
6276 g.add_entity(Entity::new("a", "Node")).unwrap();
6277 g.add_entity(Entity::new("b", "Node")).unwrap();
6278 g.add_entity(Entity::new("c", "Node")).unwrap();
6279 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6280 g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
6281 assert_eq!(g.outgoing_count_for(&EntityId::new("a")).unwrap(), 2);
6282 assert_eq!(g.outgoing_count_for(&EntityId::new("b")).unwrap(), 0);
6283 }
6284
6285 #[test]
6286 fn test_source_count_returns_number_of_nodes_with_no_incoming_edges() {
6287 let g = GraphStore::new();
6288 g.add_entity(Entity::new("a", "Node")).unwrap();
6289 g.add_entity(Entity::new("b", "Node")).unwrap();
6290 g.add_entity(Entity::new("c", "Node")).unwrap();
6291 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6293 g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
6294 assert_eq!(g.source_count().unwrap(), 1);
6295 }
6296
6297 #[test]
6298 fn test_source_count_all_isolated_nodes_are_sources() {
6299 let g = GraphStore::new();
6300 g.add_entity(Entity::new("x", "Node")).unwrap();
6301 g.add_entity(Entity::new("y", "Node")).unwrap();
6302 assert_eq!(g.source_count().unwrap(), 2);
6303 }
6304
6305 #[test]
6306 fn test_sink_count_returns_nodes_with_no_outgoing_edges() {
6307 let g = GraphStore::new();
6308 g.add_entity(Entity::new("a", "Node")).unwrap();
6309 g.add_entity(Entity::new("b", "Node")).unwrap();
6310 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6311 assert_eq!(g.sink_count().unwrap(), 1);
6313 assert_eq!(g.source_count().unwrap(), 1);
6314 }
6315
6316 #[test]
6317 fn test_has_self_loops_true_when_self_loop_exists() {
6318 let g = GraphStore::new();
6319 g.add_entity(Entity::new("a", "Node")).unwrap();
6320 g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
6321 assert!(g.has_self_loops().unwrap());
6322 }
6323
6324 #[test]
6325 fn test_has_self_loops_false_when_no_self_loops() {
6326 let g = GraphStore::new();
6327 g.add_entity(Entity::new("a", "Node")).unwrap();
6328 g.add_entity(Entity::new("b", "Node")).unwrap();
6329 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6330 assert!(!g.has_self_loops().unwrap());
6331 }
6332
6333 #[test]
6334 fn test_bidirectional_count_counts_mutual_edges() {
6335 let g = GraphStore::new();
6336 g.add_entity(Entity::new("a", "Node")).unwrap();
6337 g.add_entity(Entity::new("b", "Node")).unwrap();
6338 g.add_entity(Entity::new("c", "Node")).unwrap();
6339 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6340 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);
6343 }
6344
6345 #[test]
6346 fn test_bidirectional_count_zero_when_no_mutual_edges() {
6347 let g = GraphStore::new();
6348 g.add_entity(Entity::new("a", "Node")).unwrap();
6349 g.add_entity(Entity::new("b", "Node")).unwrap();
6350 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6351 assert_eq!(g.bidirectional_count().unwrap(), 0);
6352 }
6353
6354 #[test]
6357 fn test_relationship_kind_count_counts_distinct_kinds() {
6358 let g = GraphStore::new();
6359 g.add_entity(Entity::new("a", "N")).unwrap();
6360 g.add_entity(Entity::new("b", "N")).unwrap();
6361 g.add_entity(Entity::new("c", "N")).unwrap();
6362 g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
6363 g.add_relationship(Relationship::new("b", "c", "friend", 1.0)).unwrap();
6364 g.add_relationship(Relationship::new("a", "c", "enemy", 1.0)).unwrap();
6365 assert_eq!(g.relationship_kind_count().unwrap(), 2);
6366 }
6367
6368 #[test]
6369 fn test_relationship_kind_count_zero_when_empty() {
6370 let g = GraphStore::new();
6371 assert_eq!(g.relationship_kind_count().unwrap(), 0);
6372 }
6373
6374 #[test]
6375 fn test_entities_with_self_loops_returns_self_loop_ids() {
6376 let g = GraphStore::new();
6377 g.add_entity(Entity::new("a", "N")).unwrap();
6378 g.add_entity(Entity::new("b", "N")).unwrap();
6379 g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
6380 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6381 let ids = g.entities_with_self_loops().unwrap();
6382 assert_eq!(ids, vec![EntityId::new("a")]);
6383 }
6384
6385 #[test]
6386 fn test_entities_with_self_loops_empty_when_no_self_loops() {
6387 let g = GraphStore::new();
6388 g.add_entity(Entity::new("a", "N")).unwrap();
6389 g.add_entity(Entity::new("b", "N")).unwrap();
6390 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6391 assert!(g.entities_with_self_loops().unwrap().is_empty());
6392 }
6393
6394 #[test]
6397 fn test_isolated_entity_count_returns_count_with_no_edges() {
6398 let g = GraphStore::new();
6399 g.add_entity(Entity::new("a", "N")).unwrap();
6400 g.add_entity(Entity::new("b", "N")).unwrap();
6401 g.add_entity(Entity::new("c", "N")).unwrap();
6402 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6403 assert_eq!(g.isolated_entity_count().unwrap(), 1);
6405 }
6406
6407 #[test]
6408 fn test_isolated_entity_count_zero_when_all_connected() {
6409 let g = GraphStore::new();
6410 g.add_entity(Entity::new("a", "N")).unwrap();
6411 g.add_entity(Entity::new("b", "N")).unwrap();
6412 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6413 assert_eq!(g.isolated_entity_count().unwrap(), 0);
6414 }
6415
6416 #[test]
6417 fn test_avg_relationship_weight_returns_mean() {
6418 let g = GraphStore::new();
6419 g.add_entity(Entity::new("a", "N")).unwrap();
6420 g.add_entity(Entity::new("b", "N")).unwrap();
6421 g.add_entity(Entity::new("c", "N")).unwrap();
6422 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6423 g.add_relationship(Relationship::new("b", "c", "link", 3.0)).unwrap();
6424 let avg = g.avg_relationship_weight().unwrap();
6425 assert!((avg - 2.0).abs() < 1e-6);
6426 }
6427
6428 #[test]
6429 fn test_avg_relationship_weight_zero_when_no_relationships() {
6430 let g = GraphStore::new();
6431 assert_eq!(g.avg_relationship_weight().unwrap(), 0.0);
6432 }
6433
6434 #[test]
6437 fn test_total_in_degree_equals_relationship_count() {
6438 let g = GraphStore::new();
6439 g.add_entity(Entity::new("a", "N")).unwrap();
6440 g.add_entity(Entity::new("b", "N")).unwrap();
6441 g.add_entity(Entity::new("c", "N")).unwrap();
6442 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6443 g.add_relationship(Relationship::new("b", "c", "link", 1.0)).unwrap();
6444 assert_eq!(g.total_in_degree().unwrap(), 2);
6445 }
6446
6447 #[test]
6448 fn test_total_in_degree_zero_when_no_relationships() {
6449 let g = GraphStore::new();
6450 assert_eq!(g.total_in_degree().unwrap(), 0);
6451 }
6452
6453 #[test]
6454 fn test_relationship_count_between_counts_edges_between_pair() {
6455 let g = GraphStore::new();
6456 g.add_entity(Entity::new("a", "N")).unwrap();
6457 g.add_entity(Entity::new("b", "N")).unwrap();
6458 g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
6459 g.add_relationship(Relationship::new("a", "b", "colleague", 1.0)).unwrap();
6460 let from = EntityId::new("a");
6461 let to = EntityId::new("b");
6462 assert_eq!(g.relationship_count_between(&from, &to).unwrap(), 2);
6463 }
6464
6465 #[test]
6466 fn test_relationship_count_between_returns_zero_for_no_edge() {
6467 let g = GraphStore::new();
6468 g.add_entity(Entity::new("a", "N")).unwrap();
6469 g.add_entity(Entity::new("b", "N")).unwrap();
6470 let from = EntityId::new("a");
6471 let to = EntityId::new("b");
6472 assert_eq!(g.relationship_count_between(&from, &to).unwrap(), 0);
6473 }
6474
6475 #[test]
6478 fn test_edges_from_returns_all_outgoing_relationships() {
6479 let g = GraphStore::new();
6480 g.add_entity(Entity::new("a", "N")).unwrap();
6481 g.add_entity(Entity::new("b", "N")).unwrap();
6482 g.add_entity(Entity::new("c", "N")).unwrap();
6483 g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
6484 g.add_relationship(Relationship::new("a", "c", "enemy", 0.5)).unwrap();
6485 let edges = g.edges_from(&EntityId::new("a")).unwrap();
6486 assert_eq!(edges.len(), 2);
6487 }
6488
6489 #[test]
6490 fn test_edges_from_returns_empty_for_unknown_entity() {
6491 let g = GraphStore::new();
6492 assert!(g.edges_from(&EntityId::new("missing")).unwrap().is_empty());
6493 }
6494
6495 #[test]
6496 fn test_neighbors_of_returns_sorted_unique_targets() {
6497 let g = GraphStore::new();
6498 g.add_entity(Entity::new("a", "N")).unwrap();
6499 g.add_entity(Entity::new("b", "N")).unwrap();
6500 g.add_entity(Entity::new("c", "N")).unwrap();
6501 g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
6502 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6503 let neighbors = g.neighbors_of(&EntityId::new("a")).unwrap();
6504 assert_eq!(neighbors, vec![EntityId::new("b"), EntityId::new("c")]);
6505 }
6506
6507 #[test]
6508 fn test_neighbors_of_returns_empty_for_no_outgoing_edges() {
6509 let g = GraphStore::new();
6510 g.add_entity(Entity::new("a", "N")).unwrap();
6511 assert!(g.neighbors_of(&EntityId::new("a")).unwrap().is_empty());
6512 }
6513
6514 #[test]
6517 fn test_entity_id_from_string() {
6518 let id = EntityId::from("node-1".to_owned());
6519 assert_eq!(id.as_str(), "node-1");
6520 }
6521
6522 #[test]
6523 fn test_entity_id_from_str_ref() {
6524 let id = EntityId::from("node-2");
6525 assert_eq!(id.as_str(), "node-2");
6526 }
6527
6528 #[test]
6529 fn test_entity_id_from_str_parse_rejects_empty() {
6530 let result: Result<EntityId, _> = "".parse();
6531 assert!(result.is_err());
6532 }
6533
6534 #[test]
6535 fn test_entity_id_from_str_parse_accepts_nonempty() {
6536 let id: EntityId = "alice".parse().unwrap();
6537 assert_eq!(id.as_str(), "alice");
6538 }
6539
6540 #[test]
6541 fn test_entity_id_deref_to_str() {
6542 let id = EntityId::new("deref-node");
6543 let s: &str = &id;
6544 assert_eq!(s, "deref-node");
6545 }
6546
6547 #[test]
6548 fn test_entity_id_deref_enables_str_methods() {
6549 let id = EntityId::new("hello-world");
6550 assert!(id.contains('-'));
6551 assert_eq!(id.len(), 11);
6552 }
6553
6554 #[test]
6555 fn test_entity_remove_property_returns_value() {
6556 let mut e = Entity::new("e1", "Person")
6557 .with_property("age", serde_json::json!(30));
6558 let removed = e.remove_property("age");
6559 assert_eq!(removed, Some(serde_json::json!(30)));
6560 assert!(!e.has_property("age"));
6561 }
6562
6563 #[test]
6564 fn test_entity_remove_property_returns_none_when_absent() {
6565 let mut e = Entity::new("e2", "Person");
6566 assert!(e.remove_property("nonexistent").is_none());
6567 }
6568
6569 #[test]
6570 fn test_entity_property_count() {
6571 let e = Entity::new("e3", "X")
6572 .with_property("a", serde_json::json!(1))
6573 .with_property("b", serde_json::json!(2));
6574 assert_eq!(e.property_count(), 2);
6575 }
6576
6577 #[test]
6578 fn test_entity_properties_is_empty_true_when_none() {
6579 let e = Entity::new("e4", "X");
6580 assert!(e.properties_is_empty());
6581 }
6582
6583 #[test]
6584 fn test_entity_properties_is_empty_false_when_has_props() {
6585 let e = Entity::new("e5", "X").with_property("k", serde_json::json!("v"));
6586 assert!(!e.properties_is_empty());
6587 }
6588
6589 #[test]
6592 fn test_relationship_type_counts_returns_correct_map() {
6593 let g = GraphStore::new();
6594 add(&g, "a"); add(&g, "b"); add(&g, "c");
6595 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6596 g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
6597 g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
6598 let counts = g.relationship_type_counts().unwrap();
6599 assert_eq!(counts.get("KNOWS"), Some(&2));
6600 assert_eq!(counts.get("LIKES"), Some(&1));
6601 }
6602
6603 #[test]
6604 fn test_relationship_type_counts_empty_graph_returns_empty_map() {
6605 let g = GraphStore::new();
6606 assert!(g.relationship_type_counts().unwrap().is_empty());
6607 }
6608
6609 #[test]
6610 fn test_entities_without_property_returns_nodes_missing_key() {
6611 let g = GraphStore::new();
6612 g.add_entity(Entity::new("a", "N").with_property("age", serde_json::json!(30))).unwrap();
6613 g.add_entity(Entity::new("b", "N")).unwrap();
6614 g.add_entity(Entity::new("c", "N")).unwrap();
6615 let result = g.entities_without_property("age").unwrap();
6616 assert_eq!(result.len(), 2);
6617 assert!(result.iter().all(|e| !e.properties.contains_key("age")));
6618 }
6619
6620 #[test]
6621 fn test_entities_without_property_empty_when_all_have_key() {
6622 let g = GraphStore::new();
6623 g.add_entity(Entity::new("a", "N").with_property("role", serde_json::json!("admin"))).unwrap();
6624 assert!(g.entities_without_property("role").unwrap().is_empty());
6625 }
6626
6627 #[test]
6630 fn test_min_out_degree_returns_zero_when_sink_present() {
6631 let g = GraphStore::new();
6632 add(&g, "a"); add(&g, "b"); add(&g, "c");
6633 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6634 g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
6635 assert_eq!(g.min_out_degree().unwrap(), 0);
6637 }
6638
6639 #[test]
6640 fn test_min_out_degree_returns_zero_for_empty_graph() {
6641 let g = GraphStore::new();
6642 assert_eq!(g.min_out_degree().unwrap(), 0);
6643 }
6644
6645 #[test]
6646 fn test_min_in_degree_returns_zero_when_source_present() {
6647 let g = GraphStore::new();
6648 add(&g, "a"); add(&g, "b");
6649 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
6650 assert_eq!(g.min_in_degree().unwrap(), 0);
6652 }
6653
6654 #[test]
6655 fn test_min_in_degree_returns_zero_for_empty_graph() {
6656 let g = GraphStore::new();
6657 assert_eq!(g.min_in_degree().unwrap(), 0);
6658 }
6659
6660 #[test]
6661 fn test_relationship_kinds_from_returns_sorted_distinct_kinds() {
6662 let g = GraphStore::new();
6663 add(&g, "a"); add(&g, "b"); add(&g, "c");
6664 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6665 g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
6666 g.add_relationship(Relationship::new("a", "b", "TRUSTS", 1.0)).unwrap();
6667 let kinds = g.relationship_kinds_from(&EntityId::new("a")).unwrap();
6668 assert_eq!(kinds, vec!["KNOWS", "LIKES", "TRUSTS"]);
6669 }
6670
6671 #[test]
6672 fn test_relationship_kinds_from_returns_empty_for_unknown_entity() {
6673 let g = GraphStore::new();
6674 assert!(g.relationship_kinds_from(&EntityId::new("missing")).unwrap().is_empty());
6675 }
6676
6677 #[test]
6678 fn test_relationship_kinds_from_deduplicates_kinds() {
6679 let g = GraphStore::new();
6680 add(&g, "x"); add(&g, "y"); add(&g, "z");
6681 g.add_relationship(Relationship::new("x", "y", "SAME", 1.0)).unwrap();
6682 g.add_relationship(Relationship::new("x", "z", "SAME", 0.5)).unwrap();
6683 let kinds = g.relationship_kinds_from(&EntityId::new("x")).unwrap();
6684 assert_eq!(kinds, vec!["SAME"]);
6685 }
6686
6687 #[test]
6690 fn test_unique_relationship_types_returns_sorted_deduped_types() {
6691 let g = GraphStore::new();
6692 add(&g, "a"); add(&g, "b"); add(&g, "c");
6693 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6694 g.add_relationship(Relationship::new("b", "c", "LIKES", 1.0)).unwrap();
6695 g.add_relationship(Relationship::new("a", "c", "KNOWS", 0.5)).unwrap();
6696 let types = g.unique_relationship_types().unwrap();
6697 assert_eq!(types, vec!["KNOWS", "LIKES"]);
6698 }
6699
6700 #[test]
6701 fn test_unique_relationship_types_empty_graph_returns_empty() {
6702 let g = GraphStore::new();
6703 assert!(g.unique_relationship_types().unwrap().is_empty());
6704 }
6705
6706 #[test]
6707 fn test_avg_property_count_returns_correct_average() {
6708 let g = GraphStore::new();
6709 g.add_entity(Entity::new("a", "N")
6710 .with_property("x", serde_json::json!(1))
6711 .with_property("y", serde_json::json!(2))).unwrap();
6712 g.add_entity(Entity::new("b", "N")).unwrap(); assert!((g.avg_property_count().unwrap() - 1.0).abs() < 1e-9);
6715 }
6716
6717 #[test]
6718 fn test_avg_property_count_empty_graph_returns_zero() {
6719 let g = GraphStore::new();
6720 assert_eq!(g.avg_property_count().unwrap(), 0.0);
6721 }
6722
6723 #[test]
6726 fn test_property_key_frequency_counts_correctly() {
6727 let g = GraphStore::new();
6728 g.add_entity(Entity::new("a", "N")
6729 .with_property("age", serde_json::json!(30))
6730 .with_property("role", serde_json::json!("admin"))).unwrap();
6731 g.add_entity(Entity::new("b", "N")
6732 .with_property("age", serde_json::json!(25))).unwrap();
6733 let freq = g.property_key_frequency().unwrap();
6734 assert_eq!(freq.get("age"), Some(&2));
6735 assert_eq!(freq.get("role"), Some(&1));
6736 }
6737
6738 #[test]
6739 fn test_property_key_frequency_empty_graph_returns_empty() {
6740 let g = GraphStore::new();
6741 assert!(g.property_key_frequency().unwrap().is_empty());
6742 }
6743
6744 #[test]
6747 fn test_has_edge_returns_true_when_edge_exists() {
6748 let g = GraphStore::new();
6749 add(&g, "a"); add(&g, "b");
6750 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6751 assert!(g.has_edge(&EntityId::new("a"), &EntityId::new("b")).unwrap());
6752 }
6753
6754 #[test]
6755 fn test_has_edge_returns_false_when_no_edge() {
6756 let g = GraphStore::new();
6757 add(&g, "a"); add(&g, "b");
6758 assert!(!g.has_edge(&EntityId::new("a"), &EntityId::new("b")).unwrap());
6759 }
6760
6761 #[test]
6762 fn test_has_edge_is_directional() {
6763 let g = GraphStore::new();
6764 add(&g, "a"); add(&g, "b");
6765 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6766 assert!(!g.has_edge(&EntityId::new("b"), &EntityId::new("a")).unwrap());
6768 }
6769
6770 #[test]
6773 fn test_entity_display_with_props() {
6774 let e = Entity::new("alice", "Person")
6775 .with_property("age", serde_json::json!(30));
6776 let s = e.to_string();
6777 assert!(s.contains("alice") && s.contains("Person") && s.contains("props=1"));
6778 }
6779
6780 #[test]
6781 fn test_entity_display_no_props() {
6782 let e = Entity::new("bob", "Node");
6783 assert_eq!(e.to_string(), "Entity[id='bob', label='Node', props=0]");
6784 }
6785
6786 #[test]
6787 fn test_relationship_display_format() {
6788 let r = Relationship::new("alice", "bob", "KNOWS", 1.5);
6789 let s = r.to_string();
6790 assert!(s.contains("alice") && s.contains("KNOWS") && s.contains("bob") && s.contains("1.50"));
6791 }
6792
6793 #[test]
6794 fn test_graph_total_degree_sum_of_in_and_out() {
6795 let g = GraphStore::new();
6796 g.add_entity(Entity::new("a", "N")).unwrap();
6797 g.add_entity(Entity::new("b", "N")).unwrap();
6798 g.add_entity(Entity::new("c", "N")).unwrap();
6799 g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
6800 g.add_relationship(Relationship::new("c", "a", "e", 1.0)).unwrap();
6801 assert_eq!(g.total_degree(&EntityId::new("a")).unwrap(), 2);
6802 }
6803
6804 #[test]
6805 fn test_graph_total_degree_zero_for_isolated_node() {
6806 let g = GraphStore::new();
6807 g.add_entity(Entity::new("iso", "N")).unwrap();
6808 assert_eq!(g.total_degree(&EntityId::new("iso")).unwrap(), 0);
6809 }
6810
6811 #[test]
6812 fn test_graph_entity_property_keys_returns_sorted() {
6813 let g = GraphStore::new();
6814 let e = Entity::new("e1", "X")
6815 .with_property("z", serde_json::json!(1))
6816 .with_property("a", serde_json::json!(2))
6817 .with_property("m", serde_json::json!(3));
6818 g.add_entity(e).unwrap();
6819 assert_eq!(
6820 g.entity_property_keys(&EntityId::new("e1")).unwrap(),
6821 vec!["a", "m", "z"]
6822 );
6823 }
6824
6825 #[test]
6826 fn test_graph_entity_property_keys_empty_for_unknown() {
6827 let g = GraphStore::new();
6828 assert!(g.entity_property_keys(&EntityId::new("missing")).unwrap().is_empty());
6829 }
6830
6831 #[test]
6832 fn test_entity_property_keys_method_sorted() {
6833 let e = Entity::new("p", "T")
6834 .with_property("b", serde_json::json!(0))
6835 .with_property("a", serde_json::json!(0));
6836 let keys = e.property_keys();
6837 assert_eq!(keys, vec!["a", "b"]);
6838 }
6839
6840 #[test]
6843 fn test_entities_sorted_by_label_returns_alphabetical_order() {
6844 let g = GraphStore::new();
6845 g.add_entity(Entity::new("1", "Zebra")).unwrap();
6846 g.add_entity(Entity::new("2", "Apple")).unwrap();
6847 g.add_entity(Entity::new("3", "Mango")).unwrap();
6848 let sorted = g.entities_sorted_by_label().unwrap();
6849 assert_eq!(sorted[0].label, "Apple");
6850 assert_eq!(sorted[1].label, "Mango");
6851 assert_eq!(sorted[2].label, "Zebra");
6852 }
6853
6854 #[test]
6855 fn test_entities_sorted_by_label_empty_graph_returns_empty() {
6856 let g = GraphStore::new();
6857 assert!(g.entities_sorted_by_label().unwrap().is_empty());
6858 }
6859
6860 #[test]
6863 fn test_entities_with_property_returns_entities_with_key() {
6864 let g = GraphStore::new();
6865 g.add_entity(Entity::new("a", "N").with_property("age", serde_json::json!(30))).unwrap();
6866 g.add_entity(Entity::new("b", "N").with_property("name", serde_json::json!("bob"))).unwrap();
6867 g.add_entity(Entity::new("c", "N").with_property("age", serde_json::json!(25))).unwrap();
6868 let result = g.entities_with_property("age").unwrap();
6869 assert_eq!(result.len(), 2);
6870 assert!(result.iter().all(|e| e.properties.contains_key("age")));
6871 }
6872
6873 #[test]
6874 fn test_entities_with_property_returns_empty_when_no_match() {
6875 let g = GraphStore::new();
6876 add(&g, "a");
6877 assert!(g.entities_with_property("missing_key").unwrap().is_empty());
6878 }
6879
6880 #[test]
6881 fn test_total_relationship_count_returns_edge_count() {
6882 let g = GraphStore::new();
6883 add(&g, "a"); add(&g, "b"); add(&g, "c");
6884 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6885 g.add_relationship(Relationship::new("b", "c", "LIKES", 1.0)).unwrap();
6886 assert_eq!(g.total_relationship_count().unwrap(), 2);
6887 }
6888
6889 #[test]
6890 fn test_total_relationship_count_zero_for_empty_graph() {
6891 let g = GraphStore::new();
6892 assert_eq!(g.total_relationship_count().unwrap(), 0);
6893 }
6894
6895 #[test]
6898 fn test_entities_without_outgoing_returns_nodes_with_no_out_edges() {
6899 let g = GraphStore::new();
6900 add(&g, "a"); add(&g, "b");
6901 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6902 let no_out = g.entities_without_outgoing().unwrap();
6904 assert_eq!(no_out.len(), 1);
6905 assert_eq!(no_out[0].id, EntityId::new("b"));
6906 }
6907
6908 #[test]
6909 fn test_entities_without_outgoing_all_returned_for_empty_graph() {
6910 let g = GraphStore::new();
6911 add(&g, "x");
6912 let result = g.entities_without_outgoing().unwrap();
6913 assert_eq!(result.len(), 1);
6914 }
6915
6916 #[test]
6917 fn test_entities_without_incoming_returns_nodes_with_no_in_edges() {
6918 let g = GraphStore::new();
6919 add(&g, "a"); add(&g, "b");
6920 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6921 let no_in = g.entities_without_incoming().unwrap();
6923 assert_eq!(no_in.len(), 1);
6924 assert_eq!(no_in[0].id, EntityId::new("a"));
6925 }
6926
6927 #[test]
6928 fn test_entities_without_incoming_all_returned_for_isolated_node() {
6929 let g = GraphStore::new();
6930 add(&g, "x");
6931 assert_eq!(g.entities_without_incoming().unwrap().len(), 1);
6932 }
6933
6934 #[test]
6937 fn test_path_to_string_uses_labels_when_entities_exist() {
6938 let g = GraphStore::new();
6939 let mut e1 = Entity::new("a", "Alice");
6940 let mut e2 = Entity::new("b", "Bob");
6941 let mut e3 = Entity::new("c", "Carol");
6942 e1.label = "Alice".into();
6943 e2.label = "Bob".into();
6944 e3.label = "Carol".into();
6945 g.add_entity(e1).unwrap();
6946 g.add_entity(e2).unwrap();
6947 g.add_entity(e3).unwrap();
6948 let path = vec![EntityId::new("a"), EntityId::new("b"), EntityId::new("c")];
6949 let s = g.path_to_string(&path).unwrap();
6950 assert_eq!(s, "Alice \u{2192} Bob \u{2192} Carol");
6951 }
6952
6953 #[test]
6954 fn test_path_to_string_falls_back_to_id_for_unknown_entities() {
6955 let g = GraphStore::new();
6956 let path = vec![EntityId::new("x"), EntityId::new("y")];
6957 let s = g.path_to_string(&path).unwrap();
6958 assert!(s.contains("x") && s.contains("y"));
6959 }
6960
6961 #[test]
6962 fn test_path_to_string_empty_path_returns_empty_string() {
6963 let g = GraphStore::new();
6964 assert_eq!(g.path_to_string(&[]).unwrap(), "");
6965 }
6966
6967 #[test]
6968 fn test_relationship_with_weight_changes_weight() {
6969 let rel = Relationship::new("a", "b", "KNOWS", 1.0).with_weight(0.25);
6970 assert_eq!(rel.weight, 0.25);
6971 assert_eq!(rel.from, EntityId::new("a"));
6972 assert_eq!(rel.kind, "KNOWS");
6973 }
6974
6975 #[test]
6976 fn test_relationship_with_weight_zero_allowed() {
6977 let rel = Relationship::new("x", "y", "EDGE", 5.0).with_weight(0.0);
6978 assert_eq!(rel.weight, 0.0);
6979 }
6980
6981 #[test]
6984 fn test_total_out_degree_sums_all_outgoing_edges() {
6985 let g = GraphStore::new();
6986 add(&g, "a");
6987 add(&g, "b");
6988 add(&g, "c");
6989 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
6990 g.add_relationship(Relationship::new("a", "c", "KNOWS", 1.0)).unwrap();
6991 g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
6992 assert_eq!(g.total_out_degree().unwrap(), 3);
6993 }
6994
6995 #[test]
6996 fn test_total_out_degree_zero_for_empty_graph() {
6997 let g = GraphStore::new();
6998 assert_eq!(g.total_out_degree().unwrap(), 0);
6999 }
7000
7001 #[test]
7002 fn test_relationships_with_weight_above_filters_correctly() {
7003 let g = GraphStore::new();
7004 add(&g, "a");
7005 add(&g, "b");
7006 add(&g, "c");
7007 g.add_relationship(Relationship::new("a", "b", "EDGE", 0.5)).unwrap();
7008 g.add_relationship(Relationship::new("a", "c", "EDGE", 1.5)).unwrap();
7009 let heavy = g.relationships_with_weight_above(1.0).unwrap();
7010 assert_eq!(heavy.len(), 1);
7011 assert_eq!(heavy[0].to, EntityId::new("c"));
7012 }
7013
7014 #[test]
7015 fn test_relationships_with_weight_above_returns_empty_when_none_qualify() {
7016 let g = GraphStore::new();
7017 add(&g, "a");
7018 add(&g, "b");
7019 g.add_relationship(Relationship::new("a", "b", "EDGE", 0.3)).unwrap();
7020 assert!(g.relationships_with_weight_above(1.0).unwrap().is_empty());
7021 }
7022
7023 #[test]
7026 fn test_relationships_of_kind_returns_matching_edges() {
7027 let g = GraphStore::new();
7028 add(&g, "a"); add(&g, "b"); add(&g, "c");
7029 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7030 g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
7031 g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
7032 let knows = g.relationships_of_kind("KNOWS").unwrap();
7033 assert_eq!(knows.len(), 2);
7034 assert!(knows.iter().all(|r| r.kind == "KNOWS"));
7035 }
7036
7037 #[test]
7038 fn test_relationships_of_kind_returns_empty_for_unknown_kind() {
7039 let g = GraphStore::new();
7040 add(&g, "a"); add(&g, "b");
7041 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7042 assert!(g.relationships_of_kind("MISSING").unwrap().is_empty());
7043 }
7044
7045 #[test]
7046 fn test_entities_without_label_excludes_matching_entities() {
7047 let g = GraphStore::new();
7048 let mut e1 = Entity::new("a", "Person");
7049 let mut e2 = Entity::new("b", "Company");
7050 let mut e3 = Entity::new("c", "Person");
7051 e1.label = "Person".into();
7052 e2.label = "Company".into();
7053 e3.label = "Person".into();
7054 g.add_entity(e1).unwrap();
7055 g.add_entity(e2).unwrap();
7056 g.add_entity(e3).unwrap();
7057 let non_persons = g.entities_without_label("Person").unwrap();
7058 assert_eq!(non_persons.len(), 1);
7059 assert_eq!(non_persons[0].id, EntityId::new("b"));
7060 }
7061
7062 #[test]
7063 fn test_entities_without_label_returns_all_when_no_match() {
7064 let g = GraphStore::new();
7065 add(&g, "x");
7066 assert_eq!(g.entities_without_label("NonExistent").unwrap().len(), 1);
7067 }
7068
7069 #[test]
7072 fn test_relationships_of_kind_returns_matching_relationships() {
7073 let g = GraphStore::new();
7074 add(&g, "a");
7075 add(&g, "b");
7076 add(&g, "c");
7077 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7078 g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
7079 let knows = g.relationships_of_kind("KNOWS").unwrap();
7080 assert_eq!(knows.len(), 1);
7081 assert_eq!(knows[0].to, EntityId::new("b"));
7082 }
7083
7084 #[test]
7085 fn test_relationships_of_kind_empty_when_none_match() {
7086 let g = GraphStore::new();
7087 add(&g, "a");
7088 add(&g, "b");
7089 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7090 assert!(g.relationships_of_kind("HATES").unwrap().is_empty());
7091 }
7092
7093 #[test]
7094 fn test_entity_count_with_label_counts_matching_entities() {
7095 let g = GraphStore::new();
7096 g.add_entity(Entity::new("a", "Person")).unwrap();
7097 g.add_entity(Entity::new("b", "Person")).unwrap();
7098 g.add_entity(Entity::new("c", "Organization")).unwrap();
7099 assert_eq!(g.entity_count_with_label("Person").unwrap(), 2);
7100 assert_eq!(g.entity_count_with_label("Organization").unwrap(), 1);
7101 }
7102
7103 #[test]
7104 fn test_entity_count_with_label_zero_for_no_match() {
7105 let g = GraphStore::new();
7106 add(&g, "a");
7107 assert_eq!(g.entity_count_with_label("Alien").unwrap(), 0);
7108 }
7109
7110 #[test]
7113 fn test_edge_count_above_weight_counts_heavy_edges() {
7114 let g = GraphStore::new();
7115 add(&g, "a"); add(&g, "b"); add(&g, "c");
7116 g.add_relationship(Relationship::new("a", "b", "K", 0.9)).unwrap();
7117 g.add_relationship(Relationship::new("b", "c", "K", 0.3)).unwrap();
7118 g.add_relationship(Relationship::new("a", "c", "K", 1.5)).unwrap();
7119 assert_eq!(g.edge_count_above_weight(0.8).unwrap(), 2); }
7121
7122 #[test]
7123 fn test_edge_count_above_weight_zero_for_empty_graph() {
7124 let g = GraphStore::new();
7125 assert_eq!(g.edge_count_above_weight(0.0).unwrap(), 0);
7126 }
7127
7128 #[test]
7129 fn test_entities_with_label_prefix_returns_matching_entities() {
7130 let g = GraphStore::new();
7131 g.add_entity(Entity::new("a", "Person")).unwrap();
7132 g.add_entity(Entity::new("b", "Pet")).unwrap();
7133 g.add_entity(Entity::new("c", "Organization")).unwrap();
7134 let result = g.entities_with_label_prefix("Pe").unwrap();
7135 assert_eq!(result.len(), 2);
7136 assert!(result.iter().all(|e| e.label.starts_with("Pe")));
7137 }
7138
7139 #[test]
7140 fn test_entities_with_label_prefix_empty_when_no_match() {
7141 let g = GraphStore::new();
7142 add(&g, "a");
7143 assert!(g.entities_with_label_prefix("XYZ").unwrap().is_empty());
7144 }
7145
7146 #[test]
7147 fn test_bidirectional_pairs_returns_pairs_with_edges_in_both_directions() {
7148 let g = GraphStore::new();
7149 add(&g, "a"); add(&g, "b"); add(&g, "c");
7150 g.add_relationship(Relationship::new("a", "b", "K", 1.0)).unwrap();
7151 g.add_relationship(Relationship::new("b", "a", "K", 1.0)).unwrap(); g.add_relationship(Relationship::new("a", "c", "K", 1.0)).unwrap(); let pairs = g.bidirectional_pairs().unwrap();
7154 assert_eq!(pairs.len(), 1);
7155 }
7156
7157 #[test]
7158 fn test_bidirectional_pairs_empty_for_one_directional_graph() {
7159 let g = GraphStore::new();
7160 add(&g, "a"); add(&g, "b");
7161 g.add_relationship(Relationship::new("a", "b", "K", 1.0)).unwrap();
7162 assert!(g.bidirectional_pairs().unwrap().is_empty());
7163 }
7164
7165 #[test]
7168 fn test_entities_with_min_out_degree_filters_correctly() {
7169 let g = GraphStore::new();
7170 add(&g, "a"); add(&g, "b"); add(&g, "c");
7171 g.add_relationship(Relationship::new("a", "b", "R", 1.0)).unwrap();
7172 g.add_relationship(Relationship::new("a", "c", "R", 1.0)).unwrap();
7173 let result = g.entities_with_min_out_degree(2).unwrap();
7175 assert_eq!(result.len(), 1);
7176 assert_eq!(result[0].id.as_str(), "a");
7177 }
7178
7179 #[test]
7180 fn test_entities_with_min_out_degree_zero_includes_all() {
7181 let g = GraphStore::new();
7182 add(&g, "x"); add(&g, "y");
7183 assert_eq!(g.entities_with_min_out_degree(0).unwrap().len(), 2);
7184 }
7185
7186 #[test]
7187 fn test_entities_with_min_out_degree_empty_for_empty_graph() {
7188 let g = GraphStore::new();
7189 assert!(g.entities_with_min_out_degree(1).unwrap().is_empty());
7190 }
7191
7192 #[test]
7195 fn test_mean_in_degree_computes_average() {
7196 let g = GraphStore::new();
7197 add(&g, "a"); add(&g, "b"); add(&g, "c");
7198 g.add_relationship(Relationship::new("a", "b", "EDGE", 1.0)).unwrap();
7199 g.add_relationship(Relationship::new("a", "c", "EDGE", 1.0)).unwrap();
7200 let mean = g.mean_in_degree().unwrap();
7202 assert!((mean - 2.0 / 3.0).abs() < 1e-9);
7203 }
7204
7205 #[test]
7206 fn test_mean_in_degree_zero_for_empty_graph() {
7207 let g = GraphStore::new();
7208 assert_eq!(g.mean_in_degree().unwrap(), 0.0);
7209 }
7210
7211 #[test]
7212 fn test_entity_count_by_label_prefix_counts_matching_entities() {
7213 let g = GraphStore::new();
7214 g.add_entity(Entity::new("a", "Person-Alice")).unwrap();
7215 g.add_entity(Entity::new("b", "Person-Bob")).unwrap();
7216 g.add_entity(Entity::new("c", "Organization")).unwrap();
7217 assert_eq!(g.entity_count_by_label_prefix("Person").unwrap(), 2);
7218 assert_eq!(g.entity_count_by_label_prefix("Org").unwrap(), 1);
7219 }
7220
7221 #[test]
7222 fn test_entity_count_by_label_prefix_zero_for_no_match() {
7223 let g = GraphStore::new();
7224 add(&g, "x");
7225 assert_eq!(g.entity_count_by_label_prefix("Alien").unwrap(), 0);
7226 }
7227
7228 #[test]
7231 fn test_relationship_weight_sum_sums_all_weights() {
7232 let g = GraphStore::new();
7233 add(&g, "a"); add(&g, "b"); add(&g, "c");
7234 g.add_relationship(Relationship::new("a", "b", "E", 2.0)).unwrap();
7235 g.add_relationship(Relationship::new("a", "c", "E", 3.0)).unwrap();
7236 assert!((g.relationship_weight_sum().unwrap() - 5.0).abs() < 1e-6);
7237 }
7238
7239 #[test]
7240 fn test_relationship_weight_sum_zero_for_empty_graph() {
7241 let g = GraphStore::new();
7242 assert_eq!(g.relationship_weight_sum().unwrap(), 0.0);
7243 }
7244
7245 #[test]
7246 fn test_label_frequency_counts_labels() {
7247 let g = GraphStore::new();
7248 g.add_entity(Entity::new("a", "Person")).unwrap();
7249 g.add_entity(Entity::new("b", "Person")).unwrap();
7250 g.add_entity(Entity::new("c", "Node")).unwrap();
7251 let freq = g.label_frequency().unwrap();
7252 assert_eq!(*freq.get("Person").unwrap(), 2);
7253 assert_eq!(*freq.get("Node").unwrap(), 1);
7254 }
7255
7256 #[test]
7259 fn test_entities_sorted_by_id_returns_alphabetical_order() {
7260 let g = GraphStore::new();
7261 g.add_entity(Entity::new("charlie", "Node")).unwrap();
7262 g.add_entity(Entity::new("alice", "Node")).unwrap();
7263 g.add_entity(Entity::new("bob", "Node")).unwrap();
7264 let sorted = g.entities_sorted_by_id().unwrap();
7265 let ids: Vec<&str> = sorted.iter().map(|e| e.id.as_str()).collect();
7266 assert_eq!(ids, vec!["alice", "bob", "charlie"]);
7267 }
7268
7269 #[test]
7270 fn test_entities_sorted_by_id_empty_for_empty_graph() {
7271 let g = GraphStore::new();
7272 assert!(g.entities_sorted_by_id().unwrap().is_empty());
7273 }
7274
7275 #[test]
7276 fn test_label_frequency_empty_for_empty_graph() {
7277 let g = GraphStore::new();
7278 assert!(g.label_frequency().unwrap().is_empty());
7279 }
7280
7281 #[test]
7284 fn test_entities_with_label_containing_returns_matching_entities() {
7285 let g = GraphStore::new();
7286 g.add_entity(Entity::new("a", "PersonA")).unwrap();
7287 g.add_entity(Entity::new("b", "PersonB")).unwrap();
7288 g.add_entity(Entity::new("c", "Location")).unwrap();
7289 let mut result = g.entities_with_label_containing("Person").unwrap();
7290 result.sort_unstable_by(|a, b| a.id.cmp(&b.id));
7291 assert_eq!(result.len(), 2);
7292 assert_eq!(result[0].id.as_str(), "a");
7293 assert_eq!(result[1].id.as_str(), "b");
7294 }
7295
7296 #[test]
7297 fn test_entities_with_label_containing_empty_when_no_match() {
7298 let g = GraphStore::new();
7299 g.add_entity(Entity::new("a", "Node")).unwrap();
7300 assert!(g.entities_with_label_containing("Person").unwrap().is_empty());
7301 }
7302
7303 #[test]
7306 fn test_entities_with_min_in_degree_returns_correct_entities() {
7307 let g = GraphStore::new();
7308 g.add_entity(Entity::new("a", "Node")).unwrap();
7309 g.add_entity(Entity::new("b", "Node")).unwrap();
7310 g.add_entity(Entity::new("c", "Node")).unwrap();
7311 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
7313 g.add_relationship(Relationship::new("c", "b", "link", 1.0)).unwrap();
7314 g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
7316 let result = g.entities_with_min_in_degree(2).unwrap();
7318 assert_eq!(result.len(), 1);
7319 assert_eq!(result[0].id.as_str(), "b");
7320 }
7321
7322 #[test]
7323 fn test_entities_with_min_in_degree_zero_includes_all() {
7324 let g = GraphStore::new();
7325 g.add_entity(Entity::new("a", "Node")).unwrap();
7326 g.add_entity(Entity::new("b", "Node")).unwrap();
7327 assert_eq!(g.entities_with_min_in_degree(0).unwrap().len(), 2);
7328 }
7329
7330 #[test]
7331 fn test_entities_with_min_in_degree_empty_for_empty_graph() {
7332 let g = GraphStore::new();
7333 assert!(g.entities_with_min_in_degree(1).unwrap().is_empty());
7334 }
7335
7336 #[test]
7339 fn test_total_property_count_sums_across_entities() {
7340 let g = GraphStore::new();
7341 g.add_entity(
7342 Entity::new("a", "Node")
7343 .with_property("x", serde_json::json!(1))
7344 .with_property("y", serde_json::json!(2)),
7345 )
7346 .unwrap();
7347 g.add_entity(Entity::new("b", "Node").with_property("z", serde_json::json!(3))).unwrap();
7348 assert_eq!(g.total_property_count().unwrap(), 3);
7349 }
7350
7351 #[test]
7352 fn test_total_property_count_zero_for_empty_graph() {
7353 let g = GraphStore::new();
7354 assert_eq!(g.total_property_count().unwrap(), 0);
7355 }
7356
7357 #[test]
7358 fn test_entities_with_no_properties_returns_bare_entities() {
7359 let g = GraphStore::new();
7360 g.add_entity(Entity::new("bare", "Node")).unwrap();
7361 g.add_entity(
7362 Entity::new("annotated", "Node").with_property("k", serde_json::json!("v")),
7363 )
7364 .unwrap();
7365 let result = g.entities_with_no_properties().unwrap();
7366 assert_eq!(result.len(), 1);
7367 assert_eq!(result[0].id.as_str(), "bare");
7368 }
7369
7370 #[test]
7371 fn test_entities_with_no_properties_empty_when_all_have_properties() {
7372 let g = GraphStore::new();
7373 g.add_entity(Entity::new("a", "N").with_property("k", serde_json::json!(1))).unwrap();
7374 assert!(g.entities_with_no_properties().unwrap().is_empty());
7375 }
7376
7377 #[test]
7380 fn test_entity_neighbor_count_returns_out_degree() {
7381 let g = GraphStore::new();
7382 g.add_entity(Entity::new("a", "N")).unwrap();
7383 g.add_entity(Entity::new("b", "N")).unwrap();
7384 g.add_entity(Entity::new("c", "N")).unwrap();
7385 g.add_relationship(Relationship::new("a", "b", "E", 1.0)).unwrap();
7386 g.add_relationship(Relationship::new("a", "c", "E", 1.0)).unwrap();
7387 let a_id = EntityId("a".to_string());
7388 assert_eq!(g.entity_neighbor_count(&a_id).unwrap(), 2);
7389 }
7390
7391 #[test]
7392 fn test_entity_neighbor_count_zero_for_isolated_entity() {
7393 let g = GraphStore::new();
7394 g.add_entity(Entity::new("lone", "N")).unwrap();
7395 let id = EntityId("lone".to_string());
7396 assert_eq!(g.entity_neighbor_count(&id).unwrap(), 0);
7397 }
7398
7399 #[test]
7402 fn test_entity_by_label_returns_matching_entity() {
7403 let g = GraphStore::new();
7404 g.add_entity(Entity::new("a", "Person")).unwrap();
7405 let result = g.entity_by_label("Person").unwrap();
7406 assert!(result.is_some());
7407 assert_eq!(result.unwrap().id.as_str(), "a");
7408 }
7409
7410 #[test]
7411 fn test_entity_by_label_returns_none_when_not_found() {
7412 let g = GraphStore::new();
7413 g.add_entity(Entity::new("a", "Node")).unwrap();
7414 assert!(g.entity_by_label("Missing").unwrap().is_none());
7415 }
7416
7417 #[test]
7418 fn test_distinct_relationship_kind_count_counts_unique_kinds() {
7419 let g = GraphStore::new();
7420 g.add_entity(Entity::new("a", "N")).unwrap();
7421 g.add_entity(Entity::new("b", "N")).unwrap();
7422 g.add_entity(Entity::new("c", "N")).unwrap();
7423 g.add_relationship(Relationship::new("a", "b", "FRIEND", 1.0)).unwrap();
7424 g.add_relationship(Relationship::new("a", "c", "ENEMY", 1.0)).unwrap();
7425 assert_eq!(g.distinct_relationship_kind_count().unwrap(), 2);
7426 }
7427
7428 #[test]
7429 fn test_distinct_relationship_kind_count_zero_for_empty_graph() {
7430 let g = GraphStore::new();
7431 assert_eq!(g.distinct_relationship_kind_count().unwrap(), 0);
7432 }
7433
7434 #[test]
7437 fn test_weight_above_threshold_ratio_returns_correct_fraction() {
7438 let g = GraphStore::new();
7439 g.add_entity(Entity::new("a", "N")).unwrap();
7440 g.add_entity(Entity::new("b", "N")).unwrap();
7441 g.add_entity(Entity::new("c", "N")).unwrap();
7442 g.add_relationship(Relationship::new("a", "b", "E", 0.8)).unwrap();
7443 g.add_relationship(Relationship::new("a", "c", "E", 0.3)).unwrap();
7444 let ratio = g.weight_above_threshold_ratio(0.5).unwrap();
7446 assert!((ratio - 0.5).abs() < 1e-9);
7447 }
7448
7449 #[test]
7450 fn test_weight_above_threshold_ratio_zero_for_empty_graph() {
7451 let g = GraphStore::new();
7452 assert_eq!(g.weight_above_threshold_ratio(0.5).unwrap(), 0.0);
7453 }
7454
7455 #[test]
7456 fn test_entities_sorted_by_out_degree_most_connected_first() {
7457 let g = GraphStore::new();
7458 g.add_entity(Entity::new("hub", "N")).unwrap();
7459 g.add_entity(Entity::new("leaf", "N")).unwrap();
7460 g.add_entity(Entity::new("mid", "N")).unwrap();
7461 g.add_relationship(Relationship::new("hub", "leaf", "E", 1.0)).unwrap();
7462 g.add_relationship(Relationship::new("hub", "mid", "E", 1.0)).unwrap();
7463 g.add_relationship(Relationship::new("mid", "leaf", "E", 1.0)).unwrap();
7464 let sorted = g.entities_sorted_by_out_degree().unwrap();
7465 assert_eq!(sorted[0].id.as_str(), "hub"); assert_eq!(sorted[1].id.as_str(), "mid"); }
7468
7469 #[test]
7470 fn test_entities_sorted_by_out_degree_empty_for_empty_graph() {
7471 let g = GraphStore::new();
7472 assert!(g.entities_sorted_by_out_degree().unwrap().is_empty());
7473 }
7474
7475 #[test]
7478 fn test_entity_labels_sorted_returns_unique_labels_in_order() {
7479 let g = GraphStore::new();
7480 g.add_entity(Entity::new("a", "Zebra")).unwrap();
7481 g.add_entity(Entity::new("b", "Apple")).unwrap();
7482 g.add_entity(Entity::new("c", "Zebra")).unwrap(); let labels = g.entity_labels_sorted().unwrap();
7484 assert_eq!(labels, vec!["Apple", "Zebra"]);
7485 }
7486
7487 #[test]
7488 fn test_entity_labels_sorted_empty_for_empty_graph() {
7489 let g = GraphStore::new();
7490 assert!(g.entity_labels_sorted().unwrap().is_empty());
7491 }
7492
7493 #[test]
7494 fn test_relationship_type_count_counts_distinct_kinds() {
7495 let g = GraphStore::new();
7496 g.add_entity(Entity::new("a", "N")).unwrap();
7497 g.add_entity(Entity::new("b", "N")).unwrap();
7498 g.add_entity(Entity::new("c", "N")).unwrap();
7499 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7500 g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
7501 assert_eq!(g.relationship_type_count().unwrap(), 2);
7502 }
7503
7504 #[test]
7505 fn test_relationship_type_count_zero_for_empty_graph() {
7506 let g = GraphStore::new();
7507 assert_eq!(g.relationship_type_count().unwrap(), 0);
7508 }
7509
7510 #[test]
7513 fn test_has_entity_with_label_true_when_present() {
7514 let g = GraphStore::new();
7515 g.add_entity(Entity::new("a", "Person")).unwrap();
7516 assert!(g.has_entity_with_label("Person").unwrap());
7517 }
7518
7519 #[test]
7520 fn test_has_entity_with_label_false_when_absent() {
7521 let g = GraphStore::new();
7522 g.add_entity(Entity::new("a", "Person")).unwrap();
7523 assert!(!g.has_entity_with_label("Robot").unwrap());
7524 }
7525
7526 #[test]
7527 fn test_has_entity_with_label_false_for_empty_graph() {
7528 let g = GraphStore::new();
7529 assert!(!g.has_entity_with_label("Anything").unwrap());
7530 }
7531
7532 #[test]
7533 fn test_min_weight_returns_smallest_weight() {
7534 let g = GraphStore::new();
7535 g.add_entity(Entity::new("a", "N")).unwrap();
7536 g.add_entity(Entity::new("b", "N")).unwrap();
7537 g.add_entity(Entity::new("c", "N")).unwrap();
7538 g.add_relationship(Relationship::new("a", "b", "k", 0.5)).unwrap();
7539 g.add_relationship(Relationship::new("b", "c", "k", 2.0)).unwrap();
7540 assert_eq!(g.min_weight().unwrap(), Some(0.5));
7541 }
7542
7543 #[test]
7544 fn test_min_weight_none_for_graph_with_no_relationships() {
7545 let g = GraphStore::new();
7546 g.add_entity(Entity::new("a", "N")).unwrap();
7547 assert_eq!(g.min_weight().unwrap(), None);
7548 }
7549
7550 #[test]
7553 fn test_entities_with_exact_label_returns_matching_entities() {
7554 let g = GraphStore::new();
7555 g.add_entity(Entity::new("a", "Person")).unwrap();
7556 g.add_entity(Entity::new("b", "Person")).unwrap();
7557 g.add_entity(Entity::new("c", "Robot")).unwrap();
7558 let people = g.entities_with_exact_label("Person").unwrap();
7559 assert_eq!(people.len(), 2);
7560 assert!(people.iter().all(|e| e.label == "Person"));
7561 }
7562
7563 #[test]
7564 fn test_entities_with_exact_label_empty_when_no_match() {
7565 let g = GraphStore::new();
7566 g.add_entity(Entity::new("a", "Person")).unwrap();
7567 assert!(g.entities_with_exact_label("Robot").unwrap().is_empty());
7568 }
7569
7570 #[test]
7571 fn test_entities_with_exact_label_empty_for_empty_graph() {
7572 let g = GraphStore::new();
7573 assert!(g.entities_with_exact_label("Person").unwrap().is_empty());
7574 }
7575
7576 #[test]
7579 fn test_average_weight_returns_mean_of_all_edges() {
7580 let g = GraphStore::new();
7581 g.add_entity(Entity::new("a", "N")).unwrap();
7582 g.add_entity(Entity::new("b", "N")).unwrap();
7583 g.add_entity(Entity::new("c", "N")).unwrap();
7584 g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7585 g.add_relationship(Relationship::new("b", "c", "k", 3.0)).unwrap();
7586 assert_eq!(g.average_weight().unwrap(), 2.0);
7587 }
7588
7589 #[test]
7590 fn test_average_weight_zero_for_graph_with_no_edges() {
7591 let g = GraphStore::new();
7592 g.add_entity(Entity::new("a", "N")).unwrap();
7593 assert_eq!(g.average_weight().unwrap(), 0.0);
7594 }
7595
7596 #[test]
7597 fn test_max_weight_returns_largest_weight() {
7598 let g = GraphStore::new();
7599 g.add_entity(Entity::new("a", "N")).unwrap();
7600 g.add_entity(Entity::new("b", "N")).unwrap();
7601 g.add_entity(Entity::new("c", "N")).unwrap();
7602 g.add_relationship(Relationship::new("a", "b", "k", 0.5)).unwrap();
7603 g.add_relationship(Relationship::new("b", "c", "k", 10.0)).unwrap();
7604 assert_eq!(g.max_weight().unwrap(), Some(10.0));
7605 }
7606
7607 #[test]
7608 fn test_max_weight_none_for_empty_graph() {
7609 let g = GraphStore::new();
7610 assert_eq!(g.max_weight().unwrap(), None);
7611 }
7612
7613 #[test]
7616 fn test_relationships_of_kind_count_returns_correct_count() {
7617 let g = GraphStore::new();
7618 g.add_entity(Entity::new("a", "N")).unwrap();
7619 g.add_entity(Entity::new("b", "N")).unwrap();
7620 g.add_entity(Entity::new("c", "N")).unwrap();
7621 g.add_relationship(Relationship::new("a", "b", "FOLLOWS", 1.0)).unwrap();
7622 g.add_relationship(Relationship::new("b", "c", "FOLLOWS", 1.0)).unwrap();
7623 g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
7624 assert_eq!(g.relationships_of_kind_count("FOLLOWS").unwrap(), 2);
7625 assert_eq!(g.relationships_of_kind_count("LIKES").unwrap(), 1);
7626 }
7627
7628 #[test]
7629 fn test_relationships_of_kind_count_zero_for_absent_kind() {
7630 let g = GraphStore::new();
7631 g.add_entity(Entity::new("a", "N")).unwrap();
7632 g.add_entity(Entity::new("b", "N")).unwrap();
7633 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7634 assert_eq!(g.relationships_of_kind_count("MISSING").unwrap(), 0);
7635 }
7636
7637 #[test]
7638 fn test_entities_with_incoming_returns_targets() {
7639 let g = GraphStore::new();
7640 g.add_entity(Entity::new("src", "N")).unwrap();
7641 g.add_entity(Entity::new("dst", "N")).unwrap();
7642 g.add_entity(Entity::new("iso", "N")).unwrap();
7643 g.add_relationship(Relationship::new("src", "dst", "E", 1.0)).unwrap();
7644 let with_in: Vec<_> = g.entities_with_incoming().unwrap();
7645 let ids: Vec<&str> = with_in.iter().map(|e| e.id.as_str()).collect();
7646 assert!(ids.contains(&"dst"));
7647 assert!(!ids.contains(&"src"));
7648 assert!(!ids.contains(&"iso"));
7649 }
7650
7651 #[test]
7652 fn test_entities_with_incoming_empty_for_no_relationships() {
7653 let g = GraphStore::new();
7654 g.add_entity(Entity::new("a", "N")).unwrap();
7655 assert!(g.entities_with_incoming().unwrap().is_empty());
7656 }
7657
7658 #[test]
7661 fn test_entities_with_no_relationships_returns_sink_nodes() {
7662 let g = GraphStore::new();
7663 g.add_entity(Entity::new("src", "N")).unwrap();
7664 g.add_entity(Entity::new("sink", "N")).unwrap();
7665 g.add_relationship(Relationship::new("src", "sink", "E", 1.0)).unwrap();
7666 let sinks = g.entities_with_no_relationships().unwrap();
7667 let ids: Vec<&str> = sinks.iter().map(|e| e.id.as_str()).collect();
7668 assert!(ids.contains(&"sink"));
7669 assert!(!ids.contains(&"src"));
7670 }
7671
7672 #[test]
7673 fn test_entities_with_no_relationships_all_when_no_edges() {
7674 let g = GraphStore::new();
7675 g.add_entity(Entity::new("a", "N")).unwrap();
7676 g.add_entity(Entity::new("b", "N")).unwrap();
7677 assert_eq!(g.entities_with_no_relationships().unwrap().len(), 2);
7678 }
7679
7680 #[test]
7681 fn test_entities_with_no_relationships_empty_for_empty_graph() {
7682 let g = GraphStore::new();
7683 assert!(g.entities_with_no_relationships().unwrap().is_empty());
7684 }
7685
7686 #[test]
7689 fn test_entity_ids_sorted_returns_alphabetical_ids() {
7690 let g = GraphStore::new();
7691 g.add_entity(Entity::new("zebra", "N")).unwrap();
7692 g.add_entity(Entity::new("alpha", "N")).unwrap();
7693 g.add_entity(Entity::new("mango", "N")).unwrap();
7694 let ids = g.entity_ids_sorted().unwrap();
7695 assert_eq!(ids[0].0, "alpha");
7696 assert_eq!(ids[1].0, "mango");
7697 assert_eq!(ids[2].0, "zebra");
7698 }
7699
7700 #[test]
7701 fn test_entity_ids_sorted_empty_for_empty_graph() {
7702 let g = GraphStore::new();
7703 assert!(g.entity_ids_sorted().unwrap().is_empty());
7704 }
7705
7706 #[test]
7707 fn test_relationship_count_for_returns_out_degree() {
7708 let g = GraphStore::new();
7709 g.add_entity(Entity::new("a", "N")).unwrap();
7710 g.add_entity(Entity::new("b", "N")).unwrap();
7711 g.add_entity(Entity::new("c", "N")).unwrap();
7712 g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7713 g.add_relationship(Relationship::new("a", "c", "k", 1.0)).unwrap();
7714 let a_id = EntityId("a".to_string());
7715 assert_eq!(g.relationship_count_for(&a_id).unwrap(), 2);
7716 }
7717
7718 #[test]
7719 fn test_relationship_count_for_zero_for_unknown_entity() {
7720 let g = GraphStore::new();
7721 let id = EntityId("unknown".to_string());
7722 assert_eq!(g.relationship_count_for(&id).unwrap(), 0);
7723 }
7724
7725 #[test]
7728 fn test_entity_pair_has_relationship_true_when_edge_exists() {
7729 let g = GraphStore::new();
7730 g.add_entity(Entity::new("a", "N")).unwrap();
7731 g.add_entity(Entity::new("b", "N")).unwrap();
7732 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7733 let a = EntityId("a".to_string());
7734 let b = EntityId("b".to_string());
7735 assert!(g.entity_pair_has_relationship(&a, &b).unwrap());
7736 }
7737
7738 #[test]
7739 fn test_entity_pair_has_relationship_false_when_no_edge() {
7740 let g = GraphStore::new();
7741 g.add_entity(Entity::new("a", "N")).unwrap();
7742 g.add_entity(Entity::new("b", "N")).unwrap();
7743 let a = EntityId("a".to_string());
7744 let b = EntityId("b".to_string());
7745 assert!(!g.entity_pair_has_relationship(&a, &b).unwrap());
7746 }
7747
7748 #[test]
7749 fn test_nodes_reachable_from_returns_all_reachable_nodes() {
7750 let g = GraphStore::new();
7751 g.add_entity(Entity::new("a", "N")).unwrap();
7752 g.add_entity(Entity::new("b", "N")).unwrap();
7753 g.add_entity(Entity::new("c", "N")).unwrap();
7754 g.add_entity(Entity::new("d", "N")).unwrap();
7755 g.add_relationship(Relationship::new("a", "b", "E", 1.0)).unwrap();
7756 g.add_relationship(Relationship::new("b", "c", "E", 1.0)).unwrap();
7757 let start = EntityId("a".to_string());
7758 let reachable = g.nodes_reachable_from(&start).unwrap();
7759 let ids: Vec<&str> = reachable.iter().map(|id| id.0.as_str()).collect();
7760 assert!(ids.contains(&"b"));
7761 assert!(ids.contains(&"c"));
7762 assert!(!ids.contains(&"a"));
7763 assert!(!ids.contains(&"d"));
7764 }
7765
7766 #[test]
7767 fn test_nodes_reachable_from_empty_for_isolated_node() {
7768 let g = GraphStore::new();
7769 g.add_entity(Entity::new("iso", "N")).unwrap();
7770 let id = EntityId("iso".to_string());
7771 assert!(g.nodes_reachable_from(&id).unwrap().is_empty());
7772 }
7773
7774 #[test]
7777 fn test_self_loops_returns_loop_relationships() {
7778 let g = GraphStore::new();
7779 g.add_entity(Entity::new("a", "N")).unwrap();
7780 g.add_entity(Entity::new("b", "N")).unwrap();
7781 g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
7782 g.add_relationship(Relationship::new("a", "b", "other", 1.0)).unwrap();
7783 let loops = g.self_loops().unwrap();
7784 assert_eq!(loops.len(), 1);
7785 assert_eq!(loops[0].from.as_str(), "a");
7786 assert_eq!(loops[0].to.as_str(), "a");
7787 }
7788
7789 #[test]
7790 fn test_self_loops_empty_when_no_loops() {
7791 let g = GraphStore::new();
7792 g.add_entity(Entity::new("a", "N")).unwrap();
7793 g.add_entity(Entity::new("b", "N")).unwrap();
7794 g.add_relationship(Relationship::new("a", "b", "E", 1.0)).unwrap();
7795 assert!(g.self_loops().unwrap().is_empty());
7796 }
7797
7798 #[test]
7799 fn test_entities_with_label_and_property_returns_matching() {
7800 let g = GraphStore::new();
7801 let mut e = Entity::new("a", "Person");
7802 e.properties.insert("age".to_string(), serde_json::json!("30"));
7803 g.add_entity(e).unwrap();
7804 g.add_entity(Entity::new("b", "Person")).unwrap();
7805 g.add_entity(Entity::new("c", "Robot")).unwrap();
7806 let result = g.entities_with_label_and_property("Person", "age").unwrap();
7807 assert_eq!(result.len(), 1);
7808 assert_eq!(result[0].id.as_str(), "a");
7809 }
7810
7811 #[test]
7812 fn test_entities_with_label_and_property_empty_when_no_match() {
7813 let g = GraphStore::new();
7814 g.add_entity(Entity::new("a", "Person")).unwrap();
7815 assert!(g.entities_with_label_and_property("Person", "age").unwrap().is_empty());
7816 }
7817
7818 #[test]
7821 fn test_total_edge_weight_sums_all_weights() {
7822 let g = GraphStore::new();
7823 g.add_entity(Entity::new("a", "N")).unwrap();
7824 g.add_entity(Entity::new("b", "N")).unwrap();
7825 g.add_entity(Entity::new("c", "N")).unwrap();
7826 g.add_relationship(Relationship::new("a", "b", "E", 2.0)).unwrap();
7827 g.add_relationship(Relationship::new("b", "c", "E", 3.0)).unwrap();
7828 let total = g.total_edge_weight().unwrap();
7829 assert!((total - 5.0).abs() < 1e-9);
7830 }
7831
7832 #[test]
7833 fn test_total_edge_weight_zero_for_no_relationships() {
7834 let g = GraphStore::new();
7835 g.add_entity(Entity::new("a", "N")).unwrap();
7836 assert!((g.total_edge_weight().unwrap()).abs() < 1e-9);
7837 }
7838
7839 #[test]
7840 fn test_entity_with_max_out_degree_returns_highest_degree_entity() {
7841 let g = GraphStore::new();
7842 g.add_entity(Entity::new("hub", "N")).unwrap();
7843 g.add_entity(Entity::new("spoke1", "N")).unwrap();
7844 g.add_entity(Entity::new("spoke2", "N")).unwrap();
7845 g.add_entity(Entity::new("leaf", "N")).unwrap();
7846 g.add_relationship(Relationship::new("hub", "spoke1", "E", 1.0)).unwrap();
7847 g.add_relationship(Relationship::new("hub", "spoke2", "E", 1.0)).unwrap();
7848 g.add_relationship(Relationship::new("spoke1", "leaf", "E", 1.0)).unwrap();
7849 let top = g.entity_with_max_out_degree().unwrap().unwrap();
7850 assert_eq!(top.id.as_str(), "hub");
7851 }
7852
7853 #[test]
7854 fn test_entity_with_max_out_degree_none_for_empty_graph() {
7855 let g = GraphStore::new();
7856 assert!(g.entity_with_max_out_degree().unwrap().is_none());
7857 }
7858
7859 #[test]
7862 fn test_edge_weight_between_returns_weight_when_edge_exists() {
7863 let g = GraphStore::new();
7864 g.add_entity(Entity::new("a", "N")).unwrap();
7865 g.add_entity(Entity::new("b", "N")).unwrap();
7866 g.add_relationship(Relationship::new("a", "b", "k", 2.5)).unwrap();
7867 let a = EntityId("a".to_string());
7868 let b = EntityId("b".to_string());
7869 assert_eq!(g.edge_weight_between(&a, &b).unwrap(), Some(2.5));
7870 }
7871
7872 #[test]
7873 fn test_edge_weight_between_none_when_no_edge() {
7874 let g = GraphStore::new();
7875 g.add_entity(Entity::new("a", "N")).unwrap();
7876 g.add_entity(Entity::new("b", "N")).unwrap();
7877 let a = EntityId("a".to_string());
7878 let b = EntityId("b".to_string());
7879 assert_eq!(g.edge_weight_between(&a, &b).unwrap(), None);
7880 }
7881
7882 #[test]
7883 fn test_total_relationship_weight_sums_all_weights() {
7884 let g = GraphStore::new();
7885 g.add_entity(Entity::new("a", "N")).unwrap();
7886 g.add_entity(Entity::new("b", "N")).unwrap();
7887 g.add_entity(Entity::new("c", "N")).unwrap();
7888 g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7889 g.add_relationship(Relationship::new("b", "c", "k", 3.0)).unwrap();
7890 assert_eq!(g.total_relationship_weight().unwrap(), 4.0);
7891 }
7892
7893 #[test]
7894 fn test_total_relationship_weight_zero_for_no_edges() {
7895 let g = GraphStore::new();
7896 assert_eq!(g.total_relationship_weight().unwrap(), 0.0);
7897 }
7898
7899 #[test]
7902 fn test_avg_weight_for_kind_returns_mean() {
7903 let g = GraphStore::new();
7904 g.add_entity(Entity::new("a", "N")).unwrap();
7905 g.add_entity(Entity::new("b", "N")).unwrap();
7906 g.add_entity(Entity::new("c", "N")).unwrap();
7907 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7908 g.add_relationship(Relationship::new("b", "c", "KNOWS", 3.0)).unwrap();
7909 g.add_relationship(Relationship::new("a", "c", "LIKES", 5.0)).unwrap();
7910 assert_eq!(g.avg_weight_for_kind("KNOWS").unwrap(), 2.0);
7911 }
7912
7913 #[test]
7914 fn test_avg_weight_for_kind_zero_for_absent_kind() {
7915 let g = GraphStore::new();
7916 g.add_entity(Entity::new("a", "N")).unwrap();
7917 g.add_entity(Entity::new("b", "N")).unwrap();
7918 g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
7919 assert_eq!(g.avg_weight_for_kind("MISSING").unwrap(), 0.0);
7920 }
7921
7922 #[test]
7923 fn test_entity_degree_ratio_returns_fraction_of_total() {
7924 let g = GraphStore::new();
7925 g.add_entity(Entity::new("a", "N")).unwrap();
7926 g.add_entity(Entity::new("b", "N")).unwrap();
7927 g.add_entity(Entity::new("c", "N")).unwrap();
7928 g.add_entity(Entity::new("d", "N")).unwrap();
7929 g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7930 g.add_relationship(Relationship::new("a", "c", "k", 1.0)).unwrap();
7931 let a_id = EntityId("a".to_string());
7932 assert_eq!(g.entity_degree_ratio(&a_id).unwrap(), 0.5);
7933 }
7934
7935 #[test]
7936 fn test_entity_degree_ratio_zero_for_empty_graph() {
7937 let g = GraphStore::new();
7938 let id = EntityId("x".to_string());
7939 assert_eq!(g.entity_degree_ratio(&id).unwrap(), 0.0);
7940 }
7941
7942 #[test]
7945 fn test_entity_has_outgoing_edge_true_when_edge_exists() {
7946 let g = GraphStore::new();
7947 g.add_entity(Entity::new("a", "N")).unwrap();
7948 g.add_entity(Entity::new("b", "N")).unwrap();
7949 g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7950 let id = EntityId("a".to_string());
7951 assert!(g.entity_has_outgoing_edge(&id).unwrap());
7952 }
7953
7954 #[test]
7955 fn test_entity_has_outgoing_edge_false_for_sink_node() {
7956 let g = GraphStore::new();
7957 g.add_entity(Entity::new("a", "N")).unwrap();
7958 g.add_entity(Entity::new("b", "N")).unwrap();
7959 g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7960 let id = EntityId("b".to_string());
7961 assert!(!g.entity_has_outgoing_edge(&id).unwrap());
7962 }
7963
7964 #[test]
7965 fn test_count_nodes_with_self_loop_counts_correctly() {
7966 let g = GraphStore::new();
7967 g.add_entity(Entity::new("a", "N")).unwrap();
7968 g.add_entity(Entity::new("b", "N")).unwrap();
7969 g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
7970 g.add_relationship(Relationship::new("b", "a", "fwd", 1.0)).unwrap();
7971 assert_eq!(g.count_nodes_with_self_loop().unwrap(), 1);
7972 }
7973
7974 #[test]
7975 fn test_count_nodes_with_self_loop_zero_for_no_loops() {
7976 let g = GraphStore::new();
7977 g.add_entity(Entity::new("a", "N")).unwrap();
7978 g.add_entity(Entity::new("b", "N")).unwrap();
7979 g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
7980 assert_eq!(g.count_nodes_with_self_loop().unwrap(), 0);
7981 }
7982
7983 #[test]
7986 fn test_relationship_count_for_entity_correct() {
7987 let g = GraphStore::new();
7988 g.add_entity(Entity::new("hub", "N")).unwrap();
7989 g.add_entity(Entity::new("a", "N")).unwrap();
7990 g.add_entity(Entity::new("b", "N")).unwrap();
7991 g.add_relationship(Relationship::new("hub", "a", "E", 1.0)).unwrap();
7992 g.add_relationship(Relationship::new("hub", "b", "E", 1.0)).unwrap();
7993 let hub_id = EntityId("hub".to_string());
7994 assert_eq!(g.relationship_count_for_entity(&hub_id).unwrap(), 2);
7995 }
7996
7997 #[test]
7998 fn test_relationship_count_for_entity_zero_for_unknown() {
7999 let g = GraphStore::new();
8000 let id = EntityId("nobody".to_string());
8001 assert_eq!(g.relationship_count_for_entity(&id).unwrap(), 0);
8002 }
8003
8004 #[test]
8007 fn test_entities_by_label_returns_matching_ids() {
8008 let g = GraphStore::new();
8009 g.add_entity(Entity::new("a1", "Person")).unwrap();
8010 g.add_entity(Entity::new("a2", "Person")).unwrap();
8011 g.add_entity(Entity::new("a3", "Place")).unwrap();
8012 let mut ids = g.entities_by_label("Person").unwrap();
8013 ids.sort_by(|a, b| a.as_str().cmp(b.as_str()));
8014 assert_eq!(ids.len(), 2);
8015 assert_eq!(ids[0].as_str(), "a1");
8016 assert_eq!(ids[1].as_str(), "a2");
8017 }
8018
8019 #[test]
8020 fn test_entities_by_label_empty_for_unknown_label() {
8021 let g = GraphStore::new();
8022 g.add_entity(Entity::new("x", "Thing")).unwrap();
8023 let ids = g.entities_by_label("Nonexistent").unwrap();
8024 assert!(ids.is_empty());
8025 }
8026
8027 #[test]
8028 fn test_edge_count_between_returns_count() {
8029 let g = GraphStore::new();
8030 g.add_entity(Entity::new("u", "N")).unwrap();
8031 g.add_entity(Entity::new("v", "N")).unwrap();
8032 g.add_relationship(Relationship::new("u", "v", "LINKS", 1.0)).unwrap();
8033 g.add_relationship(Relationship::new("u", "v", "ALSO", 1.0)).unwrap();
8034 let from = EntityId::new("u");
8035 let to = EntityId::new("v");
8036 assert_eq!(g.edge_count_between(&from, &to).unwrap(), 2);
8037 }
8038
8039 #[test]
8040 fn test_edge_count_between_zero_when_no_edge() {
8041 let g = GraphStore::new();
8042 g.add_entity(Entity::new("u2", "N")).unwrap();
8043 g.add_entity(Entity::new("v2", "N")).unwrap();
8044 let from = EntityId::new("u2");
8045 let to = EntityId::new("v2");
8046 assert_eq!(g.edge_count_between(&from, &to).unwrap(), 0);
8047 }
8048
8049 #[test]
8052 fn test_is_connected_true_when_edge_exists() {
8053 let g = GraphStore::new();
8054 g.add_entity(Entity::new("p", "N")).unwrap();
8055 g.add_entity(Entity::new("q", "N")).unwrap();
8056 g.add_relationship(Relationship::new("p", "q", "LINKS", 1.0)).unwrap();
8057 let p = EntityId::new("p");
8058 let q = EntityId::new("q");
8059 assert!(g.is_connected(&p, &q).unwrap());
8060 }
8061
8062 #[test]
8063 fn test_is_connected_false_when_no_edge() {
8064 let g = GraphStore::new();
8065 g.add_entity(Entity::new("p2", "N")).unwrap();
8066 g.add_entity(Entity::new("q2", "N")).unwrap();
8067 let p2 = EntityId::new("p2");
8068 let q2 = EntityId::new("q2");
8069 assert!(!g.is_connected(&p2, &q2).unwrap());
8070 }
8071
8072 #[test]
8075 fn test_graph_is_empty_true_for_new_store() {
8076 let g = GraphStore::new();
8077 assert!(g.graph_is_empty().unwrap());
8078 }
8079
8080 #[test]
8081 fn test_graph_is_empty_false_after_adding_entity() {
8082 let g = GraphStore::new();
8083 g.add_entity(Entity::new("e", "N")).unwrap();
8084 assert!(!g.graph_is_empty().unwrap());
8085 }
8086
8087 #[test]
8090 fn test_unique_relationship_kinds_returns_sorted_kinds() {
8091 let g = GraphStore::new();
8092 g.add_entity(Entity::new("x", "N")).unwrap();
8093 g.add_entity(Entity::new("y", "N")).unwrap();
8094 g.add_entity(Entity::new("z", "N")).unwrap();
8095 g.add_relationship(Relationship::new("x", "y", "ZEBRA", 1.0)).unwrap();
8096 g.add_relationship(Relationship::new("x", "z", "APPLE", 1.0)).unwrap();
8097 g.add_relationship(Relationship::new("y", "z", "APPLE", 1.0)).unwrap();
8098 let kinds = g.unique_relationship_kinds().unwrap();
8099 assert_eq!(kinds, vec!["APPLE".to_string(), "ZEBRA".to_string()]);
8100 }
8101
8102 #[test]
8103 fn test_unique_relationship_kinds_empty_for_new_graph() {
8104 let g = GraphStore::new();
8105 assert!(g.unique_relationship_kinds().unwrap().is_empty());
8106 }
8107
8108 #[test]
8109 fn test_has_any_relationships_true_when_edge_added() {
8110 let g = GraphStore::new();
8111 g.add_entity(Entity::new("a", "N")).unwrap();
8112 g.add_entity(Entity::new("b", "N")).unwrap();
8113 g.add_relationship(Relationship::new("a", "b", "R", 1.0)).unwrap();
8114 assert!(g.has_any_relationships().unwrap());
8115 }
8116
8117 #[test]
8118 fn test_has_any_relationships_false_for_new_graph() {
8119 let g = GraphStore::new();
8120 assert!(!g.has_any_relationships().unwrap());
8121 }
8122
8123 #[test]
8126 fn test_avg_edge_weight_correct() {
8127 let g = GraphStore::new();
8128 g.add_entity(Entity::new("n1", "N")).unwrap();
8129 g.add_entity(Entity::new("n2", "N")).unwrap();
8130 g.add_relationship(Relationship::new("n1", "n2", "E", 2.0)).unwrap();
8131 g.add_relationship(Relationship::new("n1", "n2", "F", 4.0)).unwrap();
8132 assert!((g.avg_edge_weight().unwrap() - 3.0).abs() < 1e-6);
8133 }
8134
8135 #[test]
8136 fn test_avg_edge_weight_zero_for_empty_graph() {
8137 let g = GraphStore::new();
8138 assert_eq!(g.avg_edge_weight().unwrap(), 0.0);
8139 }
8140
8141 #[test]
8144 fn test_nodes_with_no_outgoing_returns_sink_nodes() {
8145 let g = GraphStore::new();
8146 g.add_entity(Entity::new("src", "N")).unwrap();
8147 g.add_entity(Entity::new("sink", "N")).unwrap();
8148 g.add_entity(Entity::new("iso", "N")).unwrap();
8149 g.add_relationship(Relationship::new("src", "sink", "E", 1.0)).unwrap();
8150 let sinks = g.nodes_with_no_outgoing().unwrap();
8151 let ids: Vec<&str> = sinks.iter().map(|e| e.id.as_str()).collect();
8152 assert!(ids.contains(&"sink"));
8153 assert!(ids.contains(&"iso"));
8154 assert!(!ids.contains(&"src"));
8155 }
8156
8157 #[test]
8158 fn test_nodes_with_no_outgoing_all_for_graph_with_no_edges() {
8159 let g = GraphStore::new();
8160 g.add_entity(Entity::new("a", "N")).unwrap();
8161 g.add_entity(Entity::new("b", "N")).unwrap();
8162 assert_eq!(g.nodes_with_no_outgoing().unwrap().len(), 2);
8163 }
8164
8165 #[test]
8168 fn test_relationship_kinds_returns_sorted_unique_kinds() {
8169 let g = GraphStore::new();
8170 g.add_entity(Entity::new("a", "N")).unwrap();
8171 g.add_entity(Entity::new("b", "N")).unwrap();
8172 g.add_entity(Entity::new("c", "N")).unwrap();
8173 g.add_relationship(Relationship::new("a", "b", "follows", 1.0)).unwrap();
8174 g.add_relationship(Relationship::new("b", "c", "likes", 1.0)).unwrap();
8175 g.add_relationship(Relationship::new("a", "c", "follows", 1.0)).unwrap();
8176 let kinds = g.relationship_kinds().unwrap();
8177 assert_eq!(kinds, vec!["follows", "likes"]);
8178 }
8179
8180 #[test]
8181 fn test_relationship_kinds_empty_for_empty_graph() {
8182 let g = GraphStore::new();
8183 assert!(g.relationship_kinds().unwrap().is_empty());
8184 }
8185
8186 #[test]
8187 fn test_max_out_degree_returns_correct_max() {
8188 let g = GraphStore::new();
8189 g.add_entity(Entity::new("a", "N")).unwrap();
8190 g.add_entity(Entity::new("b", "N")).unwrap();
8191 g.add_entity(Entity::new("c", "N")).unwrap();
8192 g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
8193 g.add_relationship(Relationship::new("a", "c", "k", 1.0)).unwrap();
8194 g.add_relationship(Relationship::new("b", "c", "k", 1.0)).unwrap();
8195 assert_eq!(g.max_out_degree().unwrap(), 2);
8196 }
8197
8198 #[test]
8201 fn test_in_degree_of_counts_incoming_edges() {
8202 let g = GraphStore::new();
8203 g.add_entity(Entity::new("src1", "N")).unwrap();
8204 g.add_entity(Entity::new("src2", "N")).unwrap();
8205 g.add_entity(Entity::new("dst", "N")).unwrap();
8206 g.add_relationship(Relationship::new("src1", "dst", "k", 1.0)).unwrap();
8207 g.add_relationship(Relationship::new("src2", "dst", "k", 1.0)).unwrap();
8208 let dst = EntityId("dst".to_string());
8209 assert_eq!(g.in_degree_of(&dst).unwrap(), 2);
8210 }
8211
8212 #[test]
8213 fn test_in_degree_of_zero_for_node_with_no_incoming() {
8214 let g = GraphStore::new();
8215 g.add_entity(Entity::new("alone", "N")).unwrap();
8216 let id = EntityId("alone".to_string());
8217 assert_eq!(g.in_degree_of(&id).unwrap(), 0);
8218 }
8219
8220 #[test]
8223 fn test_total_weight_for_kind_sums_correctly() {
8224 let g = GraphStore::new();
8225 g.add_entity(Entity::new("a", "N")).unwrap();
8226 g.add_entity(Entity::new("b", "N")).unwrap();
8227 g.add_entity(Entity::new("c", "N")).unwrap();
8228 g.add_relationship(Relationship::new("a", "b", "link", 2.0)).unwrap();
8229 g.add_relationship(Relationship::new("b", "c", "link", 4.0)).unwrap();
8230 g.add_relationship(Relationship::new("a", "c", "other", 1.0)).unwrap();
8231 let sum = g.total_weight_for_kind("link").unwrap();
8232 assert!((sum - 6.0).abs() < 1e-9);
8233 }
8234
8235 #[test]
8236 fn test_total_weight_for_kind_zero_for_unknown_kind() {
8237 let g = GraphStore::new();
8238 assert_eq!(g.total_weight_for_kind("nope").unwrap(), 0.0);
8239 }
8240
8241 #[test]
8242 fn test_entity_ids_with_label_returns_matching_ids() {
8243 let g = GraphStore::new();
8244 g.add_entity(Entity::new("e1", "Person")).unwrap();
8245 g.add_entity(Entity::new("e2", "Person")).unwrap();
8246 g.add_entity(Entity::new("e3", "Place")).unwrap();
8247 let mut ids = g.entity_ids_with_label("Person").unwrap();
8248 ids.sort_by(|a, b| a.0.cmp(&b.0));
8249 let strs: Vec<&str> = ids.iter().map(|id| id.0.as_str()).collect();
8250 assert_eq!(strs, vec!["e1", "e2"]);
8251 }
8252
8253 #[test]
8254 fn test_entity_ids_with_label_empty_for_unknown_label() {
8255 let g = GraphStore::new();
8256 assert!(g.entity_ids_with_label("Unknown").unwrap().is_empty());
8257 }
8258
8259 #[test]
8262 fn test_out_degree_of_returns_edge_count() {
8263 let g = GraphStore::new();
8264 g.add_entity(Entity::new("a", "N")).unwrap();
8265 g.add_entity(Entity::new("b", "N")).unwrap();
8266 g.add_entity(Entity::new("c", "N")).unwrap();
8267 g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
8268 g.add_relationship(Relationship::new("a", "c", "k", 1.0)).unwrap();
8269 let id = EntityId("a".to_string());
8270 assert_eq!(g.out_degree_of(&id).unwrap(), 2);
8271 }
8272
8273 #[test]
8274 fn test_out_degree_of_zero_for_sink_node() {
8275 let g = GraphStore::new();
8276 g.add_entity(Entity::new("sink", "N")).unwrap();
8277 let id = EntityId("sink".to_string());
8278 assert_eq!(g.out_degree_of(&id).unwrap(), 0);
8279 }
8280
8281 #[test]
8282 fn test_has_relationship_with_kind_true_when_kind_present() {
8283 let g = GraphStore::new();
8284 g.add_entity(Entity::new("a", "N")).unwrap();
8285 g.add_entity(Entity::new("b", "N")).unwrap();
8286 g.add_relationship(Relationship::new("a", "b", "follows", 1.0)).unwrap();
8287 assert!(g.has_relationship_with_kind("follows").unwrap());
8288 }
8289
8290 #[test]
8291 fn test_has_relationship_with_kind_false_for_absent_kind() {
8292 let g = GraphStore::new();
8293 g.add_entity(Entity::new("a", "N")).unwrap();
8294 g.add_entity(Entity::new("b", "N")).unwrap();
8295 g.add_relationship(Relationship::new("a", "b", "likes", 1.0)).unwrap();
8296 assert!(!g.has_relationship_with_kind("follows").unwrap());
8297 }
8298
8299 #[test]
8302 fn test_labels_unique_count_returns_distinct_count() {
8303 let g = GraphStore::new();
8304 g.add_entity(Entity::new("e1", "Person")).unwrap();
8305 g.add_entity(Entity::new("e2", "Person")).unwrap();
8306 g.add_entity(Entity::new("e3", "Place")).unwrap();
8307 assert_eq!(g.labels_unique_count().unwrap(), 2);
8308 }
8309
8310 #[test]
8311 fn test_labels_unique_count_zero_for_empty_graph() {
8312 let g = GraphStore::new();
8313 assert_eq!(g.labels_unique_count().unwrap(), 0);
8314 }
8315
8316 #[test]
8317 fn test_relationships_from_returns_outgoing_edges() {
8318 let g = GraphStore::new();
8319 g.add_entity(Entity::new("a", "N")).unwrap();
8320 g.add_entity(Entity::new("b", "N")).unwrap();
8321 g.add_entity(Entity::new("c", "N")).unwrap();
8322 g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
8323 g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
8324 let from = EntityId("a".to_string());
8325 assert_eq!(g.relationships_from(&from).unwrap().len(), 2);
8326 }
8327
8328 #[test]
8329 fn test_relationships_from_empty_for_node_with_no_edges() {
8330 let g = GraphStore::new();
8331 g.add_entity(Entity::new("lone", "N")).unwrap();
8332 let id = EntityId("lone".to_string());
8333 assert!(g.relationships_from(&id).unwrap().is_empty());
8334 }
8335
8336 #[test]
8339 fn test_cycle_count_counts_self_loops() {
8340 let g = GraphStore::new();
8341 g.add_entity(Entity::new("a", "N")).unwrap();
8342 g.add_entity(Entity::new("b", "N")).unwrap();
8343 g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
8344 g.add_relationship(Relationship::new("b", "b", "self", 1.0)).unwrap();
8345 g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
8346 assert_eq!(g.cycle_count().unwrap(), 2);
8347 }
8348
8349 #[test]
8350 fn test_cycle_count_zero_when_no_self_loops() {
8351 let g = GraphStore::new();
8352 g.add_entity(Entity::new("x", "N")).unwrap();
8353 g.add_entity(Entity::new("y", "N")).unwrap();
8354 g.add_relationship(Relationship::new("x", "y", "link", 1.0)).unwrap();
8355 assert_eq!(g.cycle_count().unwrap(), 0);
8356 }
8357
8358 #[test]
8361 fn test_entities_with_property_key_returns_matching_entities() {
8362 let g = GraphStore::new();
8363 let e1 = Entity::new("e1", "N").with_property("color", serde_json::json!("red"));
8364 let e2 = Entity::new("e2", "N");
8365 g.add_entity(e1).unwrap();
8366 g.add_entity(e2).unwrap();
8367 let result = g.entities_with_property_key("color").unwrap();
8368 assert_eq!(result.len(), 1);
8369 assert_eq!(result[0].id.0, "e1");
8370 }
8371
8372 #[test]
8373 fn test_entity_properties_count_returns_count_of_matching_entities() {
8374 let g = GraphStore::new();
8375 let e1 = Entity::new("p1", "N").with_property("tag", serde_json::json!("x"));
8376 let e2 = Entity::new("p2", "N").with_property("tag", serde_json::json!("y"));
8377 let e3 = Entity::new("p3", "N");
8378 g.add_entity(e1).unwrap();
8379 g.add_entity(e2).unwrap();
8380 g.add_entity(e3).unwrap();
8381 assert_eq!(g.entity_properties_count("tag").unwrap(), 2);
8382 }
8383
8384 #[test]
8387 fn test_all_entities_have_properties_true_when_all_have_props() {
8388 let g = GraphStore::new();
8389 let e1 = Entity::new("a", "N").with_property("k", "v".into());
8390 let e2 = Entity::new("b", "N").with_property("k", "v".into());
8391 g.add_entity(e1).unwrap();
8392 g.add_entity(e2).unwrap();
8393 assert!(g.all_entities_have_properties().unwrap());
8394 }
8395
8396 #[test]
8397 fn test_all_entities_have_properties_false_when_one_has_none() {
8398 let g = GraphStore::new();
8399 let e1 = Entity::new("a", "N").with_property("k", "v".into());
8400 let e2 = Entity::new("b", "N");
8401 g.add_entity(e1).unwrap();
8402 g.add_entity(e2).unwrap();
8403 assert!(!g.all_entities_have_properties().unwrap());
8404 }
8405
8406 #[test]
8407 fn test_all_entities_have_properties_true_for_empty_graph() {
8408 let g = GraphStore::new();
8409 assert!(g.all_entities_have_properties().unwrap());
8410 }
8411
8412 #[test]
8415 fn test_edges_to_returns_incoming_relationships() {
8416 let g = GraphStore::new();
8417 g.add_entity(Entity::new("a", "N")).unwrap();
8418 g.add_entity(Entity::new("b", "N")).unwrap();
8419 g.add_entity(Entity::new("c", "N")).unwrap();
8420 g.add_relationship(Relationship::new("a", "c", "EDGE", 1.0)).unwrap();
8421 g.add_relationship(Relationship::new("b", "c", "EDGE", 1.0)).unwrap();
8422 let incoming = g.edges_to(&EntityId("c".into())).unwrap();
8423 assert_eq!(incoming.len(), 2);
8424 }
8425
8426 #[test]
8427 fn test_edges_to_empty_for_no_incoming() {
8428 let g = GraphStore::new();
8429 g.add_entity(Entity::new("x", "N")).unwrap();
8430 assert!(g.edges_to(&EntityId("x".into())).unwrap().is_empty());
8431 }
8432
8433 #[test]
8434 fn test_entity_has_property_value_true_when_matches() {
8435 let g = GraphStore::new();
8436 g.add_entity(Entity::new("e1", "N").with_property("color", serde_json::json!("blue"))).unwrap();
8437 assert!(g.entity_has_property_value(&EntityId("e1".into()), "color", "blue").unwrap());
8438 }
8439
8440 #[test]
8441 fn test_entity_has_property_value_false_for_unknown_entity() {
8442 let g = GraphStore::new();
8443 assert!(!g.entity_has_property_value(&EntityId("nope".into()), "color", "blue").unwrap());
8444 }
8445}