use crate::error::AgentRuntimeError;
use crate::util::recover_lock;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Copy, PartialEq)]
struct OrdF32(f32);
impl Eq for OrdF32 {}
impl PartialOrd for OrdF32 {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OrdF32 {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0
.partial_cmp(&other.0)
.unwrap_or(std::cmp::Ordering::Greater)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct EntityId(pub String);
impl EntityId {
pub fn new(id: impl Into<String>) -> Self {
let id = id.into();
if id.is_empty() {
debug_assert!(false, "EntityId must not be empty");
tracing::warn!("EntityId::new called with an empty string — entity IDs should be non-empty to avoid lookup ambiguity");
}
Self(id)
}
pub fn try_new(id: impl Into<String>) -> Result<Self, AgentRuntimeError> {
let id = id.into();
if id.is_empty() {
return Err(AgentRuntimeError::Graph(
"EntityId must not be empty".into(),
));
}
Ok(Self(id))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn starts_with(&self, prefix: &str) -> bool {
self.0.starts_with(prefix)
}
}
impl AsRef<str> for EntityId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for EntityId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for EntityId {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<&str> for EntityId {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl std::str::FromStr for EntityId {
type Err = AgentRuntimeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_new(s)
}
}
impl std::ops::Deref for EntityId {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entity {
pub id: EntityId,
pub label: String,
pub properties: HashMap<String, Value>,
}
impl Entity {
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
Self {
id: EntityId::new(id),
label: label.into(),
properties: HashMap::new(),
}
}
pub fn with_properties(
id: impl Into<String>,
label: impl Into<String>,
properties: HashMap<String, Value>,
) -> Self {
Self {
id: EntityId::new(id),
label: label.into(),
properties,
}
}
pub fn with_property(mut self, key: impl Into<String>, value: Value) -> Self {
self.properties.insert(key.into(), value);
self
}
pub fn has_property(&self, key: &str) -> bool {
self.properties.contains_key(key)
}
pub fn property_value(&self, key: &str) -> Option<&serde_json::Value> {
self.properties.get(key)
}
pub fn remove_property(&mut self, key: &str) -> Option<serde_json::Value> {
self.properties.remove(key)
}
pub fn property_count(&self) -> usize {
self.properties.len()
}
pub fn properties_is_empty(&self) -> bool {
self.properties.is_empty()
}
pub fn property_keys(&self) -> Vec<&str> {
let mut keys: Vec<&str> = self.properties.keys().map(|k| k.as_str()).collect();
keys.sort_unstable();
keys
}
}
impl std::fmt::Display for Entity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Entity[id='{}', label='{}', props={}]",
self.id,
self.label,
self.properties.len()
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Relationship {
pub from: EntityId,
pub to: EntityId,
pub kind: String,
pub weight: f32,
}
impl Relationship {
pub fn new(
from: impl Into<String>,
to: impl Into<String>,
kind: impl Into<String>,
weight: f32,
) -> Self {
Self {
from: EntityId::new(from),
to: EntityId::new(to),
kind: kind.into(),
weight,
}
}
pub fn is_self_loop(&self) -> bool {
self.from == self.to
}
pub fn reversed(&self) -> Self {
Self {
from: self.to.clone(),
to: self.from.clone(),
kind: self.kind.clone(),
weight: self.weight,
}
}
pub fn with_weight(self, w: f32) -> Self {
Self { weight: w, ..self }
}
}
impl std::fmt::Display for Relationship {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} --{}({:.2})--> {}", self.from, self.kind, self.weight, self.to)
}
}
#[derive(Debug, thiserror::Error)]
pub enum MemGraphError {
#[error("Entity '{0}' not found")]
EntityNotFound(String),
#[error("Relationship '{kind}' from '{from}' to '{to}' already exists")]
DuplicateRelationship {
from: String,
to: String,
kind: String,
},
#[error("Graph internal error: {0}")]
Internal(String),
}
impl From<MemGraphError> for AgentRuntimeError {
fn from(e: MemGraphError) -> Self {
AgentRuntimeError::Graph(e.to_string())
}
}
#[derive(Debug, Clone)]
pub struct GraphStore {
inner: Arc<Mutex<GraphInner>>,
}
#[derive(Debug)]
struct GraphInner {
entities: HashMap<EntityId, Entity>,
relationships: Vec<Relationship>,
adjacency: HashMap<EntityId, Vec<Relationship>>,
reverse_adjacency: HashMap<EntityId, Vec<EntityId>>,
cycle_cache: Option<bool>,
}
impl GraphStore {
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(GraphInner {
entities: HashMap::new(),
relationships: Vec::new(),
adjacency: HashMap::new(),
reverse_adjacency: HashMap::new(),
cycle_cache: None,
})),
}
}
pub fn add_entity(&self, entity: Entity) -> Result<(), AgentRuntimeError> {
let mut inner = recover_lock(self.inner.lock(), "add_entity");
inner.cycle_cache = None;
inner.adjacency.entry(entity.id.clone()).or_default();
inner.entities.insert(entity.id.clone(), entity);
Ok(())
}
pub fn get_entity(&self, id: &EntityId) -> Result<Entity, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "get_entity");
inner
.entities
.get(id)
.cloned()
.ok_or_else(|| AgentRuntimeError::Graph(format!("entity '{}' not found", id.0)))
}
pub fn has_entity(&self, id: &EntityId) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::has_entity");
Ok(inner.entities.contains_key(id))
}
pub fn has_any_entities(&self) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::has_any_entities");
Ok(!inner.entities.is_empty())
}
pub fn entity_type_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_type_count");
let types: std::collections::HashSet<&str> =
inner.entities.values().map(|e| e.label.as_str()).collect();
Ok(types.len())
}
pub fn labels(&self) -> Result<Vec<String>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::labels");
let mut labels: Vec<String> = inner
.entities
.values()
.map(|e| e.label.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
labels.sort();
Ok(labels)
}
pub fn incoming_count_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::incoming_count_for");
Ok(inner.reverse_adjacency.get(id).map_or(0, |v| v.len()))
}
pub fn outgoing_count_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::outgoing_count_for");
Ok(inner.adjacency.get(id).map_or(0, |v| v.len()))
}
pub fn sink_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::sink_count");
let count = inner
.entities
.keys()
.filter(|id| inner.adjacency.get(*id).map_or(true, |v| v.is_empty()))
.count();
Ok(count)
}
pub fn bidirectional_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::bidirectional_count");
let mut count = 0usize;
for (from, rels) in &inner.adjacency {
for rel in rels {
let to = &rel.to;
if to > from {
if inner.adjacency.get(to).map_or(false, |v| v.iter().any(|r| &r.to == from)) {
count += 1;
}
}
}
}
Ok(count)
}
pub fn has_self_loops(&self) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::has_self_loops");
let found = inner.adjacency.iter().any(|(from, rels)| {
rels.iter().any(|r| &r.to == from)
});
Ok(found)
}
pub fn source_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::source_count");
let count = inner
.entities
.keys()
.filter(|id| inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty()))
.count();
Ok(count)
}
pub fn orphan_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::orphan_count");
let count = inner
.entities
.keys()
.filter(|id| inner.adjacency.get(*id).map_or(true, |v| v.is_empty()))
.count();
Ok(count)
}
pub fn isolated_entity_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::isolated_entity_count");
let count = inner.entities.keys().filter(|id| {
inner.adjacency.get(*id).map_or(true, |v| v.is_empty())
&& inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty())
}).count();
Ok(count)
}
pub fn avg_relationship_weight(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::avg_relationship_weight");
if inner.relationships.is_empty() {
return Ok(0.0);
}
let total: f32 = inner.relationships.iter().map(|r| r.weight).sum();
Ok(total as f64 / inner.relationships.len() as f64)
}
pub fn total_in_degree(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::total_in_degree");
Ok(inner.relationships.len())
}
pub fn relationship_count_between(
&self,
from: &EntityId,
to: &EntityId,
) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_count_between");
Ok(inner
.adjacency
.get(from)
.map_or(0, |rels| rels.iter().filter(|r| &r.to == to).count()))
}
pub fn edges_from(&self, id: &EntityId) -> Result<Vec<Relationship>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::edges_from");
Ok(inner
.adjacency
.get(id)
.cloned()
.unwrap_or_default())
}
pub fn has_edge(
&self,
from: &EntityId,
to: &EntityId,
) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::has_edge");
Ok(inner
.adjacency
.get(from)
.map_or(false, |rels| rels.iter().any(|r| &r.to == to)))
}
pub fn neighbors_of(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::neighbors_of");
let mut targets: Vec<EntityId> = inner
.adjacency
.get(id)
.map_or_else(Vec::new, |rels| rels.iter().map(|r| r.to.clone()).collect());
targets.sort_unstable();
targets.dedup();
Ok(targets)
}
pub fn add_relationship(&self, rel: Relationship) -> Result<(), AgentRuntimeError> {
let mut inner = recover_lock(self.inner.lock(), "add_relationship");
if !inner.entities.contains_key(&rel.from) {
return Err(AgentRuntimeError::Graph(format!(
"source entity '{}' not found",
rel.from.0
)));
}
if !inner.entities.contains_key(&rel.to) {
return Err(AgentRuntimeError::Graph(format!(
"target entity '{}' not found",
rel.to.0
)));
}
let duplicate = inner
.relationships
.iter()
.any(|r| r.from == rel.from && r.to == rel.to && r.kind == rel.kind);
if duplicate {
return Err(AgentRuntimeError::Graph(
MemGraphError::DuplicateRelationship {
from: rel.from.0.clone(),
to: rel.to.0.clone(),
kind: rel.kind.clone(),
}
.to_string(),
));
}
inner.cycle_cache = None;
inner
.adjacency
.entry(rel.from.clone())
.or_default()
.push(rel.clone());
inner
.reverse_adjacency
.entry(rel.to.clone())
.or_default()
.push(rel.from.clone());
inner.relationships.push(rel);
Ok(())
}
pub fn remove_relationship(
&self,
from: &EntityId,
to: &EntityId,
kind: &str,
) -> Result<(), AgentRuntimeError> {
let mut inner = recover_lock(self.inner.lock(), "remove_relationship");
let before = inner.relationships.len();
inner
.relationships
.retain(|r| !(&r.from == from && &r.to == to && r.kind == kind));
if inner.relationships.len() == before {
return Err(AgentRuntimeError::Graph(format!(
"relationship '{kind}' from '{}' to '{}' not found",
from.0, to.0
)));
}
if let Some(adj) = inner.adjacency.get_mut(from) {
adj.retain(|r| !(&r.to == to && r.kind == kind));
}
if let Some(rev) = inner.reverse_adjacency.get_mut(to) {
rev.retain(|src| src != from);
}
inner.cycle_cache = None;
Ok(())
}
pub fn remove_entity(&self, id: &EntityId) -> Result<(), AgentRuntimeError> {
let mut inner = recover_lock(self.inner.lock(), "remove_entity");
if inner.entities.remove(id).is_none() {
return Err(AgentRuntimeError::Graph(format!(
"entity '{}' not found",
id.0
)));
}
inner.cycle_cache = None;
inner.relationships.retain(|r| &r.from != id && &r.to != id);
inner.adjacency.remove(id);
for adj in inner.adjacency.values_mut() {
adj.retain(|r| &r.to != id);
}
inner.reverse_adjacency.remove(id);
for rev in inner.reverse_adjacency.values_mut() {
rev.retain(|src| src != id);
}
Ok(())
}
fn neighbours(adjacency: &HashMap<EntityId, Vec<Relationship>>, id: &EntityId) -> Vec<EntityId> {
adjacency
.get(id)
.map(|rels| rels.iter().map(|r| r.to.clone()).collect())
.unwrap_or_default()
}
#[tracing::instrument(skip(self))]
pub fn bfs(&self, start: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "bfs");
if !inner.entities.contains_key(start) {
return Err(AgentRuntimeError::Graph(format!(
"start entity '{}' not found",
start.0
)));
}
let mut visited: HashSet<EntityId> = HashSet::new();
let mut queue: VecDeque<EntityId> = VecDeque::new();
let mut result: Vec<EntityId> = Vec::new();
visited.insert(start.clone());
queue.push_back(start.clone());
while let Some(current) = queue.pop_front() {
let neighbours: Vec<EntityId> = Self::neighbours(&inner.adjacency, ¤t);
for neighbour in neighbours {
if visited.insert(neighbour.clone()) {
result.push(neighbour.clone());
queue.push_back(neighbour);
}
}
}
tracing::debug!("BFS visited {} nodes", result.len());
Ok(result)
}
#[tracing::instrument(skip(self))]
pub fn dfs(&self, start: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "dfs");
if !inner.entities.contains_key(start) {
return Err(AgentRuntimeError::Graph(format!(
"start entity '{}' not found",
start.0
)));
}
let mut visited: HashSet<EntityId> = HashSet::new();
let mut stack: Vec<EntityId> = Vec::new();
let mut result: Vec<EntityId> = Vec::new();
visited.insert(start.clone());
stack.push(start.clone());
while let Some(current) = stack.pop() {
let neighbours: Vec<EntityId> = Self::neighbours(&inner.adjacency, ¤t);
for neighbour in neighbours {
if visited.insert(neighbour.clone()) {
result.push(neighbour.clone());
stack.push(neighbour);
}
}
}
tracing::debug!("DFS visited {} nodes", result.len());
Ok(result)
}
#[tracing::instrument(skip(self))]
pub fn shortest_path(
&self,
from: &EntityId,
to: &EntityId,
) -> Result<Option<Vec<EntityId>>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "shortest_path");
if !inner.entities.contains_key(from) {
return Err(AgentRuntimeError::Graph(format!(
"source entity '{}' not found",
from.0
)));
}
if !inner.entities.contains_key(to) {
return Err(AgentRuntimeError::Graph(format!(
"target entity '{}' not found",
to.0
)));
}
if from == to {
return Ok(Some(vec![from.clone()]));
}
let mut visited: HashSet<EntityId> = HashSet::new();
let mut prev: HashMap<EntityId, EntityId> = HashMap::new();
let mut queue: VecDeque<EntityId> = VecDeque::new();
visited.insert(from.clone());
queue.push_back(from.clone());
while let Some(current) = queue.pop_front() {
for neighbour in Self::neighbours(&inner.adjacency, ¤t) {
if &neighbour == to {
let mut path = vec![neighbour, current.clone()];
let mut node = current;
while let Some(p) = prev.get(&node) {
path.push(p.clone());
node = p.clone();
}
path.reverse();
return Ok(Some(path));
}
if visited.insert(neighbour.clone()) {
prev.insert(neighbour.clone(), current.clone());
queue.push_back(neighbour);
}
}
}
Ok(None)
}
pub fn shortest_path_weighted(
&self,
from: &EntityId,
to: &EntityId,
) -> Result<Option<(Vec<EntityId>, f32)>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "shortest_path_weighted");
if !inner.entities.contains_key(from) {
return Err(AgentRuntimeError::Graph(format!(
"source entity '{}' not found",
from.0
)));
}
if !inner.entities.contains_key(to) {
return Err(AgentRuntimeError::Graph(format!(
"target entity '{}' not found",
to.0
)));
}
for rel in &inner.relationships {
if rel.weight < 0.0 {
return Err(AgentRuntimeError::Graph(format!(
"negative weight {:.4} on edge '{}' -> '{}'",
rel.weight, rel.from.0, rel.to.0
)));
}
}
if from == to {
return Ok(Some((vec![from.clone()], 0.0)));
}
let mut dist: HashMap<EntityId, f32> = HashMap::new();
let mut prev: HashMap<EntityId, EntityId> = HashMap::new();
let mut heap: BinaryHeap<(OrdF32, EntityId)> = BinaryHeap::new();
dist.insert(from.clone(), 0.0);
heap.push((OrdF32(-0.0), from.clone()));
while let Some((OrdF32(neg_cost), current)) = heap.pop() {
let cost = -neg_cost;
if let Some(&best) = dist.get(¤t) {
if cost > best {
continue;
}
}
if ¤t == to {
let mut path = vec![to.clone()];
let mut node = to.clone();
while let Some(p) = prev.get(&node) {
path.push(p.clone());
node = p.clone();
}
path.reverse();
return Ok(Some((path, cost)));
}
if let Some(rels) = inner.adjacency.get(¤t) {
for rel in rels {
let next_cost = cost + rel.weight;
let entry = dist.entry(rel.to.clone()).or_insert(f32::INFINITY);
if next_cost < *entry {
*entry = next_cost;
prev.insert(rel.to.clone(), current.clone());
heap.push((OrdF32(-next_cost), rel.to.clone()));
}
}
}
}
Ok(None)
}
fn bfs_into_set(inner: &GraphInner, start: &EntityId) -> HashSet<EntityId> {
let mut visited: HashSet<EntityId> = HashSet::new();
let mut queue: VecDeque<EntityId> = VecDeque::new();
visited.insert(start.clone());
queue.push_back(start.clone());
while let Some(current) = queue.pop_front() {
for neighbour in Self::neighbours(&inner.adjacency, ¤t) {
if visited.insert(neighbour.clone()) {
queue.push_back(neighbour);
}
}
}
visited
}
pub fn transitive_closure(
&self,
start: &EntityId,
) -> Result<HashSet<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "transitive_closure");
if !inner.entities.contains_key(start) {
return Err(AgentRuntimeError::Graph(format!(
"start entity '{}' not found",
start.0
)));
}
Ok(Self::bfs_into_set(&inner, start))
}
pub fn entity_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "entity_count");
Ok(inner.entities.len())
}
pub fn node_count(&self) -> Result<usize, AgentRuntimeError> {
self.entity_count()
}
pub fn relationship_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "relationship_count");
Ok(inner.relationships.len())
}
pub fn average_out_degree(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "average_out_degree");
let n = inner.entities.len();
if n == 0 {
return Ok(0.0);
}
Ok(inner.relationships.len() as f64 / n as f64)
}
pub fn in_degree_for(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "in_degree_for");
Ok(inner
.reverse_adjacency
.get(entity_id)
.map(|v| v.len())
.unwrap_or(0))
}
pub fn out_degree_for(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "out_degree_for");
Ok(inner
.adjacency
.get(entity_id)
.map(|v| v.len())
.unwrap_or(0))
}
pub fn predecessors(&self, entity_id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "predecessors");
let ids = inner
.reverse_adjacency
.get(entity_id)
.cloned()
.unwrap_or_default();
Ok(ids
.iter()
.filter_map(|id| inner.entities.get(id).cloned())
.collect())
}
pub fn is_source(&self, entity_id: &EntityId) -> Result<bool, AgentRuntimeError> {
Ok(self.in_degree_for(entity_id)? == 0)
}
pub fn successors(&self, entity_id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "successors");
let rels = inner.adjacency.get(entity_id).cloned().unwrap_or_default();
Ok(rels
.iter()
.filter_map(|r| inner.entities.get(&r.to).cloned())
.collect())
}
pub fn is_sink(&self, entity_id: &EntityId) -> Result<bool, AgentRuntimeError> {
Ok(self.out_degree_for(entity_id)? == 0)
}
pub fn reachable_from(
&self,
start: &EntityId,
) -> Result<std::collections::HashSet<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "reachable_from");
let mut visited = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
if let Some(rels) = inner.adjacency.get(start) {
for r in rels {
if visited.insert(r.to.clone()) {
queue.push_back(r.to.clone());
}
}
}
while let Some(current) = queue.pop_front() {
if let Some(rels) = inner.adjacency.get(¤t) {
for r in rels {
if visited.insert(r.to.clone()) {
queue.push_back(r.to.clone());
}
}
}
}
Ok(visited)
}
pub fn contains_cycle(&self) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "contains_cycle");
let mut color: HashMap<&EntityId, u8> = HashMap::new();
for start in inner.entities.keys() {
if color.get(start).copied().unwrap_or(0) != 0 {
continue;
}
let mut stack: Vec<(&EntityId, usize)> = vec![(start, 0)];
color.insert(start, 1);
while let Some((node, idx)) = stack.last_mut() {
let neighbors = inner.adjacency.get(node).map(|v| v.as_slice()).unwrap_or(&[]);
if *idx < neighbors.len() {
let neighbor = &neighbors[*idx].to;
*idx += 1;
match color.get(neighbor).copied().unwrap_or(0) {
1 => return Ok(true),
0 => {
color.insert(neighbor, 1);
stack.push((neighbor, 0));
}
_ => {}
}
} else {
color.insert(*node, 2);
stack.pop();
}
}
}
Ok(false)
}
pub fn edge_count(&self) -> Result<usize, AgentRuntimeError> {
self.relationship_count()
}
pub fn is_acyclic(&self) -> Result<bool, AgentRuntimeError> {
Ok(!self.contains_cycle()?)
}
pub fn avg_out_degree(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::avg_out_degree");
let n = inner.entities.len();
if n == 0 {
return Ok(0.0);
}
let total: usize = inner.entities.keys().map(|id| {
inner.adjacency.get(id).map_or(0, |v| v.len())
}).sum();
Ok(total as f64 / n as f64)
}
pub fn avg_in_degree(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::avg_in_degree");
let n = inner.entities.len();
if n == 0 {
return Ok(0.0);
}
let total: usize = inner.entities.keys().map(|id| {
inner.reverse_adjacency.get(id).map_or(0, |v| v.len())
}).sum();
Ok(total as f64 / n as f64)
}
pub fn max_out_degree(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "max_out_degree");
Ok(inner
.adjacency
.values()
.map(|v| v.len())
.max()
.unwrap_or(0))
}
pub fn max_in_degree(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "max_in_degree");
Ok(inner
.reverse_adjacency
.values()
.map(|v| v.len())
.max()
.unwrap_or(0))
}
pub fn min_out_degree(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "min_out_degree");
Ok(inner
.adjacency
.values()
.map(|v| v.len())
.min()
.unwrap_or(0))
}
pub fn min_in_degree(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "min_in_degree");
let min_from_reverse = inner
.reverse_adjacency
.values()
.map(|v| v.len())
.min()
.unwrap_or(0);
let has_zero_in_degree = inner
.entities
.keys()
.any(|id| !inner.reverse_adjacency.contains_key(id));
if has_zero_in_degree {
Ok(0)
} else {
Ok(min_from_reverse)
}
}
pub fn relationship_kinds_from(
&self,
id: &EntityId,
) -> Result<Vec<String>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "relationship_kinds_from");
let mut kinds: Vec<String> = inner
.adjacency
.get(id)
.map(|rels| {
rels.iter()
.map(|r| r.kind.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect()
})
.unwrap_or_default();
kinds.sort_unstable();
Ok(kinds)
}
pub fn weight_stats(&self) -> Result<Option<(f64, f64, f64)>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "weight_stats");
let weights: Vec<f64> = inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.map(|r| r.weight as f64)
.collect();
if weights.is_empty() {
return Ok(None);
}
let min = weights.iter().cloned().fold(f64::INFINITY, f64::min);
let max = weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let mean = weights.iter().sum::<f64>() / weights.len() as f64;
Ok(Some((min, max, mean)))
}
pub fn isolated_nodes(&self) -> Result<HashSet<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::isolated_nodes");
let mut result = HashSet::new();
for id in inner.entities.keys() {
let has_outbound = inner
.adjacency
.get(id)
.map_or(false, |v| !v.is_empty());
let has_inbound = inner
.reverse_adjacency
.get(id)
.map_or(false, |v| !v.is_empty());
if !has_outbound && !has_inbound {
result.insert(id.clone());
}
}
Ok(result)
}
pub fn sum_edge_weights(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "sum_edge_weights");
Ok(inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.map(|r| r.weight as f64)
.sum())
}
pub fn entity_ids(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "entity_ids");
Ok(inner.entities.keys().cloned().collect())
}
pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
Ok(self.entity_count()? == 0)
}
pub fn clear(&self) -> Result<(), AgentRuntimeError> {
let mut inner = recover_lock(self.inner.lock(), "GraphStore::clear");
inner.entities.clear();
inner.relationships.clear();
inner.adjacency.clear();
inner.reverse_adjacency.clear();
inner.cycle_cache = None;
Ok(())
}
pub fn entity_count_by_label(&self, label: &str) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "entity_count_by_label");
Ok(inner.entities.values().filter(|e| e.label == label).count())
}
pub fn graph_density(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "graph_density");
let v = inner.entities.len();
if v < 2 {
return Ok(0.0);
}
let e = inner.relationships.len() as f64;
Ok(e / (v as f64 * (v - 1) as f64))
}
pub fn entity_labels(&self) -> Result<Vec<String>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "entity_labels");
let mut labels: Vec<String> = inner
.entities
.values()
.map(|e| e.label.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
labels.sort_unstable();
Ok(labels)
}
pub fn relationship_kinds(&self) -> Result<Vec<String>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "relationship_kinds");
let mut kinds: Vec<String> = inner
.relationships
.iter()
.map(|r| r.kind.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
kinds.sort_unstable();
Ok(kinds)
}
pub fn relationship_kind_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_kind_count");
let count = inner
.relationships
.iter()
.map(|r| r.kind.as_str())
.collect::<std::collections::HashSet<_>>()
.len();
Ok(count)
}
pub fn entities_with_self_loops(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_self_loops");
let mut ids: Vec<EntityId> = inner
.adjacency
.iter()
.filter(|(from, rels)| rels.iter().any(|r| &r.to == *from))
.map(|(id, _)| id.clone())
.collect();
ids.sort_unstable();
Ok(ids)
}
pub fn update_entity_label(
&self,
id: &EntityId,
new_label: impl Into<String>,
) -> Result<bool, AgentRuntimeError> {
let mut inner = recover_lock(self.inner.lock(), "update_entity_label");
if let Some(entity) = inner.entities.get_mut(id) {
entity.label = new_label.into();
inner.cycle_cache = None;
Ok(true)
} else {
Ok(false)
}
}
pub fn degree_centrality(&self) -> Result<HashMap<EntityId, f32>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "degree_centrality");
let n = inner.entities.len();
let denom = if n <= 1 { 1.0 } else { (n - 1) as f32 };
let mut result = HashMap::new();
for id in inner.entities.keys() {
let od = inner.adjacency.get(id).map_or(0, |v| v.len());
let id_ = inner.reverse_adjacency.get(id).map_or(0, |v| v.len());
let centrality = if n <= 1 {
0.0
} else {
(od + id_) as f32 / denom
};
result.insert(id.clone(), centrality);
}
Ok(result)
}
pub fn betweenness_centrality(&self) -> Result<HashMap<EntityId, f32>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "betweenness_centrality");
let n = inner.entities.len();
let nodes: Vec<EntityId> = inner.entities.keys().cloned().collect();
let mut centrality: HashMap<EntityId, f32> =
nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
let mut stack: Vec<EntityId> = Vec::with_capacity(n);
let mut predecessors: HashMap<EntityId, Vec<EntityId>> =
nodes.iter().map(|id| (id.clone(), vec![])).collect();
let mut sigma: HashMap<EntityId, f32> =
nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
let mut dist: HashMap<EntityId, i64> =
nodes.iter().map(|id| (id.clone(), -1i64)).collect();
let mut delta: HashMap<EntityId, f32> =
nodes.iter().map(|id| (id.clone(), 0.0f32)).collect();
let mut queue: VecDeque<EntityId> = VecDeque::with_capacity(n);
for source in &nodes {
stack.clear();
for v in predecessors.values_mut() {
v.clear();
}
for v in sigma.values_mut() {
*v = 0.0;
}
for v in dist.values_mut() {
*v = -1;
}
for v in delta.values_mut() {
*v = 0.0;
}
queue.clear();
*sigma.entry(source.clone()).or_insert(0.0) = 1.0;
*dist.entry(source.clone()).or_insert(-1) = 0;
queue.push_back(source.clone());
while let Some(v) = queue.pop_front() {
stack.push(v.clone());
let d_v = *dist.get(&v).unwrap_or(&0);
let sigma_v = *sigma.get(&v).unwrap_or(&0.0);
if let Some(rels) = inner.adjacency.get(&v) {
for rel in rels {
let w = &rel.to;
let d_w = dist.get(w).copied().unwrap_or(-1);
if d_w < 0 {
queue.push_back(w.clone());
*dist.entry(w.clone()).or_insert(-1) = d_v + 1;
}
if dist.get(w).copied().unwrap_or(-1) == d_v + 1 {
*sigma.entry(w.clone()).or_insert(0.0) += sigma_v;
predecessors.entry(w.clone()).or_default().push(v.clone());
}
}
}
}
while let Some(w) = stack.pop() {
let delta_w = *delta.get(&w).unwrap_or(&0.0);
let sigma_w = *sigma.get(&w).unwrap_or(&1.0);
for v in predecessors
.get(&w)
.map(|ps| ps.as_slice())
.unwrap_or_default()
{
let sigma_v = *sigma.get(v).unwrap_or(&1.0);
let contribution = (sigma_v / sigma_w) * (1.0 + delta_w);
*delta.entry(v.clone()).or_insert(0.0) += contribution;
}
if &w != source {
*centrality.entry(w.clone()).or_insert(0.0) += delta_w;
}
}
}
if n > 2 {
let norm = 2.0 / (((n - 1) * (n - 2)) as f32);
for v in centrality.values_mut() {
*v *= norm;
}
} else {
for v in centrality.values_mut() {
*v = 0.0;
}
}
Ok(centrality)
}
pub fn label_propagation_communities(
&self,
max_iterations: usize,
) -> Result<HashMap<EntityId, usize>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "label_propagation_communities");
let nodes: Vec<EntityId> = inner.entities.keys().cloned().collect();
let mut reverse_adj: HashMap<EntityId, Vec<EntityId>> =
nodes.iter().map(|id| (id.clone(), vec![])).collect();
for rel in &inner.relationships {
reverse_adj
.entry(rel.to.clone())
.or_default()
.push(rel.from.clone());
}
let mut labels: HashMap<EntityId, usize> = nodes
.iter()
.enumerate()
.map(|(i, id)| (id.clone(), i))
.collect();
let mut freq: HashMap<usize, usize> = HashMap::new();
for _ in 0..max_iterations {
let mut changed = false;
for node in &nodes {
let out_labels = inner
.adjacency
.get(node)
.map(|rels| {
rels.iter()
.map(|r| labels.get(&r.to).copied().unwrap_or(0))
})
.into_iter()
.flatten();
let in_labels = reverse_adj
.get(node)
.map(|froms| froms.iter().map(|f| labels.get(f).copied().unwrap_or(0)))
.into_iter()
.flatten();
freq.clear();
for lbl in out_labels.chain(in_labels) {
*freq.entry(lbl).or_insert(0) += 1;
}
if freq.is_empty() {
continue;
}
let best = freq
.iter()
.max_by_key(|&(_, count)| count)
.map(|(&lbl, _)| lbl);
if let Some(new_label) = best {
let current = labels.entry(node.clone()).or_insert(0);
if *current != new_label {
*current = new_label;
changed = true;
}
}
}
if !changed {
break;
}
}
Ok(labels)
}
pub fn detect_cycles(&self) -> Result<bool, AgentRuntimeError> {
let mut inner = recover_lock(self.inner.lock(), "detect_cycles");
if let Some(cached) = inner.cycle_cache {
return Ok(cached);
}
let mut color: HashMap<&EntityId, u8> =
inner.entities.keys().map(|id| (id, 0u8)).collect();
let has_cycle = 'outer: {
for start in inner.entities.keys() {
if *color.get(start).unwrap_or(&0) != 0 {
continue;
}
let mut stack: Vec<(&EntityId, usize)> = vec![(start, 0)];
*color.entry(start).or_insert(0) = 1;
while let Some((node, idx)) = stack.last_mut() {
let rels = inner
.adjacency
.get(*node)
.map(|v| v.as_slice())
.unwrap_or(&[]);
if *idx < rels.len() {
let next = &rels[*idx].to;
*idx += 1;
match color.get(next).copied().unwrap_or(0) {
1 => break 'outer true, 0 => {
*color.entry(next).or_insert(0) = 1;
stack.push((next, 0));
}
_ => {} }
} else {
*color.entry(*node).or_insert(0) = 2;
stack.pop();
}
}
}
false
};
inner.cycle_cache = Some(has_cycle);
Ok(has_cycle)
}
pub fn topological_sort(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "topological_sort");
let mut color: HashMap<&EntityId, u8> =
inner.entities.keys().map(|id| (id, 0u8)).collect();
let mut result: Vec<EntityId> = Vec::with_capacity(inner.entities.len());
for start in inner.entities.keys() {
if *color.get(start).unwrap_or(&0) != 0 {
continue;
}
let mut stack: Vec<(&EntityId, usize, bool)> = vec![(start, 0, false)];
*color.entry(start).or_insert(0) = 1;
while let Some((node, idx, pushed)) = stack.last_mut() {
let rels = inner.adjacency.get(*node).map(|v| v.as_slice()).unwrap_or(&[]);
if *idx < rels.len() {
let next = &rels[*idx].to;
*idx += 1;
match color.get(next).copied().unwrap_or(0) {
1 => return Err(AgentRuntimeError::Graph(
"topological_sort: graph contains a cycle".into(),
)),
0 => {
*color.entry(next).or_insert(0) = 1;
stack.push((next, 0, false));
}
_ => {} }
} else {
if !*pushed {
result.push((*node).clone());
*pushed = true;
}
*color.entry(*node).or_insert(0) = 2;
stack.pop();
}
}
}
result.reverse();
Ok(result)
}
pub fn update_entity_property(
&self,
id: &EntityId,
key: impl Into<String>,
value: serde_json::Value,
) -> Result<bool, AgentRuntimeError> {
let mut inner = recover_lock(self.inner.lock(), "update_entity_property");
if let Some(entity) = inner.entities.get_mut(id) {
entity.properties.insert(key.into(), value);
Ok(true)
} else {
Ok(false)
}
}
pub fn is_dag(&self) -> Result<bool, AgentRuntimeError> {
Ok(!self.detect_cycles()?)
}
pub fn connected_components(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "connected_components");
let ids: Vec<&EntityId> = inner.entities.keys().collect();
if ids.is_empty() {
return Ok(0);
}
let idx: HashMap<&EntityId, usize> =
ids.iter().enumerate().map(|(i, id)| (*id, i)).collect();
let mut parent: Vec<usize> = (0..ids.len()).collect();
fn find(parent: &mut Vec<usize>, x: usize) -> usize {
if parent[x] != x {
parent[x] = find(parent, parent[x]);
}
parent[x]
}
fn union(parent: &mut Vec<usize>, a: usize, b: usize) {
let ra = find(parent, a);
let rb = find(parent, b);
if ra != rb {
parent[ra] = rb;
}
}
for rel in &inner.relationships {
if let (Some(&a), Some(&b)) = (idx.get(&rel.from), idx.get(&rel.to)) {
union(&mut parent, a, b);
}
}
let components = ids
.iter()
.enumerate()
.filter(|(i, _)| find(&mut parent, *i) == *i)
.count();
Ok(components)
}
pub fn weakly_connected(&self) -> Result<bool, AgentRuntimeError> {
Ok(self.connected_components()? <= 1)
}
pub fn sink_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "sink_nodes");
Ok(inner
.entities
.values()
.filter(|e| {
inner
.adjacency
.get(&e.id)
.map_or(true, |v| v.is_empty())
})
.cloned()
.collect())
}
pub fn source_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "source_nodes");
Ok(inner
.entities
.values()
.filter(|e| {
inner
.reverse_adjacency
.get(&e.id)
.map_or(true, |v| v.is_empty())
})
.cloned()
.collect())
}
pub fn isolates(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "isolates");
Ok(inner
.entities
.values()
.filter(|e| {
inner
.adjacency
.get(&e.id)
.map_or(true, |v| v.is_empty())
&& inner
.reverse_adjacency
.get(&e.id)
.map_or(true, |v| v.is_empty())
})
.cloned()
.collect())
}
pub fn out_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "out_degree");
Ok(inner.adjacency.get(id).map_or(0, |rels| rels.len()))
}
pub fn in_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "in_degree");
Ok(inner
.reverse_adjacency
.get(id)
.map_or(0, |srcs| srcs.len()))
}
pub fn total_degree(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
let out = self.out_degree(id)?;
let r#in = self.in_degree(id)?;
Ok(out + r#in)
}
pub fn entity_property_keys(&self, id: &EntityId) -> Result<Vec<String>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "entity_property_keys");
let entity = match inner.entities.get(id) {
Some(e) => e,
None => return Ok(vec![]),
};
let mut keys: Vec<String> = entity.properties.keys().cloned().collect();
keys.sort_unstable();
Ok(keys)
}
pub fn path_exists(&self, from: &str, to: &str) -> Result<bool, AgentRuntimeError> {
let from_id = EntityId::new(from);
let to_id = EntityId::new(to);
match self.shortest_path(&from_id, &to_id) {
Ok(Some(_)) => Ok(true),
Ok(None) => Ok(false),
Err(e) => Err(e),
}
}
pub fn shortest_path_length(
&self,
from: &EntityId,
to: &EntityId,
) -> Result<Option<usize>, AgentRuntimeError> {
Ok(self.shortest_path(from, to)?.map(|path| path.len().saturating_sub(1)))
}
pub fn bfs_bounded(
&self,
start: &str,
max_depth: usize,
max_nodes: usize,
) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "bfs_bounded");
let start_id = EntityId::new(start);
if !inner.entities.contains_key(&start_id) {
return Err(AgentRuntimeError::Graph(format!(
"start entity '{start}' not found"
)));
}
let mut visited: std::collections::HashMap<EntityId, usize> = std::collections::HashMap::new();
let mut queue: VecDeque<(EntityId, usize)> = VecDeque::new();
let mut result: Vec<EntityId> = Vec::new();
visited.insert(start_id.clone(), 0);
queue.push_back((start_id.clone(), 0));
result.push(start_id);
while let Some((current, depth)) = queue.pop_front() {
if result.len() >= max_nodes {
break;
}
if depth >= max_depth {
continue;
}
for neighbour in Self::neighbours(&inner.adjacency, ¤t) {
if !visited.contains_key(&neighbour) {
let new_depth = depth + 1;
visited.insert(neighbour.clone(), new_depth);
result.push(neighbour.clone());
if result.len() >= max_nodes {
break;
}
queue.push_back((neighbour, new_depth));
}
}
}
Ok(result)
}
pub fn dfs_bounded(
&self,
start: &str,
max_depth: usize,
max_nodes: usize,
) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "dfs_bounded");
let start_id = EntityId::new(start);
if !inner.entities.contains_key(&start_id) {
return Err(AgentRuntimeError::Graph(format!(
"start entity '{start}' not found"
)));
}
let mut visited: HashSet<EntityId> = HashSet::new();
let mut stack: Vec<(EntityId, usize)> = Vec::new();
let mut result: Vec<EntityId> = Vec::new();
visited.insert(start_id.clone());
stack.push((start_id.clone(), 0));
result.push(start_id);
while let Some((current, depth)) = stack.pop() {
if result.len() >= max_nodes {
break;
}
if depth >= max_depth {
continue;
}
for neighbour in Self::neighbours(&inner.adjacency, ¤t) {
if visited.insert(neighbour.clone()) {
result.push(neighbour.clone());
if result.len() >= max_nodes {
break;
}
stack.push((neighbour, depth + 1));
}
}
}
Ok(result)
}
pub fn all_entities(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "all_entities");
Ok(inner.entities.values().cloned().collect())
}
pub fn all_relationships(&self) -> Result<Vec<Relationship>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "all_relationships");
Ok(inner.relationships.clone())
}
pub fn find_relationships_by_kind(&self, kind: &str) -> Result<Vec<Relationship>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "find_relationships_by_kind");
Ok(inner.relationships.iter().filter(|r| r.kind == kind).cloned().collect())
}
pub fn count_relationships_by_kind(&self, kind: &str) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "count_relationships_by_kind");
Ok(inner.relationships.iter().filter(|r| r.kind == kind).count())
}
pub fn find_entities_by_label(&self, label: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "find_entities_by_label");
Ok(inner
.entities
.values()
.filter(|e| e.label == label)
.cloned()
.collect())
}
pub fn find_entities_by_labels(&self, labels: &[&str]) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "find_entities_by_labels");
let label_set: std::collections::HashSet<&str> = labels.iter().copied().collect();
Ok(inner
.entities
.values()
.filter(|e| label_set.contains(e.label.as_str()))
.cloned()
.collect())
}
pub fn remove_isolated(&self) -> Result<usize, AgentRuntimeError> {
let mut inner = recover_lock(self.inner.lock(), "remove_isolated");
let isolated: Vec<EntityId> = inner
.entities
.keys()
.filter(|id| {
inner.adjacency.get(*id).map_or(true, |v| v.is_empty())
&& inner.reverse_adjacency.get(*id).map_or(true, |v| v.is_empty())
})
.cloned()
.collect();
let count = isolated.len();
for id in &isolated {
inner.entities.remove(id);
inner.adjacency.remove(id);
inner.reverse_adjacency.remove(id);
}
if count > 0 {
inner.cycle_cache = None;
}
Ok(count)
}
pub fn get_relationships_for(
&self,
id: &EntityId,
) -> Result<Vec<Relationship>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "get_relationships_for");
Ok(inner
.adjacency
.get(id)
.cloned()
.unwrap_or_default())
}
pub fn relationships_between(
&self,
from: &EntityId,
to: &EntityId,
) -> Result<Vec<Relationship>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "relationships_between");
Ok(inner
.relationships
.iter()
.filter(|r| {
(r.from == *from && r.to == *to) || (r.from == *to && r.to == *from)
})
.cloned()
.collect())
}
pub fn find_entities_by_property(
&self,
key: &str,
expected: &serde_json::Value,
) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "find_entities_by_property");
Ok(inner
.entities
.values()
.filter(|e| e.properties.get(key) == Some(expected))
.cloned()
.collect())
}
pub fn merge(&self, other: &GraphStore) -> Result<(), AgentRuntimeError> {
let other_inner = recover_lock(other.inner.lock(), "merge:read");
let other_entities: Vec<Entity> = other_inner.entities.values().cloned().collect();
let other_rels: Vec<Relationship> = other_inner.relationships.clone();
drop(other_inner);
let mut inner = recover_lock(self.inner.lock(), "merge:write");
inner.cycle_cache = None;
for entity in other_entities {
inner.adjacency.entry(entity.id.clone()).or_default();
inner.entities.insert(entity.id.clone(), entity);
}
for rel in other_rels {
let already_exists = inner
.relationships
.iter()
.any(|r| r.from == rel.from && r.to == rel.to && r.kind == rel.kind);
if !already_exists && inner.entities.contains_key(&rel.from) && inner.entities.contains_key(&rel.to) {
inner
.adjacency
.entry(rel.from.clone())
.or_default()
.push(rel.clone());
inner.relationships.push(rel);
}
}
Ok(())
}
pub fn neighbor_entities(&self, id: &EntityId) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "neighbor_entities");
let neighbors: Vec<Entity> = inner
.adjacency
.get(id)
.iter()
.flat_map(|rels| rels.iter())
.filter_map(|r| inner.entities.get(&r.to).cloned())
.collect();
Ok(neighbors)
}
pub fn neighbor_ids(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "neighbor_ids");
let ids: Vec<EntityId> = inner
.adjacency
.get(id)
.iter()
.flat_map(|rels| rels.iter())
.map(|r| r.to.clone())
.collect();
Ok(ids)
}
pub fn remove_all_relationships_for(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
let mut inner = recover_lock(self.inner.lock(), "remove_all_relationships_for");
inner.adjacency.remove(id);
let before = inner.relationships.len();
inner.relationships.retain(|r| &r.from != id);
inner.cycle_cache = None;
Ok(before - inner.relationships.len())
}
pub fn entity_exists(&self, id: &EntityId) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "entity_exists");
Ok(inner.entities.contains_key(id))
}
pub fn relationship_exists(
&self,
from: &EntityId,
to: &EntityId,
kind: &str,
) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "relationship_exists");
Ok(inner
.adjacency
.get(from)
.map_or(false, |rels| rels.iter().any(|r| r.to == *to && r.kind == kind)))
}
pub fn subgraph(&self, node_ids: &[EntityId]) -> Result<GraphStore, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "subgraph");
let id_set: HashSet<&EntityId> = node_ids.iter().collect();
let new_store = GraphStore::new();
let entities_to_copy: Vec<Entity> = node_ids
.iter()
.map(|id| {
inner
.entities
.get(id)
.cloned()
.ok_or_else(|| {
AgentRuntimeError::Graph(format!("entity '{}' not found", id.0))
})
})
.collect::<Result<_, _>>()?;
{
let mut new_inner = recover_lock(new_store.inner.lock(), "subgraph:add_entities");
for entity in entities_to_copy {
new_inner.adjacency.entry(entity.id.clone()).or_default();
new_inner.entities.insert(entity.id.clone(), entity);
}
}
{
let mut new_inner =
recover_lock(new_store.inner.lock(), "subgraph:add_relationships");
for rel in inner.relationships.iter() {
if id_set.contains(&rel.from) && id_set.contains(&rel.to) {
new_inner
.adjacency
.entry(rel.from.clone())
.or_default()
.push(rel.clone());
new_inner.relationships.push(rel.clone());
}
}
}
Ok(new_store)
}
pub fn reverse(&self) -> Result<GraphStore, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "reverse");
let reversed = GraphStore::new();
{
let mut r_inner = recover_lock(reversed.inner.lock(), "reverse:entities");
for entity in inner.entities.values() {
r_inner.adjacency.entry(entity.id.clone()).or_default();
r_inner.entities.insert(entity.id.clone(), entity.clone());
}
}
for rel in &inner.relationships {
let flipped = Relationship {
from: rel.to.clone(),
to: rel.from.clone(),
kind: rel.kind.clone(),
weight: rel.weight,
};
let mut r_inner = recover_lock(reversed.inner.lock(), "reverse:rels");
r_inner
.adjacency
.entry(flipped.from.clone())
.or_default()
.push(flipped.clone());
r_inner.relationships.push(flipped);
}
Ok(reversed)
}
pub fn common_neighbors(
&self,
a: &EntityId,
b: &EntityId,
) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "common_neighbors");
let a_set: HashSet<&EntityId> = inner
.adjacency
.get(a)
.map_or(HashSet::new(), |rels| rels.iter().map(|r| &r.to).collect());
let b_set: HashSet<&EntityId> = inner
.adjacency
.get(b)
.map_or(HashSet::new(), |rels| rels.iter().map(|r| &r.to).collect());
let common: Vec<Entity> = a_set
.intersection(&b_set)
.filter_map(|id| inner.entities.get(*id).cloned())
.collect();
Ok(common)
}
pub fn weight_of(
&self,
from: &EntityId,
to: &EntityId,
) -> Result<Option<f32>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "weight_of");
let weight = inner
.adjacency
.get(from)
.and_then(|rels| rels.iter().find(|r| &r.to == to))
.map(|r| r.weight);
Ok(weight)
}
pub fn neighbors_in(&self, id: &EntityId) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "neighbors_in");
Ok(inner
.reverse_adjacency
.get(id)
.cloned()
.unwrap_or_default())
}
pub fn density(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "density");
let n = inner.entities.len();
if n < 2 {
return Ok(0.0);
}
let max_edges = n * (n - 1);
Ok(inner.relationships.len() as f64 / max_edges as f64)
}
pub fn avg_degree(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "avg_degree");
let n = inner.entities.len();
if n == 0 {
return Ok(0.0);
}
Ok(inner.relationships.len() as f64 / n as f64)
}
pub fn total_weight(&self) -> Result<f32, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "total_weight");
Ok(inner.relationships.iter().map(|r| r.weight).sum())
}
pub fn max_edge_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "max_edge_weight");
Ok(inner
.relationships
.iter()
.map(|r| r.weight)
.reduce(f32::max))
}
pub fn min_edge_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "min_edge_weight");
Ok(inner
.relationships
.iter()
.map(|r| r.weight)
.reduce(f32::min))
}
pub fn top_n_by_out_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
if n == 0 {
return Ok(Vec::new());
}
let inner = recover_lock(self.inner.lock(), "top_n_by_out_degree");
let mut pairs: Vec<(&EntityId, usize)> = inner
.adjacency
.iter()
.map(|(id, rels)| (id, rels.len()))
.collect();
pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
Ok(pairs
.into_iter()
.take(n)
.filter_map(|(id, _)| inner.entities.get(id).cloned())
.collect())
}
pub fn remove_entity_and_edges(&self, id: &EntityId) -> Result<(), AgentRuntimeError> {
let mut inner = recover_lock(self.inner.lock(), "remove_entity_and_edges");
if !inner.entities.contains_key(id) {
return Err(AgentRuntimeError::Graph(format!(
"entity '{}' not found",
id.0
)));
}
inner.entities.remove(id);
inner.relationships.retain(|r| &r.from != id && &r.to != id);
inner.adjacency.remove(id);
for adj in inner.adjacency.values_mut() {
adj.retain(|r| &r.to != id);
}
inner.reverse_adjacency.remove(id);
for rev in inner.reverse_adjacency.values_mut() {
rev.retain(|src| src != id);
}
inner.cycle_cache = None;
Ok(())
}
pub fn hub_nodes(&self, threshold: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "hub_nodes");
Ok(inner
.entities
.values()
.filter(|e| {
inner
.adjacency
.get(&e.id)
.map_or(0, |rels| rels.len())
>= threshold
})
.cloned()
.collect())
}
pub fn incident_relationships(
&self,
entity_id: &EntityId,
) -> Result<Vec<Relationship>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "incident_relationships");
Ok(inner
.relationships
.iter()
.filter(|r| &r.from == entity_id || &r.to == entity_id)
.cloned()
.collect())
}
pub fn max_out_degree_entity(&self) -> Result<Option<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "max_out_degree_entity");
let best = inner
.adjacency
.iter()
.max_by_key(|(_, rels)| rels.len())
.and_then(|(id, _)| inner.entities.get(id).cloned());
Ok(best)
}
pub fn max_in_degree_entity(&self) -> Result<Option<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "max_in_degree_entity");
let best = inner
.reverse_adjacency
.iter()
.max_by_key(|(_, srcs)| srcs.len())
.and_then(|(id, _)| inner.entities.get(id).cloned());
Ok(best)
}
pub fn leaf_nodes(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "leaf_nodes");
Ok(inner
.entities
.values()
.filter(|e| {
inner
.adjacency
.get(&e.id)
.map_or(true, |rels| rels.is_empty())
})
.cloned()
.collect())
}
pub fn top_nodes_by_out_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "top_nodes_by_out_degree");
let mut pairs: Vec<(&EntityId, usize)> = inner
.entities
.keys()
.map(|id| (id, inner.adjacency.get(id).map_or(0, |v| v.len())))
.collect();
pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
pairs.truncate(n);
Ok(pairs
.into_iter()
.filter_map(|(id, _)| inner.entities.get(id).cloned())
.collect())
}
pub fn top_nodes_by_in_degree(&self, n: usize) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "top_nodes_by_in_degree");
let mut pairs: Vec<(&EntityId, usize)> = inner
.entities
.keys()
.map(|id| (id, inner.reverse_adjacency.get(id).map_or(0, |v| v.len())))
.collect();
pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
pairs.truncate(n);
Ok(pairs
.into_iter()
.filter_map(|(id, _)| inner.entities.get(id).cloned())
.collect())
}
pub fn relationship_type_counts(&self) -> Result<HashMap<String, usize>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_type_counts");
let mut counts: HashMap<String, usize> = HashMap::new();
for rel in &inner.relationships {
*counts.entry(rel.kind.clone()).or_insert(0) += 1;
}
Ok(counts)
}
pub fn entities_without_property(&self, key: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_without_property");
Ok(inner
.entities
.values()
.filter(|e| !e.properties.contains_key(key))
.cloned()
.collect())
}
pub fn unique_relationship_types(&self) -> Result<Vec<String>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::unique_relationship_types");
let mut kinds: Vec<String> = inner
.relationships
.iter()
.map(|r| r.kind.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
kinds.sort_unstable();
Ok(kinds)
}
pub fn avg_property_count(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::avg_property_count");
let n = inner.entities.len();
if n == 0 {
return Ok(0.0);
}
let total: usize = inner.entities.values().map(|e| e.properties.len()).sum();
Ok(total as f64 / n as f64)
}
pub fn property_key_frequency(&self) -> Result<HashMap<String, usize>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::property_key_frequency");
let mut freq: HashMap<String, usize> = HashMap::new();
for entity in inner.entities.values() {
for key in entity.properties.keys() {
*freq.entry(key.clone()).or_insert(0) += 1;
}
}
Ok(freq)
}
pub fn entities_sorted_by_label(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_sorted_by_label");
let mut entities: Vec<Entity> = inner.entities.values().cloned().collect();
entities.sort_unstable_by(|a, b| a.label.cmp(&b.label).then_with(|| a.id.cmp(&b.id)));
Ok(entities)
}
pub fn entities_with_property(&self, key: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_property");
let entities: Vec<Entity> = inner
.entities
.values()
.filter(|e| e.properties.contains_key(key))
.cloned()
.collect();
Ok(entities)
}
pub fn total_relationship_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::total_relationship_count");
Ok(inner.adjacency.values().map(|rels| rels.len()).sum())
}
pub fn path_to_string(&self, path: &[EntityId]) -> Result<String, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::path_to_string");
let parts: Vec<String> = path
.iter()
.map(|id| {
inner
.entities
.get(id)
.map(|e| e.label.clone())
.unwrap_or_else(|| id.0.clone())
})
.collect();
Ok(parts.join(" \u{2192} "))
}
pub fn relationships_of_kind(&self, kind: &str) -> Result<Vec<Relationship>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::relationships_of_kind");
let mut result = Vec::new();
for rels in inner.adjacency.values() {
for rel in rels {
if rel.kind == kind {
result.push(rel.clone());
}
}
}
Ok(result)
}
pub fn entities_without_label(&self, label: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_without_label");
Ok(inner
.entities
.values()
.filter(|e| e.label != label)
.cloned()
.collect())
}
pub fn entities_without_outgoing(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_without_outgoing");
let entities: Vec<Entity> = inner
.entities
.values()
.filter(|e| !inner.adjacency.contains_key(&e.id) || inner.adjacency[&e.id].is_empty())
.cloned()
.collect();
Ok(entities)
}
pub fn entities_without_incoming(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_without_incoming");
let entities: Vec<Entity> = inner
.entities
.values()
.filter(|e| !inner.reverse_adjacency.contains_key(&e.id) || inner.reverse_adjacency[&e.id].is_empty())
.cloned()
.collect();
Ok(entities)
}
pub fn total_out_degree(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::total_out_degree");
Ok(inner.adjacency.values().map(|rels| rels.len()).sum())
}
pub fn relationships_with_weight_above(
&self,
threshold: f32,
) -> Result<Vec<Relationship>, AgentRuntimeError> {
let inner = recover_lock(
self.inner.lock(),
"GraphStore::relationships_with_weight_above",
);
let rels: Vec<Relationship> = inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.filter(|r| r.weight > threshold)
.cloned()
.collect();
Ok(rels)
}
pub fn entity_with_most_properties(&self) -> Result<Option<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_with_most_properties");
let entity = inner.entities.values().max_by(|a, b| {
a.properties
.len()
.cmp(&b.properties.len())
.then_with(|| b.id.0.cmp(&a.id.0))
});
Ok(entity.cloned())
}
pub fn avg_weight(&self) -> Result<Option<f64>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::avg_weight");
let all: Vec<f32> = inner
.adjacency
.values()
.flat_map(|rels| rels.iter().map(|r| r.weight))
.collect();
if all.is_empty() {
return Ok(None);
}
let sum: f64 = all.iter().map(|&w| w as f64).sum();
Ok(Some(sum / all.len() as f64))
}
pub fn entity_count_with_label(&self, label: &str) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_count_with_label");
Ok(inner.entities.values().filter(|e| e.label == label).count())
}
pub fn edge_count_above_weight(&self, threshold: f32) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::edge_count_above_weight");
Ok(inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.filter(|r| r.weight > threshold)
.count())
}
pub fn entities_with_label_prefix(&self, prefix: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_label_prefix");
let entities: Vec<Entity> = inner
.entities
.values()
.filter(|e| e.label.starts_with(prefix))
.cloned()
.collect();
Ok(entities)
}
pub fn bidirectional_pairs(&self) -> Result<Vec<(EntityId, EntityId)>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::bidirectional_pairs");
let mut pairs: Vec<(EntityId, EntityId)> = Vec::new();
for (from, rels) in &inner.adjacency {
for rel in rels {
let to = &rel.to;
if from < to {
if inner
.adjacency
.get(to)
.map_or(false, |v| v.iter().any(|r| &r.to == from))
{
pairs.push((from.clone(), to.clone()));
}
}
}
}
Ok(pairs)
}
pub fn mean_in_degree(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::mean_in_degree");
let entity_count = inner.entities.len();
if entity_count == 0 {
return Ok(0.0);
}
let total: usize = inner.reverse_adjacency.values().map(|v| v.len()).sum();
Ok(total as f64 / entity_count as f64)
}
pub fn entity_count_by_label_prefix(&self, prefix: &str) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(
self.inner.lock(),
"GraphStore::entity_count_by_label_prefix",
);
Ok(inner.entities.values().filter(|e| e.label.starts_with(prefix)).count())
}
pub fn relationship_weight_sum(&self) -> Result<f32, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_weight_sum");
Ok(inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.map(|r| r.weight)
.sum())
}
pub fn label_frequency(&self) -> Result<std::collections::HashMap<String, usize>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::label_frequency");
let mut freq: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for entity in inner.entities.values() {
*freq.entry(entity.label.clone()).or_insert(0) += 1;
}
Ok(freq)
}
pub fn entities_sorted_by_id(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_sorted_by_id");
let mut entities: Vec<Entity> = inner.entities.values().cloned().collect();
entities.sort_unstable_by(|a, b| a.id.cmp(&b.id));
Ok(entities)
}
pub fn entities_with_label_containing(&self, substr: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_label_containing");
Ok(inner.entities.values().filter(|e| e.label.contains(substr)).cloned().collect())
}
pub fn entity_neighbor_count(&self, entity_id: &EntityId) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_neighbor_count");
Ok(inner.adjacency.get(entity_id).map_or(0, |rels| rels.len()))
}
pub fn entity_labels_sorted(&self) -> Result<Vec<String>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_labels_sorted");
let mut labels: Vec<String> = inner
.entities
.values()
.map(|e| e.label.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
labels.sort_unstable();
Ok(labels)
}
pub fn relationship_type_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_type_count");
let kinds: std::collections::HashSet<&str> = inner
.adjacency
.values()
.flat_map(|rels| rels.iter().map(|r| r.kind.as_str()))
.collect();
Ok(kinds.len())
}
pub fn entities_with_exact_label(&self, label: &str) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_exact_label");
Ok(inner.entities.values().filter(|e| e.label == label).cloned().collect())
}
pub fn entity_ids_sorted(&self) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_ids_sorted");
let mut ids: Vec<EntityId> = inner.entities.keys().cloned().collect();
ids.sort_by(|a, b| a.0.cmp(&b.0));
Ok(ids)
}
pub fn relationship_count_for(
&self,
entity_id: &EntityId,
) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::relationship_count_for");
Ok(inner
.adjacency
.get(entity_id)
.map_or(0, |rels| rels.len()))
}
pub fn entity_pair_has_relationship(
&self,
from: &EntityId,
to: &EntityId,
) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_pair_has_relationship");
let to_id = to.clone();
Ok(inner
.adjacency
.get(from)
.map_or(false, |rels| rels.iter().any(|r| r.to == to_id)))
}
pub fn nodes_reachable_from(
&self,
start: &EntityId,
) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::nodes_reachable_from");
let mut visited: std::collections::HashSet<EntityId> = std::collections::HashSet::new();
let mut queue: std::collections::VecDeque<EntityId> = std::collections::VecDeque::new();
queue.push_back(start.clone());
while let Some(current) = queue.pop_front() {
if !visited.insert(current.clone()) {
continue;
}
if let Some(rels) = inner.adjacency.get(¤t) {
for r in rels {
if !visited.contains(&r.to) {
queue.push_back(r.to.clone());
}
}
}
}
visited.remove(start);
let mut result: Vec<EntityId> = visited.into_iter().collect();
result.sort_by(|a, b| a.0.cmp(&b.0));
Ok(result)
}
pub fn average_weight(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::average_weight");
let all: Vec<f64> = inner
.adjacency
.values()
.flat_map(|rels| rels.iter().map(|r| r.weight as f64))
.collect();
if all.is_empty() {
return Ok(0.0);
}
Ok(all.iter().sum::<f64>() / all.len() as f64)
}
pub fn max_weight(&self) -> Result<Option<f64>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::max_weight");
Ok(inner
.adjacency
.values()
.flat_map(|rels| rels.iter().map(|r| r.weight as f64))
.reduce(f64::max))
}
pub fn edge_weight_between(
&self,
from: &EntityId,
to: &EntityId,
) -> Result<Option<f64>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::edge_weight_between");
let to_id = to.clone();
Ok(inner
.adjacency
.get(from)
.and_then(|rels| rels.iter().find(|r| r.to == to_id))
.map(|r| r.weight as f64))
}
pub fn total_relationship_weight(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::total_relationship_weight");
Ok(inner
.adjacency
.values()
.flat_map(|rels| rels.iter().map(|r| r.weight as f64))
.sum())
}
pub fn avg_weight_for_kind(&self, kind: &str) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::avg_weight_for_kind");
let weights: Vec<f64> = inner
.adjacency
.values()
.flat_map(|rels| {
rels.iter()
.filter(|r| r.kind.as_str() == kind)
.map(|r| r.weight as f64)
})
.collect();
if weights.is_empty() {
return Ok(0.0);
}
Ok(weights.iter().sum::<f64>() / weights.len() as f64)
}
pub fn entity_degree_ratio(&self, entity_id: &EntityId) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_degree_ratio");
let total = inner.entities.len();
if total == 0 {
return Ok(0.0);
}
let out_deg = inner
.adjacency
.get(entity_id)
.map_or(0, |rels| rels.len());
Ok(out_deg as f64 / total as f64)
}
pub fn nodes_with_no_outgoing(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::nodes_with_no_outgoing");
Ok(inner
.entities
.values()
.filter(|e| {
inner
.adjacency
.get(&e.id)
.map_or(true, |rels| rels.is_empty())
})
.cloned()
.collect())
}
pub fn in_degree_of(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::in_degree_of");
let count = inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.filter(|r| &r.to == id)
.count();
Ok(count)
}
pub fn total_weight_for_kind(&self, kind: &str) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::total_weight_for_kind");
let sum: f64 = inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.filter(|r| r.kind == kind)
.map(|r| r.weight as f64)
.sum();
Ok(sum)
}
pub fn entity_ids_with_label(&self, label: &str) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_ids_with_label");
Ok(inner
.entities
.values()
.filter(|e| e.label == label)
.map(|e| e.id.clone())
.collect())
}
pub fn labels_unique_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::labels_unique_count");
let labels: std::collections::HashSet<&str> =
inner.entities.values().map(|e| e.label.as_str()).collect();
Ok(labels.len())
}
pub fn entities_with_property_key(
&self,
property_key: &str,
) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_property_key");
Ok(inner
.entities
.values()
.filter(|e| e.properties.contains_key(property_key))
.cloned()
.collect())
}
pub fn entity_properties_count(&self, property_key: &str) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_properties_count");
Ok(inner
.entities
.values()
.filter(|e| e.properties.contains_key(property_key))
.count())
}
pub fn edges_to(&self, id: &EntityId) -> Result<Vec<Relationship>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::edges_to");
Ok(inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.filter(|r| &r.to == id)
.cloned()
.collect())
}
pub fn entity_has_property_value(
&self,
id: &EntityId,
key: &str,
value: &str,
) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_has_property_value");
Ok(inner
.entities
.get(id)
.and_then(|e| e.properties.get(key))
.map_or(false, |v| v == value))
}
pub fn relationships_from(&self, id: &EntityId) -> Result<Vec<Relationship>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::relationships_from");
Ok(inner
.adjacency
.get(id)
.map(|rels| rels.clone())
.unwrap_or_default())
}
pub fn entities_with_min_out_degree(
&self,
min_degree: usize,
) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_min_out_degree");
let entities: Vec<Entity> = inner
.entities
.values()
.filter(|e| {
inner
.adjacency
.get(&e.id)
.map_or(0, |rels| rels.len())
>= min_degree
})
.cloned()
.collect();
Ok(entities)
}
pub fn entities_with_min_in_degree(
&self,
min_degree: usize,
) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_min_in_degree");
let mut in_degree: std::collections::HashMap<&EntityId, usize> =
std::collections::HashMap::new();
for rels in inner.adjacency.values() {
for rel in rels {
*in_degree.entry(&rel.to).or_insert(0) += 1;
}
}
let entities: Vec<Entity> = inner
.entities
.values()
.filter(|e| in_degree.get(&e.id).copied().unwrap_or(0) >= min_degree)
.cloned()
.collect();
Ok(entities)
}
pub fn total_property_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::total_property_count");
Ok(inner.entities.values().map(|e| e.property_count()).sum())
}
pub fn entities_with_no_properties(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner =
recover_lock(self.inner.lock(), "GraphStore::entities_with_no_properties");
Ok(inner
.entities
.values()
.filter(|e| e.properties_is_empty())
.cloned()
.collect())
}
pub fn weight_above_threshold_ratio(
&self,
threshold: f32,
) -> Result<f64, AgentRuntimeError> {
let inner =
recover_lock(self.inner.lock(), "GraphStore::weight_above_threshold_ratio");
let all: Vec<f32> = inner
.adjacency
.values()
.flat_map(|rels| rels.iter().map(|r| r.weight))
.collect();
if all.is_empty() {
return Ok(0.0);
}
let above = all.iter().filter(|&&w| w > threshold).count();
Ok(above as f64 / all.len() as f64)
}
pub fn entities_sorted_by_out_degree(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner =
recover_lock(self.inner.lock(), "GraphStore::entities_sorted_by_out_degree");
let mut pairs: Vec<(Entity, usize)> = inner
.entities
.values()
.map(|e| {
let degree = inner
.adjacency
.get(&e.id)
.map_or(0, |rels| rels.len());
(e.clone(), degree)
})
.collect();
pairs.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.id.as_str().cmp(b.0.id.as_str())));
Ok(pairs.into_iter().map(|(e, _)| e).collect())
}
pub fn entity_by_label(&self, label: &str) -> Result<Option<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_by_label");
Ok(inner.entities.values().find(|e| e.label == label).cloned())
}
pub fn distinct_relationship_kind_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(
self.inner.lock(),
"GraphStore::distinct_relationship_kind_count",
);
let kinds: std::collections::HashSet<&str> = inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.map(|r| r.kind.as_str())
.collect();
Ok(kinds.len())
}
pub fn has_entity_with_label(&self, label: &str) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::has_entity_with_label");
Ok(inner.entities.values().any(|e| e.label == label))
}
pub fn min_weight(&self) -> Result<Option<f32>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::min_weight");
let min = inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.map(|r| r.weight)
.reduce(f32::min);
Ok(min)
}
pub fn relationships_of_kind_count(&self, kind: &str) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(
self.inner.lock(),
"GraphStore::relationships_of_kind_count",
);
let count = inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.filter(|r| r.kind == kind)
.count();
Ok(count)
}
pub fn relationship_count_for_entity(
&self,
entity_id: &EntityId,
) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(
self.inner.lock(),
"GraphStore::relationship_count_for_entity",
);
Ok(inner
.adjacency
.get(entity_id)
.map_or(0, |rels| rels.len()))
}
pub fn entities_by_label(
&self,
label: &str,
) -> Result<Vec<EntityId>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_by_label");
Ok(inner
.entities
.values()
.filter(|e| e.label == label)
.map(|e| e.id.clone())
.collect())
}
pub fn edge_count_between(
&self,
from_id: &EntityId,
to_id: &EntityId,
) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::edge_count_between");
Ok(inner
.adjacency
.get(from_id)
.map_or(0, |rels| rels.iter().filter(|r| &r.to == to_id).count()))
}
pub fn is_connected(
&self,
from_id: &EntityId,
to_id: &EntityId,
) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::is_connected");
Ok(inner
.adjacency
.get(from_id)
.map_or(false, |rels| rels.iter().any(|r| &r.to == to_id)))
}
pub fn graph_is_empty(&self) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::graph_is_empty");
Ok(inner.entities.is_empty() && inner.adjacency.is_empty())
}
pub fn unique_relationship_kinds(&self) -> Result<Vec<String>, AgentRuntimeError> {
let inner =
recover_lock(self.inner.lock(), "GraphStore::unique_relationship_kinds");
let mut kinds: Vec<String> = inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.map(|r| r.kind.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
kinds.sort_unstable();
Ok(kinds)
}
pub fn has_any_relationships(&self) -> Result<bool, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::has_any_relationships");
Ok(inner.adjacency.values().any(|rels| !rels.is_empty()))
}
pub fn avg_edge_weight(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::avg_edge_weight");
let rels: Vec<&crate::graph::Relationship> = inner
.adjacency
.values()
.flat_map(|v| v.iter())
.collect();
if rels.is_empty() {
return Ok(0.0);
}
let total: f64 = rels.iter().map(|r| r.weight as f64).sum();
Ok(total / rels.len() as f64)
}
pub fn entities_with_incoming(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entities_with_incoming");
let mut has_in: std::collections::HashSet<&EntityId> =
std::collections::HashSet::new();
for rels in inner.adjacency.values() {
for r in rels {
has_in.insert(&r.to);
}
}
Ok(inner
.entities
.values()
.filter(|e| has_in.contains(&e.id))
.cloned()
.collect())
}
pub fn entities_with_no_relationships(&self) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(
self.inner.lock(),
"GraphStore::entities_with_no_relationships",
);
Ok(inner
.entities
.values()
.filter(|e| {
inner
.adjacency
.get(&e.id)
.map_or(true, |rels| rels.is_empty())
})
.cloned()
.collect())
}
pub fn total_edge_weight(&self) -> Result<f64, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::total_edge_weight");
Ok(inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.map(|r| r.weight as f64)
.sum())
}
pub fn entity_with_max_out_degree(&self) -> Result<Option<Entity>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::entity_with_max_out_degree");
Ok(inner
.entities
.values()
.max_by_key(|e| {
inner
.adjacency
.get(&e.id)
.map_or(0, |rels| rels.len())
})
.cloned())
}
pub fn self_loops(&self) -> Result<Vec<Relationship>, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::self_loops");
let loops: Vec<Relationship> = inner
.adjacency
.iter()
.flat_map(|(from, rels)| {
let from = from.clone();
rels.iter()
.filter(move |r| r.to == from)
.cloned()
.collect::<Vec<_>>()
})
.collect();
Ok(loops)
}
pub fn entities_with_label_and_property(
&self,
label: &str,
key: &str,
) -> Result<Vec<Entity>, AgentRuntimeError> {
let inner = recover_lock(
self.inner.lock(),
"GraphStore::entities_with_label_and_property",
);
Ok(inner
.entities
.values()
.filter(|e| e.label == label && e.properties.contains_key(key))
.cloned()
.collect())
}
pub fn entity_has_outgoing_edge(
&self,
id: &EntityId,
) -> Result<bool, AgentRuntimeError> {
let inner =
recover_lock(self.inner.lock(), "GraphStore::entity_has_outgoing_edge");
Ok(inner
.adjacency
.get(id)
.map_or(false, |rels| !rels.is_empty()))
}
pub fn count_nodes_with_self_loop(&self) -> Result<usize, AgentRuntimeError> {
let inner =
recover_lock(self.inner.lock(), "GraphStore::count_nodes_with_self_loop");
let count = inner
.adjacency
.iter()
.filter(|(from, rels)| rels.iter().any(|r| &r.to == *from))
.count();
Ok(count)
}
pub fn cycle_count(&self) -> Result<usize, AgentRuntimeError> {
let inner = recover_lock(self.inner.lock(), "GraphStore::cycle_count");
let count = inner
.adjacency
.iter()
.flat_map(|(from, rels)| rels.iter().map(move |r| (from, r)))
.filter(|(from, r)| &r.to == *from)
.count();
Ok(count)
}
pub fn out_degree_of(&self, id: &EntityId) -> Result<usize, AgentRuntimeError> {
let inner =
recover_lock(self.inner.lock(), "GraphStore::out_degree_of");
Ok(inner.adjacency.get(id).map_or(0, |rels| rels.len()))
}
pub fn has_relationship_with_kind(
&self,
kind: &str,
) -> Result<bool, AgentRuntimeError> {
let inner =
recover_lock(self.inner.lock(), "GraphStore::has_relationship_with_kind");
Ok(inner
.adjacency
.values()
.flat_map(|rels| rels.iter())
.any(|r| r.kind == kind))
}
pub fn all_entities_have_properties(&self) -> Result<bool, AgentRuntimeError> {
let inner =
recover_lock(self.inner.lock(), "GraphStore::all_entities_have_properties");
Ok(inner.entities.values().all(|e| !e.properties.is_empty()))
}
}
impl Default for GraphStore {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_graph() -> GraphStore {
GraphStore::new()
}
fn add(g: &GraphStore, id: &str) {
g.add_entity(Entity::new(id, "Node")).unwrap();
}
fn link(g: &GraphStore, from: &str, to: &str) {
g.add_relationship(Relationship::new(from, to, "CONNECTS", 1.0))
.unwrap();
}
fn link_w(g: &GraphStore, from: &str, to: &str, weight: f32) {
g.add_relationship(Relationship::new(from, to, "CONNECTS", weight))
.unwrap();
}
#[test]
fn test_entity_id_equality() {
assert_eq!(EntityId::new("a"), EntityId::new("a"));
assert_ne!(EntityId::new("a"), EntityId::new("b"));
}
#[test]
fn test_entity_id_display() {
let id = EntityId::new("hello");
assert_eq!(id.to_string(), "hello");
}
#[test]
fn test_entity_new_has_empty_properties() {
let e = Entity::new("e1", "Person");
assert!(e.properties.is_empty());
}
#[test]
fn test_entity_with_properties_stores_props() {
let mut props = HashMap::new();
props.insert("age".into(), Value::Number(42.into()));
let e = Entity::with_properties("e1", "Person", props);
assert!(e.properties.contains_key("age"));
}
#[test]
fn test_graph_add_entity_increments_count() {
let g = make_graph();
add(&g, "a");
assert_eq!(g.entity_count().unwrap(), 1);
}
#[test]
fn test_graph_get_entity_returns_entity() {
let g = make_graph();
g.add_entity(Entity::new("e1", "Person")).unwrap();
let e = g.get_entity(&EntityId::new("e1")).unwrap();
assert_eq!(e.label, "Person");
}
#[test]
fn test_graph_get_entity_missing_returns_error() {
let g = make_graph();
assert!(g.get_entity(&EntityId::new("ghost")).is_err());
}
#[test]
fn test_graph_add_relationship_increments_count() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
link(&g, "a", "b");
assert_eq!(g.relationship_count().unwrap(), 1);
}
#[test]
fn test_graph_add_relationship_missing_source_fails() {
let g = make_graph();
add(&g, "b");
let result = g.add_relationship(Relationship::new("ghost", "b", "X", 1.0));
assert!(result.is_err());
}
#[test]
fn test_graph_add_relationship_missing_target_fails() {
let g = make_graph();
add(&g, "a");
let result = g.add_relationship(Relationship::new("a", "ghost", "X", 1.0));
assert!(result.is_err());
}
#[test]
fn test_graph_remove_entity_removes_relationships() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
link(&g, "a", "b");
g.remove_entity(&EntityId::new("a")).unwrap();
assert_eq!(g.entity_count().unwrap(), 1);
assert_eq!(g.relationship_count().unwrap(), 0);
}
#[test]
fn test_graph_remove_entity_missing_returns_error() {
let g = make_graph();
assert!(g.remove_entity(&EntityId::new("ghost")).is_err());
}
#[test]
fn test_bfs_finds_direct_neighbours() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
add(&g, "c");
link(&g, "a", "b");
link(&g, "a", "c");
let visited = g.bfs(&EntityId::new("a")).unwrap();
assert_eq!(visited.len(), 2);
}
#[test]
fn test_bfs_traverses_chain() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
add(&g, "c");
add(&g, "d");
link(&g, "a", "b");
link(&g, "b", "c");
link(&g, "c", "d");
let visited = g.bfs(&EntityId::new("a")).unwrap();
assert_eq!(visited.len(), 3);
assert_eq!(visited[0], EntityId::new("b"));
}
#[test]
fn test_bfs_handles_isolated_node() {
let g = make_graph();
add(&g, "a");
let visited = g.bfs(&EntityId::new("a")).unwrap();
assert!(visited.is_empty());
}
#[test]
fn test_bfs_missing_start_returns_error() {
let g = make_graph();
assert!(g.bfs(&EntityId::new("ghost")).is_err());
}
#[test]
fn test_dfs_visits_all_reachable_nodes() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
add(&g, "c");
add(&g, "d");
link(&g, "a", "b");
link(&g, "a", "c");
link(&g, "b", "d");
let visited = g.dfs(&EntityId::new("a")).unwrap();
assert_eq!(visited.len(), 3);
}
#[test]
fn test_dfs_handles_isolated_node() {
let g = make_graph();
add(&g, "a");
let visited = g.dfs(&EntityId::new("a")).unwrap();
assert!(visited.is_empty());
}
#[test]
fn test_dfs_missing_start_returns_error() {
let g = make_graph();
assert!(g.dfs(&EntityId::new("ghost")).is_err());
}
#[test]
fn test_shortest_path_direct_connection() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
link(&g, "a", "b");
let path = g
.shortest_path(&EntityId::new("a"), &EntityId::new("b"))
.unwrap();
assert_eq!(path, Some(vec![EntityId::new("a"), EntityId::new("b")]));
}
#[test]
fn test_shortest_path_multi_hop() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
add(&g, "c");
link(&g, "a", "b");
link(&g, "b", "c");
let path = g
.shortest_path(&EntityId::new("a"), &EntityId::new("c"))
.unwrap();
assert_eq!(path.as_ref().map(|p| p.len()), Some(3));
}
#[test]
fn test_shortest_path_returns_none_for_disconnected() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
let path = g
.shortest_path(&EntityId::new("a"), &EntityId::new("b"))
.unwrap();
assert_eq!(path, None);
}
#[test]
fn test_shortest_path_same_node_returns_single_element() {
let g = make_graph();
add(&g, "a");
let path = g
.shortest_path(&EntityId::new("a"), &EntityId::new("a"))
.unwrap();
assert_eq!(path, Some(vec![EntityId::new("a")]));
}
#[test]
fn test_shortest_path_missing_source_returns_error() {
let g = make_graph();
add(&g, "b");
assert!(g
.shortest_path(&EntityId::new("ghost"), &EntityId::new("b"))
.is_err());
}
#[test]
fn test_shortest_path_missing_target_returns_error() {
let g = make_graph();
add(&g, "a");
assert!(g
.shortest_path(&EntityId::new("a"), &EntityId::new("ghost"))
.is_err());
}
#[test]
fn test_transitive_closure_includes_start() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
link(&g, "a", "b");
let closure = g.transitive_closure(&EntityId::new("a")).unwrap();
assert!(closure.contains(&EntityId::new("a")));
assert!(closure.contains(&EntityId::new("b")));
}
#[test]
fn test_transitive_closure_isolated_node_contains_only_self() {
let g = make_graph();
add(&g, "a");
let closure = g.transitive_closure(&EntityId::new("a")).unwrap();
assert_eq!(closure.len(), 1);
}
#[test]
fn test_mem_graph_error_converts_to_runtime_error() {
let e = MemGraphError::EntityNotFound("x".into());
let re: AgentRuntimeError = e.into();
assert!(matches!(re, AgentRuntimeError::Graph(_)));
}
#[test]
fn test_shortest_path_weighted_simple() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
add(&g, "c");
link_w(&g, "a", "b", 1.0);
link_w(&g, "b", "c", 2.0);
g.add_relationship(Relationship::new("a", "c", "DIRECT", 10.0))
.unwrap();
let result = g
.shortest_path_weighted(&EntityId::new("a"), &EntityId::new("c"))
.unwrap();
assert!(result.is_some());
let (path, weight) = result.unwrap();
assert_eq!(
path,
vec![EntityId::new("a"), EntityId::new("b"), EntityId::new("c")]
);
assert!((weight - 3.0).abs() < 1e-5);
}
#[test]
fn test_shortest_path_weighted_returns_none_for_disconnected() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
let result = g
.shortest_path_weighted(&EntityId::new("a"), &EntityId::new("b"))
.unwrap();
assert!(result.is_none());
}
#[test]
fn test_shortest_path_weighted_same_node() {
let g = make_graph();
add(&g, "a");
let result = g
.shortest_path_weighted(&EntityId::new("a"), &EntityId::new("a"))
.unwrap();
assert_eq!(result, Some((vec![EntityId::new("a")], 0.0)));
}
#[test]
fn test_shortest_path_weighted_negative_weight_errors() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
g.add_relationship(Relationship::new("a", "b", "NEG", -1.0))
.unwrap();
let result = g.shortest_path_weighted(&EntityId::new("a"), &EntityId::new("b"));
assert!(result.is_err());
}
#[test]
fn test_degree_centrality_basic() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
add(&g, "c");
add(&g, "d");
link(&g, "a", "b");
link(&g, "a", "c");
link(&g, "a", "d");
let centrality = g.degree_centrality().unwrap();
let a_cent = *centrality.get(&EntityId::new("a")).unwrap();
let b_cent = *centrality.get(&EntityId::new("b")).unwrap();
assert!((a_cent - 1.0).abs() < 1e-5, "a centrality was {a_cent}");
assert!(
(b_cent - 1.0 / 3.0).abs() < 1e-5,
"b centrality was {b_cent}"
);
}
#[test]
fn test_betweenness_centrality_chain() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
add(&g, "c");
add(&g, "d");
link(&g, "a", "b");
link(&g, "b", "c");
link(&g, "c", "d");
let centrality = g.betweenness_centrality().unwrap();
let a_cent = *centrality.get(&EntityId::new("a")).unwrap();
let b_cent = *centrality.get(&EntityId::new("b")).unwrap();
let c_cent = *centrality.get(&EntityId::new("c")).unwrap();
let d_cent = *centrality.get(&EntityId::new("d")).unwrap();
assert!((a_cent).abs() < 1e-5, "expected a_cent ~ 0, got {a_cent}");
assert!(b_cent > 0.0, "expected b_cent > 0, got {b_cent}");
assert!(c_cent > 0.0, "expected c_cent > 0, got {c_cent}");
assert!((d_cent).abs() < 1e-5, "expected d_cent ~ 0, got {d_cent}");
}
#[test]
fn test_label_propagation_communities_two_clusters() {
let g = make_graph();
for id in &["a", "b", "c", "x", "y", "z"] {
add(&g, id);
}
link(&g, "a", "b");
link(&g, "b", "a");
link(&g, "b", "c");
link(&g, "c", "b");
link(&g, "a", "c");
link(&g, "c", "a");
link(&g, "x", "y");
link(&g, "y", "x");
link(&g, "y", "z");
link(&g, "z", "y");
link(&g, "x", "z");
link(&g, "z", "x");
let communities = g.label_propagation_communities(100).unwrap();
let label_a = communities[&EntityId::new("a")];
let label_b = communities[&EntityId::new("b")];
let label_c = communities[&EntityId::new("c")];
let label_x = communities[&EntityId::new("x")];
let label_y = communities[&EntityId::new("y")];
let label_z = communities[&EntityId::new("z")];
assert_eq!(label_a, label_b, "a and b should be in same community");
assert_eq!(label_b, label_c, "b and c should be in same community");
assert_eq!(label_x, label_y, "x and y should be in same community");
assert_eq!(label_y, label_z, "y and z should be in same community");
assert_ne!(
label_a, label_x,
"cluster 1 and cluster 2 should be different communities"
);
}
#[test]
fn test_subgraph_extracts_correct_nodes_and_edges() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
add(&g, "c");
add(&g, "d");
link(&g, "a", "b");
link(&g, "b", "c");
link(&g, "c", "d");
let sub = g
.subgraph(&[EntityId::new("a"), EntityId::new("b"), EntityId::new("c")])
.unwrap();
assert_eq!(sub.entity_count().unwrap(), 3);
assert_eq!(sub.relationship_count().unwrap(), 2);
assert!(sub.get_entity(&EntityId::new("d")).is_err());
let path = sub
.shortest_path(&EntityId::new("a"), &EntityId::new("c"))
.unwrap();
assert!(path.is_some());
assert_eq!(path.unwrap().len(), 3);
}
#[test]
fn test_detect_cycles_dag_returns_false() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
add(&g, "c");
link(&g, "a", "b");
link(&g, "b", "c");
assert_eq!(g.detect_cycles().unwrap(), false);
}
#[test]
fn test_detect_cycles_self_loop_returns_true() {
let g = make_graph();
add(&g, "a");
g.add_relationship(Relationship::new("a", "a", "SELF", 1.0))
.unwrap();
assert_eq!(g.detect_cycles().unwrap(), true);
}
#[test]
fn test_detect_cycles_simple_cycle_returns_true() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
link(&g, "a", "b");
g.add_relationship(Relationship::new("b", "a", "BACK", 1.0))
.unwrap();
assert_eq!(g.detect_cycles().unwrap(), true);
}
#[test]
fn test_detect_cycles_empty_graph_returns_false() {
let g = make_graph();
assert_eq!(g.detect_cycles().unwrap(), false);
}
#[test]
fn test_detect_cycles_result_is_cached() {
let g = make_graph();
add(&g, "x");
add(&g, "y");
link(&g, "x", "y");
let r1 = g.detect_cycles().unwrap();
let r2 = g.detect_cycles().unwrap();
assert_eq!(r1, r2);
}
#[test]
fn test_detect_cycles_cache_invalidated_on_mutation() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
link(&g, "a", "b");
assert_eq!(g.detect_cycles().unwrap(), false);
g.add_relationship(Relationship::new("b", "a", "BACK", 1.0))
.unwrap();
assert_eq!(
g.detect_cycles().unwrap(),
true,
"cache should be invalidated after adding a back edge"
);
}
#[test]
fn test_bfs_bounded_respects_max_depth() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
add(&g, "c");
add(&g, "d");
link(&g, "a", "b");
link(&g, "b", "c");
link(&g, "c", "d");
let visited = g.bfs_bounded("a", 1, 100).unwrap();
assert!(visited.contains(&EntityId::new("a")));
assert!(visited.contains(&EntityId::new("b")));
assert!(!visited.contains(&EntityId::new("c")), "c is at depth 2, should not be visited");
}
#[test]
fn test_path_exists_returns_true() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
add(&g, "c");
link(&g, "a", "b");
link(&g, "b", "c");
assert_eq!(g.path_exists("a", "c").unwrap(), true);
}
#[test]
fn test_path_exists_returns_false() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
assert_eq!(g.path_exists("a", "b").unwrap(), false);
}
#[test]
fn test_entity_id_as_str() {
let id = EntityId::new("my-entity");
assert_eq!(id.as_str(), "my-entity");
}
#[test]
fn test_entity_id_as_ref_str() {
let id = EntityId::new("asref-test");
let s: &str = id.as_ref();
assert_eq!(s, "asref-test");
}
#[test]
fn test_dfs_bounded_respects_max_nodes() {
let g = make_graph();
add(&g, "a");
add(&g, "b");
add(&g, "c");
add(&g, "d");
link(&g, "a", "b");
link(&g, "b", "c");
link(&g, "c", "d");
let visited = g.dfs_bounded("a", 100, 2).unwrap();
assert_eq!(visited.len(), 2, "should stop at 2 nodes");
}
#[test]
fn test_entity_exists_and_relationship_exists() {
let g = GraphStore::new();
let a = EntityId::new("a");
let b = EntityId::new("b");
assert!(!g.entity_exists(&a).unwrap());
g.add_entity(Entity::new("a", "Node")).unwrap();
assert!(g.entity_exists(&a).unwrap());
assert!(!g.relationship_exists(&a, &b, "knows").unwrap());
g.add_entity(Entity::new("b", "Node")).unwrap();
g.add_relationship(Relationship::new("a", "b", "knows", 1.0)).unwrap();
assert!(g.relationship_exists(&a, &b, "knows").unwrap());
assert!(!g.relationship_exists(&a, &b, "likes").unwrap());
}
#[test]
fn test_get_relationships_for_returns_outgoing() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
let a = EntityId::new("a");
g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
let rels = g.get_relationships_for(&a).unwrap();
assert_eq!(rels.len(), 2);
}
#[test]
fn test_relationships_between_finds_both_directions() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "N")).unwrap();
g.add_entity(Entity::new("y", "N")).unwrap();
let x = EntityId::new("x");
let y = EntityId::new("y");
g.add_relationship(Relationship::new("x", "y", "follows", 1.0)).unwrap();
g.add_relationship(Relationship::new("y", "x", "blocks", 1.0)).unwrap();
let rels = g.relationships_between(&x, &y).unwrap();
assert_eq!(rels.len(), 2);
}
#[test]
fn test_find_entities_by_property() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person").with_property("age", serde_json::json!(30))).unwrap();
g.add_entity(Entity::new("b", "Person").with_property("age", serde_json::json!(25))).unwrap();
g.add_entity(Entity::new("c", "Person").with_property("age", serde_json::json!(30))).unwrap();
let found = g.find_entities_by_property("age", &serde_json::json!(30)).unwrap();
assert_eq!(found.len(), 2);
let ids: Vec<_> = found.iter().map(|e| e.id.as_str()).collect();
assert!(ids.contains(&"a") && ids.contains(&"c"));
}
#[test]
fn test_neighbor_entities_returns_entity_objects() {
let g = GraphStore::new();
g.add_entity(Entity::new("root", "R")).unwrap();
g.add_entity(Entity::new("child1", "C")).unwrap();
g.add_entity(Entity::new("child2", "C")).unwrap();
let root = EntityId::new("root");
g.add_relationship(Relationship::new("root", "child1", "has", 1.0)).unwrap();
g.add_relationship(Relationship::new("root", "child2", "has", 1.0)).unwrap();
let neighbors = g.neighbor_entities(&root).unwrap();
assert_eq!(neighbors.len(), 2);
let labels: Vec<_> = neighbors.iter().map(|e| e.label.as_str()).collect();
assert!(labels.iter().all(|l| *l == "C"));
}
#[test]
fn test_remove_all_relationships_for() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
let a = EntityId::new("a");
g.add_relationship(Relationship::new("a", "b", "r1", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "b", "r2", 1.0)).unwrap();
let removed = g.remove_all_relationships_for(&a).unwrap();
assert_eq!(removed, 2);
assert_eq!(g.relationship_count().unwrap(), 0);
}
#[test]
fn test_topological_sort_returns_valid_order() {
let g = GraphStore::new();
for id in &["a", "b", "c", "d"] {
g.add_entity(Entity::new(*id, "N")).unwrap();
}
g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "r", 1.0)).unwrap();
g.add_relationship(Relationship::new("c", "d", "r", 1.0)).unwrap();
let order = g.topological_sort().unwrap();
assert_eq!(order.len(), 4);
let pos: std::collections::HashMap<_, _> = order.iter().enumerate().map(|(i, id)| (id.as_str().to_owned(), i)).collect();
assert!(pos["a"] < pos["b"]);
assert!(pos["b"] < pos["c"]);
assert!(pos["c"] < pos["d"]);
}
#[test]
fn test_topological_sort_rejects_cycle() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "N")).unwrap();
g.add_entity(Entity::new("y", "N")).unwrap();
g.add_relationship(Relationship::new("x", "y", "r", 1.0)).unwrap();
g.add_relationship(Relationship::new("y", "x", "r", 1.0)).unwrap();
assert!(g.topological_sort().is_err());
}
#[test]
fn test_entity_count_by_label() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person")).unwrap();
g.add_entity(Entity::new("b", "Person")).unwrap();
g.add_entity(Entity::new("c", "Organization")).unwrap();
assert_eq!(g.entity_count_by_label("Person").unwrap(), 2);
assert_eq!(g.entity_count_by_label("Organization").unwrap(), 1);
assert_eq!(g.entity_count_by_label("Unknown").unwrap(), 0);
}
#[test]
fn test_update_entity_label_and_property() {
let g = GraphStore::new();
let id = EntityId::new("e1");
g.add_entity(Entity::new("e1", "Old")).unwrap();
assert!(g.update_entity_label(&id, "New").unwrap());
assert_eq!(g.get_entity(&id).unwrap().label, "New");
assert!(g.update_entity_property(&id, "key", serde_json::json!("val")).unwrap());
assert_eq!(g.get_entity(&id).unwrap().properties["key"], serde_json::json!("val"));
}
#[test]
fn test_connected_components_single_node() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
assert_eq!(g.connected_components().unwrap(), 1);
}
#[test]
fn test_connected_components_two_isolated_nodes() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
assert_eq!(g.connected_components().unwrap(), 2);
}
#[test]
fn test_connected_components_connected_pair() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
assert_eq!(g.connected_components().unwrap(), 1);
}
#[test]
fn test_connected_components_two_separate_pairs() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_entity(Entity::new("c", "C")).unwrap();
g.add_entity(Entity::new("d", "D")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
g.add_relationship(Relationship::new("c", "d", "link", 1.0)).unwrap();
assert_eq!(g.connected_components().unwrap(), 2);
}
#[test]
fn test_connected_components_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.connected_components().unwrap(), 0);
}
#[test]
fn test_weakly_connected_true_for_empty_graph() {
let g = GraphStore::new();
assert!(g.weakly_connected().unwrap());
}
#[test]
fn test_weakly_connected_true_for_single_node() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
assert!(g.weakly_connected().unwrap());
}
#[test]
fn test_weakly_connected_true_when_all_nodes_connected() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
assert!(g.weakly_connected().unwrap());
}
#[test]
fn test_weakly_connected_false_when_nodes_isolated() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
assert!(!g.weakly_connected().unwrap());
}
#[test]
fn test_isolates_returns_nodes_with_no_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_entity(Entity::new("c", "C")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
let iso = g.isolates().unwrap();
let mut ids: Vec<String> = iso.iter().map(|e| e.id.as_str().to_string()).collect();
ids.sort();
assert_eq!(ids, vec!["c".to_string()]);
}
#[test]
fn test_isolates_returns_empty_when_all_connected() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
assert!(g.isolates().unwrap().is_empty());
}
#[test]
fn test_isolates_all_isolated() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "X")).unwrap();
g.add_entity(Entity::new("y", "Y")).unwrap();
let iso = g.isolates().unwrap();
let mut ids: Vec<String> = iso.iter().map(|e| e.id.as_str().to_string()).collect();
ids.sort();
assert_eq!(ids, vec!["x".to_string(), "y".to_string()]);
}
#[test]
fn test_is_dag_on_dag() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
assert!(g.is_dag().unwrap());
}
#[test]
fn test_is_dag_on_cyclic_graph() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "a", "back", 1.0)).unwrap();
assert!(!g.is_dag().unwrap());
}
#[test]
fn test_in_degree_and_out_degree() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_entity(Entity::new("c", "C")).unwrap();
g.add_relationship(Relationship::new("a", "b", "e1", 1.0)).unwrap();
g.add_relationship(Relationship::new("c", "b", "e2", 1.0)).unwrap();
let a = EntityId::new("a");
let b = EntityId::new("b");
let c = EntityId::new("c");
assert_eq!(g.out_degree(&a).unwrap(), 1);
assert_eq!(g.in_degree(&a).unwrap(), 0);
assert_eq!(g.in_degree(&b).unwrap(), 2);
assert_eq!(g.out_degree(&b).unwrap(), 0);
assert_eq!(g.out_degree(&c).unwrap(), 1);
assert_eq!(g.in_degree(&c).unwrap(), 0);
}
#[test]
fn test_in_degree_missing_entity_returns_zero() {
let g = GraphStore::new();
let id = EntityId::new("ghost");
assert_eq!(g.in_degree(&id).unwrap(), 0);
}
#[test]
fn test_out_degree_missing_entity_returns_zero() {
let g = GraphStore::new();
let id = EntityId::new("ghost");
assert_eq!(g.out_degree(&id).unwrap(), 0);
}
#[test]
fn test_node_count_is_alias_for_entity_count() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
assert_eq!(g.node_count().unwrap(), g.entity_count().unwrap());
assert_eq!(g.node_count().unwrap(), 2);
}
#[test]
fn test_edge_count_is_alias_for_relationship_count() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
assert_eq!(g.edge_count().unwrap(), g.relationship_count().unwrap());
assert_eq!(g.edge_count().unwrap(), 1);
}
#[test]
fn test_source_nodes_returns_nodes_with_no_incoming_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("root", "Root")).unwrap();
g.add_entity(Entity::new("child", "Child")).unwrap();
g.add_entity(Entity::new("leaf", "Leaf")).unwrap();
g.add_relationship(Relationship::new("root", "child", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("child", "leaf", "e", 1.0)).unwrap();
let sources = g.source_nodes().unwrap();
assert_eq!(sources.len(), 1);
assert_eq!(sources[0].id.as_str(), "root");
}
#[test]
fn test_sink_nodes_returns_nodes_with_no_outgoing_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("root", "Root")).unwrap();
g.add_entity(Entity::new("child", "Child")).unwrap();
g.add_entity(Entity::new("leaf", "Leaf")).unwrap();
g.add_relationship(Relationship::new("root", "child", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("child", "leaf", "e", 1.0)).unwrap();
let sinks = g.sink_nodes().unwrap();
assert_eq!(sinks.len(), 1);
assert_eq!(sinks[0].id.as_str(), "leaf");
}
#[test]
fn test_source_and_sink_empty_on_isolated_node() {
let g = GraphStore::new();
g.add_entity(Entity::new("solo", "Solo")).unwrap();
assert_eq!(g.source_nodes().unwrap().len(), 1);
assert_eq!(g.sink_nodes().unwrap().len(), 1);
}
#[test]
fn test_reverse_flips_all_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
let rev = g.reverse().unwrap();
assert_eq!(rev.entity_count().unwrap(), 2);
assert_eq!(rev.relationship_count().unwrap(), 1);
let b_id = EntityId::new("b");
let a_id = EntityId::new("a");
assert!(rev.relationship_exists(&b_id, &a_id, "edge").unwrap());
}
#[test]
fn test_reverse_empty_graph_stays_empty() {
let g = GraphStore::new();
let rev = g.reverse().unwrap();
assert_eq!(rev.entity_count().unwrap(), 0);
}
#[test]
fn test_common_neighbors_finds_shared_targets() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_entity(Entity::new("shared", "S")).unwrap();
g.add_entity(Entity::new("only_a", "OA")).unwrap();
g.add_relationship(Relationship::new("a", "shared", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "shared", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "only_a", "e", 1.0)).unwrap();
let a_id = EntityId::new("a");
let b_id = EntityId::new("b");
let common = g.common_neighbors(&a_id, &b_id).unwrap();
assert_eq!(common.len(), 1);
assert_eq!(common[0].id.as_str(), "shared");
}
#[test]
fn test_common_neighbors_empty_when_none_shared() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_entity(Entity::new("x", "X")).unwrap();
g.add_entity(Entity::new("y", "Y")).unwrap();
g.add_relationship(Relationship::new("a", "x", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "y", "e", 1.0)).unwrap();
let a_id = EntityId::new("a");
let b_id = EntityId::new("b");
assert!(g.common_neighbors(&a_id, &b_id).unwrap().is_empty());
}
#[test]
fn test_graph_is_empty_initially() {
let g = GraphStore::new();
assert!(g.is_empty().unwrap());
}
#[test]
fn test_graph_is_empty_false_after_add() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
assert!(!g.is_empty().unwrap());
}
#[test]
fn test_graph_entity_ids_returns_all_ids() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "X")).unwrap();
g.add_entity(Entity::new("y", "Y")).unwrap();
let ids = g.entity_ids().unwrap();
assert_eq!(ids.len(), 2);
assert!(ids.iter().any(|id| id.0 == "x"));
assert!(ids.iter().any(|id| id.0 == "y"));
}
#[test]
fn test_graph_clear_removes_entities_and_relationships() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_relationship(Relationship::new("a", "b", "links", 1.0))
.unwrap();
g.clear().unwrap();
assert_eq!(g.entity_count().unwrap(), 0);
assert_eq!(g.relationship_count().unwrap(), 0);
assert!(g.is_empty().unwrap());
}
#[test]
fn test_weight_of_returns_edge_weight() {
let g = make_graph();
add(&g, "x"); add(&g, "y");
link_w(&g, "x", "y", 3.5);
let w = g.weight_of(&EntityId::new("x"), &EntityId::new("y")).unwrap();
assert!(w.is_some());
assert!((w.unwrap() - 3.5).abs() < 1e-6);
}
#[test]
fn test_weight_of_absent_edge_returns_none() {
let g = make_graph();
add(&g, "a"); add(&g, "b");
let w = g.weight_of(&EntityId::new("a"), &EntityId::new("b")).unwrap();
assert!(w.is_none());
}
#[test]
fn test_neighbors_in_returns_predecessors() {
let g = make_graph();
add(&g, "a"); add(&g, "b"); add(&g, "c");
link(&g, "a", "c"); link(&g, "b", "c");
let mut preds: Vec<String> = g
.neighbors_in(&EntityId::new("c"))
.unwrap()
.into_iter()
.map(|id| id.as_str().to_string())
.collect();
preds.sort();
assert_eq!(preds, vec!["a", "b"]);
}
#[test]
fn test_neighbors_in_empty_for_node_with_no_incoming() {
let g = make_graph();
add(&g, "isolated");
let preds = g.neighbors_in(&EntityId::new("isolated")).unwrap();
assert!(preds.is_empty());
}
#[test]
fn test_path_exists_reachable() {
let g = make_graph();
add(&g, "s"); add(&g, "m"); add(&g, "t");
link(&g, "s", "m"); link(&g, "m", "t");
assert!(g.path_exists("s", "t").unwrap());
}
#[test]
fn test_path_exists_unreachable() {
let g = make_graph();
add(&g, "a"); add(&g, "b");
assert!(!g.path_exists("a", "b").unwrap());
}
#[test]
fn test_neighbor_ids_returns_direct_successors() {
let g = make_graph();
add(&g, "src"); add(&g, "dst1"); add(&g, "dst2");
link(&g, "src", "dst1"); link(&g, "src", "dst2");
let mut ids = g.neighbor_ids(&EntityId::new("src")).unwrap();
ids.sort_by_key(|id| id.0.clone());
assert_eq!(ids, vec![EntityId::new("dst1"), EntityId::new("dst2")]);
}
#[test]
fn test_neighbor_ids_empty_for_isolated_node() {
let g = make_graph();
add(&g, "isolated");
let ids = g.neighbor_ids(&EntityId::new("isolated")).unwrap();
assert!(ids.is_empty());
}
#[test]
fn test_density_zero_for_empty_graph() {
let g = make_graph();
assert_eq!(g.density().unwrap(), 0.0);
}
#[test]
fn test_density_zero_for_single_node() {
let g = make_graph();
add(&g, "solo");
assert_eq!(g.density().unwrap(), 0.0);
}
#[test]
fn test_density_one_for_complete_directed_graph() {
let g = make_graph();
add(&g, "a"); add(&g, "b");
link(&g, "a", "b"); link(&g, "b", "a");
assert!((g.density().unwrap() - 1.0).abs() < 1e-9);
}
#[test]
fn test_density_partial() {
let g = make_graph();
add(&g, "a"); add(&g, "b"); add(&g, "c");
link(&g, "a", "b");
let d = g.density().unwrap();
assert!((d - 1.0/6.0).abs() < 1e-9);
}
#[test]
fn test_all_entities_returns_all_nodes() {
let g = make_graph();
add(&g, "x"); add(&g, "y"); add(&g, "z");
assert_eq!(g.all_entities().unwrap().len(), 3);
}
#[test]
fn test_all_relationships_returns_all_edges() {
let g = make_graph();
add(&g, "a"); add(&g, "b"); add(&g, "c");
link(&g, "a", "b"); link(&g, "b", "c");
assert_eq!(g.all_relationships().unwrap().len(), 2);
}
#[test]
fn test_find_entities_by_label_returns_matches() {
let g = make_graph();
g.add_entity(Entity::new("n1", "Person")).unwrap();
g.add_entity(Entity::new("n2", "Person")).unwrap();
g.add_entity(Entity::new("n3", "Car")).unwrap();
let people = g.find_entities_by_label("Person").unwrap();
assert_eq!(people.len(), 2);
}
#[test]
fn test_bfs_bounded_limits_depth() {
let g = make_graph();
add(&g, "a"); add(&g, "b"); add(&g, "c"); add(&g, "d");
link(&g, "a", "b"); link(&g, "b", "c"); link(&g, "c", "d");
let visited = g.bfs_bounded("a", 2, 100).unwrap();
assert!(visited.contains(&EntityId::new("a")));
assert!(visited.contains(&EntityId::new("b")));
assert!(visited.contains(&EntityId::new("c")));
assert!(!visited.contains(&EntityId::new("d")));
}
#[test]
fn test_avg_degree_zero_for_empty_graph() {
let g = make_graph();
assert_eq!(g.avg_degree().unwrap(), 0.0);
}
#[test]
fn test_avg_degree_correct_value() {
let g = make_graph();
add(&g, "a"); add(&g, "b"); add(&g, "c");
link(&g, "a", "b"); link(&g, "a", "c");
let d = g.avg_degree().unwrap();
assert!((d - 2.0/3.0).abs() < 1e-9);
}
#[test]
fn test_total_weight_zero_for_empty_graph() {
let g = make_graph();
assert_eq!(g.total_weight().unwrap(), 0.0);
}
#[test]
fn test_total_weight_sums_all_edges() {
let g = make_graph();
add(&g, "a"); add(&g, "b"); add(&g, "c");
link_w(&g, "a", "b", 2.0); link_w(&g, "b", "c", 3.5);
assert!((g.total_weight().unwrap() - 5.5).abs() < 1e-6);
}
#[test]
fn test_max_edge_weight_none_for_empty() {
let g = make_graph();
assert!(g.max_edge_weight().unwrap().is_none());
}
#[test]
fn test_max_edge_weight_returns_largest() {
let g = make_graph();
add(&g, "a"); add(&g, "b"); add(&g, "c");
link_w(&g, "a", "b", 1.0); link_w(&g, "a", "c", 9.5);
assert!((g.max_edge_weight().unwrap().unwrap() - 9.5).abs() < 1e-6);
}
#[test]
fn test_min_edge_weight_returns_smallest() {
let g = make_graph();
add(&g, "a"); add(&g, "b"); add(&g, "c");
link_w(&g, "a", "b", 1.0); link_w(&g, "a", "c", 9.5);
assert!((g.min_edge_weight().unwrap().unwrap() - 1.0).abs() < 1e-6);
}
#[test]
fn test_max_out_degree_entity_returns_node_with_most_edges() {
let g = make_graph();
add(&g, "hub"); add(&g, "a"); add(&g, "b"); add(&g, "leaf");
link(&g, "hub", "a"); link(&g, "hub", "b"); link(&g, "a", "leaf");
let best = g.max_out_degree_entity().unwrap().unwrap();
assert_eq!(best.id, EntityId::new("hub"));
}
#[test]
fn test_max_out_degree_entity_none_for_empty_graph() {
let g = make_graph();
assert!(g.max_out_degree_entity().unwrap().is_none());
}
#[test]
fn test_leaf_nodes_returns_nodes_with_no_outgoing_edges() {
let g = make_graph();
add(&g, "root"); add(&g, "mid"); add(&g, "leaf1"); add(&g, "leaf2");
link(&g, "root", "mid"); link(&g, "mid", "leaf1"); link(&g, "mid", "leaf2");
let mut leaf_ids: Vec<String> = g
.leaf_nodes()
.unwrap()
.into_iter()
.map(|e| e.id.0.clone())
.collect();
leaf_ids.sort();
assert_eq!(leaf_ids, vec!["leaf1", "leaf2"]);
}
#[test]
fn test_leaf_nodes_all_are_leaves_with_no_edges() {
let g = make_graph();
add(&g, "a"); add(&g, "b");
assert_eq!(g.leaf_nodes().unwrap().len(), 2);
}
#[test]
fn test_top_n_by_out_degree_returns_descending() {
let g = make_graph();
add(&g, "hub"); add(&g, "mid"); add(&g, "tip"); add(&g, "leaf");
link(&g, "hub", "mid"); link(&g, "hub", "tip"); link(&g, "hub", "leaf");
link(&g, "mid", "leaf");
let top2 = g.top_n_by_out_degree(2).unwrap();
assert_eq!(top2.len(), 2);
assert_eq!(top2[0].id, EntityId::new("hub"));
}
#[test]
fn test_top_n_by_out_degree_zero_returns_empty() {
let g = make_graph();
add(&g, "x");
assert!(g.top_n_by_out_degree(0).unwrap().is_empty());
}
#[test]
fn test_remove_entity_and_edges_removes_node_and_incident_edges() {
let g = make_graph();
add(&g, "a"); add(&g, "b"); add(&g, "c");
link(&g, "a", "b"); link(&g, "b", "c");
g.remove_entity_and_edges(&EntityId::new("b")).unwrap();
assert_eq!(g.entity_count().unwrap(), 2);
assert_eq!(g.relationship_count().unwrap(), 0);
}
#[test]
fn test_remove_entity_and_edges_errors_for_unknown_id() {
let g = make_graph();
let result = g.remove_entity_and_edges(&EntityId::new("ghost"));
assert!(result.is_err());
}
#[test]
fn test_relationship_kinds_returns_sorted_distinct_kinds() {
let g = make_graph();
add(&g, "a"); add(&g, "b"); add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "LIKES", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "KNOWS", 1.0)).unwrap();
let kinds = g.relationship_kinds().unwrap();
assert_eq!(kinds, vec!["KNOWS", "LIKES"]);
}
#[test]
fn test_relationship_kinds_empty_graph_returns_empty() {
let g = make_graph();
assert!(g.relationship_kinds().unwrap().is_empty());
}
#[test]
fn test_graph_density_zero_for_empty() {
let g = make_graph();
assert_eq!(g.graph_density().unwrap(), 0.0);
}
#[test]
fn test_graph_density_correct_for_partial_graph() {
let g = make_graph();
add(&g, "a"); add(&g, "b"); add(&g, "c");
link(&g, "a", "b");
let d = g.graph_density().unwrap();
assert!((d - 1.0 / 6.0).abs() < 1e-9);
}
#[test]
fn test_entity_id_try_new_rejects_empty() {
let result = EntityId::try_new("");
assert!(result.is_err());
}
#[test]
fn test_entity_id_try_new_accepts_nonempty() {
let id = EntityId::try_new("valid").unwrap();
assert_eq!(id.as_str(), "valid");
}
#[test]
fn test_hub_nodes_returns_nodes_meeting_threshold() {
let g = make_graph();
add(&g, "hub"); add(&g, "mid"); add(&g, "leaf");
link(&g, "hub", "mid"); link(&g, "hub", "leaf"); link(&g, "mid", "leaf");
let hubs = g.hub_nodes(2).unwrap();
assert_eq!(hubs.len(), 1);
assert_eq!(hubs[0].id, EntityId::new("hub"));
}
#[test]
fn test_hub_nodes_threshold_zero_returns_all() {
let g = make_graph();
add(&g, "a"); add(&g, "b");
assert_eq!(g.hub_nodes(0).unwrap().len(), 2);
}
#[test]
fn test_incident_relationships_includes_outgoing_and_incoming() {
let g = make_graph();
add(&g, "a"); add(&g, "b"); add(&g, "c");
link(&g, "a", "b"); link(&g, "c", "b");
let rels = g.incident_relationships(&EntityId::new("b")).unwrap();
assert_eq!(rels.len(), 2);
}
#[test]
fn test_incident_relationships_empty_for_isolated_node() {
let g = make_graph();
add(&g, "iso");
assert!(g.incident_relationships(&EntityId::new("iso")).unwrap().is_empty());
}
#[test]
fn test_average_out_degree_empty_graph_is_zero() {
let g = GraphStore::new();
assert!((g.average_out_degree().unwrap() - 0.0).abs() < 1e-9);
}
#[test]
fn test_average_out_degree_two_nodes_one_edge() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
assert!((g.average_out_degree().unwrap() - 0.5).abs() < 1e-9);
}
#[test]
fn test_in_degree_for_counts_incoming_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_entity(Entity::new("c", "C")).unwrap();
g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
g.add_relationship(Relationship::new("c", "b", "r", 1.0)).unwrap();
assert_eq!(g.in_degree_for(&EntityId::new("b")).unwrap(), 2);
assert_eq!(g.in_degree_for(&EntityId::new("a")).unwrap(), 0);
}
#[test]
fn test_in_degree_for_returns_zero_for_unknown_entity() {
let g = GraphStore::new();
assert_eq!(g.in_degree_for(&EntityId::new("ghost")).unwrap(), 0);
}
#[test]
fn test_entity_id_is_empty_false_for_nonempty() {
let id = EntityId::new("node-1");
assert!(!id.is_empty());
}
#[test]
fn test_entity_id_starts_with_matches_prefix() {
let id = EntityId::new("concept-42");
assert!(id.starts_with("concept-"));
assert!(!id.starts_with("entity-"));
}
#[test]
fn test_entity_id_starts_with_empty_always_true() {
let id = EntityId::new("anything");
assert!(id.starts_with(""));
}
#[test]
fn test_entity_has_property_returns_true_when_present() {
let e = Entity::new("e", "Node")
.with_property("color", serde_json::json!("blue"));
assert!(e.has_property("color"));
assert!(!e.has_property("size"));
}
#[test]
fn test_entity_has_property_false_when_no_properties() {
let e = Entity::new("e", "Node");
assert!(!e.has_property("any"));
}
#[test]
fn test_entity_labels_returns_distinct_sorted_labels() {
let g = make_graph();
g.add_entity(Entity::new("a", "Person")).unwrap();
g.add_entity(Entity::new("b", "Concept")).unwrap();
g.add_entity(Entity::new("c", "Person")).unwrap();
let labels = g.entity_labels().unwrap();
assert_eq!(labels, vec!["Concept", "Person"]);
}
#[test]
fn test_entity_labels_empty_for_empty_graph() {
let g = make_graph();
assert!(g.entity_labels().unwrap().is_empty());
}
#[test]
fn test_out_degree_for_returns_outgoing_edge_count() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_entity(Entity::new("c", "C")).unwrap();
g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
assert_eq!(g.out_degree_for(&EntityId::new("a")).unwrap(), 2);
assert_eq!(g.out_degree_for(&EntityId::new("b")).unwrap(), 0);
}
#[test]
fn test_out_degree_for_returns_zero_for_unknown_entity() {
let g = GraphStore::new();
assert_eq!(g.out_degree_for(&EntityId::new("ghost")).unwrap(), 0);
}
#[test]
fn test_predecessors_returns_nodes_with_incoming_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_entity(Entity::new("c", "C")).unwrap();
g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "r", 1.0)).unwrap();
let mut preds: Vec<String> = g
.predecessors(&EntityId::new("c"))
.unwrap()
.iter()
.map(|e| e.id.as_str().to_string())
.collect();
preds.sort();
assert_eq!(preds, vec!["a", "b"]);
}
#[test]
fn test_predecessors_empty_for_source_node() {
let g = GraphStore::new();
g.add_entity(Entity::new("root", "Root")).unwrap();
g.add_entity(Entity::new("child", "Child")).unwrap();
g.add_relationship(Relationship::new("root", "child", "r", 1.0)).unwrap();
assert!(g.predecessors(&EntityId::new("root")).unwrap().is_empty());
}
#[test]
fn test_is_source_true_for_node_with_no_incoming_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("src", "Src")).unwrap();
g.add_entity(Entity::new("dst", "Dst")).unwrap();
g.add_relationship(Relationship::new("src", "dst", "r", 1.0)).unwrap();
assert!(g.is_source(&EntityId::new("src")).unwrap());
assert!(!g.is_source(&EntityId::new("dst")).unwrap());
}
#[test]
fn test_relationship_is_self_loop_true_when_from_equals_to() {
let r = Relationship::new("a", "a", "self", 1.0);
assert!(r.is_self_loop());
}
#[test]
fn test_relationship_is_self_loop_false_for_normal_edge() {
let r = Relationship::new("a", "b", "edge", 1.0);
assert!(!r.is_self_loop());
}
#[test]
fn test_relationship_reversed_swaps_endpoints() {
let r = Relationship::new("from", "to", "knows", 0.5);
let rev = r.reversed();
assert_eq!(rev.from.as_str(), "to");
assert_eq!(rev.to.as_str(), "from");
assert_eq!(rev.kind, "knows");
assert!((rev.weight - 0.5).abs() < 1e-6);
}
#[test]
fn test_find_entities_by_labels_returns_matching() {
let g = make_graph();
g.add_entity(Entity::new("p1", "Person")).unwrap();
g.add_entity(Entity::new("p2", "Person")).unwrap();
g.add_entity(Entity::new("c1", "Concept")).unwrap();
let results = g.find_entities_by_labels(&["Person"]).unwrap();
assert_eq!(results.len(), 2);
assert!(results.iter().all(|e| e.label == "Person"));
}
#[test]
fn test_find_entities_by_labels_empty_when_no_match() {
let g = make_graph();
g.add_entity(Entity::new("n1", "Node")).unwrap();
let results = g.find_entities_by_labels(&["Missing"]).unwrap();
assert!(results.is_empty());
}
#[test]
fn test_remove_isolated_removes_nodes_without_edges() {
let g = make_graph();
g.add_entity(Entity::new("connected", "N")).unwrap();
g.add_entity(Entity::new("isolated", "N")).unwrap();
g.add_entity(Entity::new("other", "N")).unwrap();
g.add_relationship(Relationship::new("connected", "other", "r", 1.0)).unwrap();
let removed = g.remove_isolated().unwrap();
assert_eq!(removed, 1);
assert!(g.get_entity(&EntityId::new("isolated")).is_err());
assert!(g.get_entity(&EntityId::new("connected")).is_ok());
}
#[test]
fn test_remove_isolated_zero_when_all_connected() {
let g = make_graph();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
assert_eq!(g.remove_isolated().unwrap(), 0);
}
#[test]
fn test_successors_returns_direct_out_neighbors() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_entity(Entity::new("c", "C")).unwrap();
g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "r", 1.0)).unwrap();
let mut ids: Vec<String> = g
.successors(&EntityId::new("a"))
.unwrap()
.iter()
.map(|e| e.id.as_str().to_string())
.collect();
ids.sort();
assert_eq!(ids, vec!["b", "c"]);
}
#[test]
fn test_successors_empty_for_sink_node() {
let g = GraphStore::new();
g.add_entity(Entity::new("leaf", "L")).unwrap();
assert!(g.successors(&EntityId::new("leaf")).unwrap().is_empty());
}
#[test]
fn test_is_sink_true_for_node_with_no_outgoing_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "A")).unwrap();
g.add_entity(Entity::new("b", "B")).unwrap();
g.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
assert!(!g.is_sink(&EntityId::new("a")).unwrap());
assert!(g.is_sink(&EntityId::new("b")).unwrap());
}
#[test]
fn test_is_sink_true_for_unknown_entity() {
let g = GraphStore::new();
assert!(g.is_sink(&EntityId::new("ghost")).unwrap());
}
#[test]
fn test_reachable_from_returns_all_downstream_nodes() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "edge", 1.0)).unwrap();
let reachable = g.reachable_from(&EntityId::new("a")).unwrap();
assert!(reachable.contains(&EntityId::new("b")));
assert!(reachable.contains(&EntityId::new("c")));
assert!(!reachable.contains(&EntityId::new("a")));
}
#[test]
fn test_reachable_from_empty_for_sink_node() {
let g = GraphStore::new();
g.add_entity(Entity::new("sink", "N")).unwrap();
let reachable = g.reachable_from(&EntityId::new("sink")).unwrap();
assert!(reachable.is_empty());
}
#[test]
fn test_reachable_from_empty_for_unknown_node() {
let g = GraphStore::new();
let reachable = g.reachable_from(&EntityId::new("ghost")).unwrap();
assert!(reachable.is_empty());
}
#[test]
fn test_contains_cycle_false_for_dag() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
assert!(!g.contains_cycle().unwrap());
}
#[test]
fn test_contains_cycle_true_for_cyclic_graph() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "N")).unwrap();
g.add_entity(Entity::new("y", "N")).unwrap();
g.add_relationship(Relationship::new("x", "y", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("y", "x", "e", 1.0)).unwrap();
assert!(g.contains_cycle().unwrap());
}
#[test]
fn test_contains_cycle_false_for_empty_graph() {
let g = GraphStore::new();
assert!(!g.contains_cycle().unwrap());
}
#[test]
fn test_is_acyclic_true_for_dag() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
assert!(g.is_acyclic().unwrap());
}
#[test]
fn test_is_acyclic_false_for_cyclic_graph() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "N")).unwrap();
g.add_entity(Entity::new("y", "N")).unwrap();
g.add_relationship(Relationship::new("x", "y", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("y", "x", "e", 1.0)).unwrap();
assert!(!g.is_acyclic().unwrap());
}
#[test]
fn test_is_acyclic_true_for_empty_graph() {
let g = GraphStore::new();
assert!(g.is_acyclic().unwrap());
}
#[test]
fn test_count_relationships_by_kind_returns_correct_count() {
let g = make_graph();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "knows", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "knows", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "likes", 0.5)).unwrap();
assert_eq!(g.count_relationships_by_kind("knows").unwrap(), 2);
assert_eq!(g.count_relationships_by_kind("likes").unwrap(), 1);
assert_eq!(g.count_relationships_by_kind("absent").unwrap(), 0);
}
#[test]
fn test_merge_imports_entities_and_relationships() {
let g1 = make_graph();
g1.add_entity(Entity::new("a", "N")).unwrap();
g1.add_entity(Entity::new("b", "N")).unwrap();
g1.add_relationship(Relationship::new("a", "b", "r", 1.0)).unwrap();
let g2 = make_graph();
g2.add_entity(Entity::new("c", "N")).unwrap();
g2.add_entity(Entity::new("a", "N")).unwrap(); g2.add_relationship(Relationship::new("c", "a", "s", 0.5)).unwrap();
g1.merge(&g2).unwrap();
assert_eq!(g1.entity_count().unwrap(), 3); assert_eq!(g1.relationship_count().unwrap(), 2); }
#[test]
fn test_top_nodes_by_in_degree_returns_sinks() {
let g = make_graph();
g.add_entity(Entity::new("hub", "N")).unwrap();
g.add_entity(Entity::new("src1", "N")).unwrap();
g.add_entity(Entity::new("src2", "N")).unwrap();
g.add_relationship(Relationship::new("src1", "hub", "r", 1.0)).unwrap();
g.add_relationship(Relationship::new("src2", "hub", "r", 1.0)).unwrap();
let top = g.top_nodes_by_in_degree(1).unwrap();
assert_eq!(top.len(), 1);
assert_eq!(top[0].id.as_str(), "hub");
}
#[test]
fn test_top_nodes_by_out_degree_returns_sources() {
let g = make_graph();
g.add_entity(Entity::new("src", "N")).unwrap();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("src", "a", "r", 1.0)).unwrap();
g.add_relationship(Relationship::new("src", "b", "r", 1.0)).unwrap();
let top = g.top_nodes_by_out_degree(1).unwrap();
assert_eq!(top.len(), 1);
assert_eq!(top[0].id.as_str(), "src");
}
#[test]
fn test_entity_property_value_returns_value() {
let e = Entity::new("n1", "Node")
.with_property("age", serde_json::Value::Number(42.into()));
let val = e.property_value("age");
assert!(val.is_some());
assert_eq!(val.unwrap(), &serde_json::Value::Number(42.into()));
}
#[test]
fn test_entity_property_value_missing_returns_none() {
let e = Entity::new("n1", "Node");
assert!(e.property_value("missing").is_none());
}
#[test]
fn test_find_relationships_by_kind_returns_matching() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
let rels = g.find_relationships_by_kind("KNOWS").unwrap();
assert_eq!(rels.len(), 2);
assert!(rels.iter().all(|r| r.kind == "KNOWS"));
}
#[test]
fn test_find_relationships_by_kind_no_match_returns_empty() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
let rels = g.find_relationships_by_kind("HATES").unwrap();
assert!(rels.is_empty());
}
#[test]
fn test_count_relationships_by_kind_correct() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "PART_OF", 1.0)).unwrap();
assert_eq!(g.count_relationships_by_kind("KNOWS").unwrap(), 2);
assert_eq!(g.count_relationships_by_kind("PART_OF").unwrap(), 1);
assert_eq!(g.count_relationships_by_kind("MISSING").unwrap(), 0);
}
#[test]
fn test_max_out_degree_returns_highest_out_degree() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
assert_eq!(g.max_out_degree().unwrap(), 2);
}
#[test]
fn test_max_out_degree_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.max_out_degree().unwrap(), 0);
}
#[test]
fn test_max_in_degree_returns_highest_in_degree() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
assert_eq!(g.max_in_degree().unwrap(), 2);
}
#[test]
fn test_max_in_degree_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.max_in_degree().unwrap(), 0);
}
#[test]
fn test_sum_edge_weights_correct_sum() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "e", 1.5)).unwrap();
g.add_relationship(Relationship::new("b", "c", "e", 2.5)).unwrap();
let total = g.sum_edge_weights().unwrap();
assert!((total - 4.0).abs() < 1e-9);
}
#[test]
fn test_sum_edge_weights_zero_for_empty_graph() {
let g = GraphStore::new();
assert!((g.sum_edge_weights().unwrap() - 0.0).abs() < 1e-9);
}
#[test]
fn test_weight_stats_none_for_empty_graph() {
let g = GraphStore::new();
assert!(g.weight_stats().unwrap().is_none());
}
#[test]
fn test_weight_stats_returns_correct_min_max_mean() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "e", 3.0)).unwrap();
let (min, max, mean) = g.weight_stats().unwrap().unwrap();
assert!((min - 1.0).abs() < 1e-9);
assert!((max - 3.0).abs() < 1e-9);
assert!((mean - 2.0).abs() < 1e-9);
}
#[test]
fn test_isolated_nodes_empty_graph_returns_empty_set() {
let g = GraphStore::new();
assert!(g.isolated_nodes().unwrap().is_empty());
}
#[test]
fn test_isolated_nodes_all_connected_returns_empty() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
assert!(g.isolated_nodes().unwrap().is_empty());
}
#[test]
fn test_isolated_nodes_returns_orphan_entity() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("orphan", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
let iso = g.isolated_nodes().unwrap();
assert_eq!(iso.len(), 1);
assert!(iso.contains(&EntityId::new("orphan")));
}
#[test]
fn test_reverse_flips_edge_direction() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "N")).unwrap();
g.add_entity(Entity::new("y", "N")).unwrap();
g.add_relationship(Relationship::new("x", "y", "edge", 1.0)).unwrap();
let rev = g.reverse().unwrap();
let y_id = EntityId::new("y");
let succs = rev.successors(&y_id).unwrap();
assert!(succs.iter().any(|e| e.id == EntityId::new("x")));
}
#[test]
fn test_reverse_empty_graph_produces_empty_reverse() {
let g = GraphStore::new();
let rev = g.reverse().unwrap();
assert!(rev.is_empty().unwrap());
}
#[test]
fn test_max_in_degree_entity_none_for_empty_graph() {
let g = GraphStore::new();
assert!(g.max_in_degree_entity().unwrap().is_none());
}
#[test]
fn test_max_in_degree_entity_returns_node_with_most_incoming() {
let g = GraphStore::new();
g.add_entity(Entity::new("hub", "N")).unwrap();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "hub", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "hub", "e", 1.0)).unwrap();
let best = g.max_in_degree_entity().unwrap().unwrap();
assert_eq!(best.id, EntityId::new("hub"));
}
#[test]
fn test_shortest_path_length_none_when_no_path() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
let len = g
.shortest_path_length(&EntityId::new("a"), &EntityId::new("b"))
.unwrap();
assert!(len.is_none());
}
#[test]
fn test_shortest_path_length_one_for_direct_edge() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
let len = g
.shortest_path_length(&EntityId::new("a"), &EntityId::new("b"))
.unwrap();
assert_eq!(len, Some(1));
}
#[test]
fn test_shortest_path_length_two_for_two_hop_path() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
let len = g
.shortest_path_length(&EntityId::new("a"), &EntityId::new("c"))
.unwrap();
assert_eq!(len, Some(2));
}
#[test]
fn test_avg_out_degree_zero_for_empty_graph() {
let g = GraphStore::new();
assert!((g.avg_out_degree().unwrap() - 0.0).abs() < 1e-9);
}
#[test]
fn test_avg_out_degree_correct_mean() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
let avg = g.avg_out_degree().unwrap();
assert!((avg - 2.0 / 3.0).abs() < 1e-9);
}
#[test]
fn test_avg_in_degree_zero_for_empty_graph() {
let g = GraphStore::new();
assert!((g.avg_in_degree().unwrap() - 0.0).abs() < 1e-9);
}
#[test]
fn test_avg_in_degree_correct_mean() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "c", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "e", 1.0)).unwrap();
let avg = g.avg_in_degree().unwrap();
assert!((avg - 2.0 / 3.0).abs() < 1e-9);
}
#[test]
fn test_graph_store_has_entity_false_when_missing() {
let g = GraphStore::new();
let id = EntityId::new("nonexistent");
assert!(!g.has_entity(&id).unwrap());
}
#[test]
fn test_graph_store_has_entity_true_after_add() {
let g = GraphStore::new();
g.add_entity(Entity::new("node-a", "Person")).unwrap();
let id = EntityId::new("node-a");
assert!(g.has_entity(&id).unwrap());
}
#[test]
fn test_entity_with_property_stores_value() {
let e = Entity::new("e1", "Label")
.with_property("score", serde_json::json!(42));
assert_eq!(e.property_value("score"), Some(&serde_json::json!(42)));
}
#[test]
fn test_entity_with_property_chaining() {
let e = Entity::new("e2", "L")
.with_property("a", serde_json::json!(1))
.with_property("b", serde_json::json!(2));
assert!(e.has_property("a"));
assert!(e.has_property("b"));
}
#[test]
fn test_remove_relationship_succeeds_when_exists() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "N")).unwrap();
g.add_entity(Entity::new("y", "N")).unwrap();
g.add_relationship(Relationship::new("x", "y", "link", 1.0)).unwrap();
g.remove_relationship(&EntityId::new("x"), &EntityId::new("y"), "link").unwrap();
assert_eq!(g.relationship_count().unwrap(), 0);
}
#[test]
fn test_remove_relationship_errors_when_missing() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "N")).unwrap();
g.add_entity(Entity::new("y", "N")).unwrap();
let result = g.remove_relationship(&EntityId::new("x"), &EntityId::new("y"), "ghost");
assert!(result.is_err());
}
#[test]
fn test_update_entity_property_returns_true_when_entity_exists() {
let g = GraphStore::new();
g.add_entity(Entity::new("node", "N")).unwrap();
let updated = g
.update_entity_property(&EntityId::new("node"), "color", serde_json::json!("red"))
.unwrap();
assert!(updated);
let entity = g.get_entity(&EntityId::new("node")).unwrap();
assert_eq!(entity.property_value("color"), Some(&serde_json::json!("red")));
}
#[test]
fn test_update_entity_property_returns_false_for_unknown_entity() {
let g = GraphStore::new();
let updated = g
.update_entity_property(&EntityId::new("ghost"), "key", serde_json::json!(1))
.unwrap();
assert!(!updated);
}
#[test]
fn test_graph_store_has_any_entities_false_when_empty() {
let g = GraphStore::new();
assert!(!g.has_any_entities().unwrap());
}
#[test]
fn test_graph_store_has_any_entities_true_after_add() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "Node")).unwrap();
assert!(g.has_any_entities().unwrap());
}
#[test]
fn test_entity_type_count_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.entity_type_count().unwrap(), 0);
}
#[test]
fn test_entity_type_count_counts_distinct_labels() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person")).unwrap();
g.add_entity(Entity::new("b", "Person")).unwrap();
g.add_entity(Entity::new("c", "Concept")).unwrap();
assert_eq!(g.entity_type_count().unwrap(), 2);
}
#[test]
fn test_orphan_count_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.orphan_count().unwrap(), 0);
}
#[test]
fn test_orphan_count_all_orphans_with_no_relationships() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
assert_eq!(g.orphan_count().unwrap(), 2);
}
#[test]
fn test_orphan_count_excludes_entities_with_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
assert_eq!(g.orphan_count().unwrap(), 1);
}
#[test]
fn test_labels_empty_for_empty_graph() {
let g = GraphStore::new();
assert!(g.labels().unwrap().is_empty());
}
#[test]
fn test_labels_returns_distinct_sorted_labels() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Concept")).unwrap();
g.add_entity(Entity::new("b", "Person")).unwrap();
g.add_entity(Entity::new("c", "Concept")).unwrap();
assert_eq!(g.labels().unwrap(), vec!["Concept".to_string(), "Person".to_string()]);
}
#[test]
fn test_incoming_count_for_counts_inbound_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Node")).unwrap();
g.add_entity(Entity::new("b", "Node")).unwrap();
g.add_entity(Entity::new("c", "Node")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
g.add_relationship(Relationship::new("c", "b", "link", 1.0)).unwrap();
assert_eq!(g.incoming_count_for(&EntityId::new("b")).unwrap(), 2);
assert_eq!(g.incoming_count_for(&EntityId::new("a")).unwrap(), 0);
}
#[test]
fn test_outgoing_count_for_counts_outbound_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Node")).unwrap();
g.add_entity(Entity::new("b", "Node")).unwrap();
g.add_entity(Entity::new("c", "Node")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
assert_eq!(g.outgoing_count_for(&EntityId::new("a")).unwrap(), 2);
assert_eq!(g.outgoing_count_for(&EntityId::new("b")).unwrap(), 0);
}
#[test]
fn test_source_count_returns_number_of_nodes_with_no_incoming_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Node")).unwrap();
g.add_entity(Entity::new("b", "Node")).unwrap();
g.add_entity(Entity::new("c", "Node")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
assert_eq!(g.source_count().unwrap(), 1);
}
#[test]
fn test_source_count_all_isolated_nodes_are_sources() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "Node")).unwrap();
g.add_entity(Entity::new("y", "Node")).unwrap();
assert_eq!(g.source_count().unwrap(), 2);
}
#[test]
fn test_sink_count_returns_nodes_with_no_outgoing_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Node")).unwrap();
g.add_entity(Entity::new("b", "Node")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
assert_eq!(g.sink_count().unwrap(), 1);
assert_eq!(g.source_count().unwrap(), 1);
}
#[test]
fn test_has_self_loops_true_when_self_loop_exists() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Node")).unwrap();
g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
assert!(g.has_self_loops().unwrap());
}
#[test]
fn test_has_self_loops_false_when_no_self_loops() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Node")).unwrap();
g.add_entity(Entity::new("b", "Node")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
assert!(!g.has_self_loops().unwrap());
}
#[test]
fn test_bidirectional_count_counts_mutual_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Node")).unwrap();
g.add_entity(Entity::new("b", "Node")).unwrap();
g.add_entity(Entity::new("c", "Node")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
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);
}
#[test]
fn test_bidirectional_count_zero_when_no_mutual_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Node")).unwrap();
g.add_entity(Entity::new("b", "Node")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
assert_eq!(g.bidirectional_count().unwrap(), 0);
}
#[test]
fn test_relationship_kind_count_counts_distinct_kinds() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "friend", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "enemy", 1.0)).unwrap();
assert_eq!(g.relationship_kind_count().unwrap(), 2);
}
#[test]
fn test_relationship_kind_count_zero_when_empty() {
let g = GraphStore::new();
assert_eq!(g.relationship_kind_count().unwrap(), 0);
}
#[test]
fn test_entities_with_self_loops_returns_self_loop_ids() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
let ids = g.entities_with_self_loops().unwrap();
assert_eq!(ids, vec![EntityId::new("a")]);
}
#[test]
fn test_entities_with_self_loops_empty_when_no_self_loops() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
assert!(g.entities_with_self_loops().unwrap().is_empty());
}
#[test]
fn test_isolated_entity_count_returns_count_with_no_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
assert_eq!(g.isolated_entity_count().unwrap(), 1);
}
#[test]
fn test_isolated_entity_count_zero_when_all_connected() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
assert_eq!(g.isolated_entity_count().unwrap(), 0);
}
#[test]
fn test_avg_relationship_weight_returns_mean() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "link", 3.0)).unwrap();
let avg = g.avg_relationship_weight().unwrap();
assert!((avg - 2.0).abs() < 1e-6);
}
#[test]
fn test_avg_relationship_weight_zero_when_no_relationships() {
let g = GraphStore::new();
assert_eq!(g.avg_relationship_weight().unwrap(), 0.0);
}
#[test]
fn test_total_in_degree_equals_relationship_count() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "link", 1.0)).unwrap();
assert_eq!(g.total_in_degree().unwrap(), 2);
}
#[test]
fn test_total_in_degree_zero_when_no_relationships() {
let g = GraphStore::new();
assert_eq!(g.total_in_degree().unwrap(), 0);
}
#[test]
fn test_relationship_count_between_counts_edges_between_pair() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "b", "colleague", 1.0)).unwrap();
let from = EntityId::new("a");
let to = EntityId::new("b");
assert_eq!(g.relationship_count_between(&from, &to).unwrap(), 2);
}
#[test]
fn test_relationship_count_between_returns_zero_for_no_edge() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
let from = EntityId::new("a");
let to = EntityId::new("b");
assert_eq!(g.relationship_count_between(&from, &to).unwrap(), 0);
}
#[test]
fn test_edges_from_returns_all_outgoing_relationships() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "friend", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "enemy", 0.5)).unwrap();
let edges = g.edges_from(&EntityId::new("a")).unwrap();
assert_eq!(edges.len(), 2);
}
#[test]
fn test_edges_from_returns_empty_for_unknown_entity() {
let g = GraphStore::new();
assert!(g.edges_from(&EntityId::new("missing")).unwrap().is_empty());
}
#[test]
fn test_neighbors_of_returns_sorted_unique_targets() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
let neighbors = g.neighbors_of(&EntityId::new("a")).unwrap();
assert_eq!(neighbors, vec![EntityId::new("b"), EntityId::new("c")]);
}
#[test]
fn test_neighbors_of_returns_empty_for_no_outgoing_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
assert!(g.neighbors_of(&EntityId::new("a")).unwrap().is_empty());
}
#[test]
fn test_entity_id_from_string() {
let id = EntityId::from("node-1".to_owned());
assert_eq!(id.as_str(), "node-1");
}
#[test]
fn test_entity_id_from_str_ref() {
let id = EntityId::from("node-2");
assert_eq!(id.as_str(), "node-2");
}
#[test]
fn test_entity_id_from_str_parse_rejects_empty() {
let result: Result<EntityId, _> = "".parse();
assert!(result.is_err());
}
#[test]
fn test_entity_id_from_str_parse_accepts_nonempty() {
let id: EntityId = "alice".parse().unwrap();
assert_eq!(id.as_str(), "alice");
}
#[test]
fn test_entity_id_deref_to_str() {
let id = EntityId::new("deref-node");
let s: &str = &id;
assert_eq!(s, "deref-node");
}
#[test]
fn test_entity_id_deref_enables_str_methods() {
let id = EntityId::new("hello-world");
assert!(id.contains('-'));
assert_eq!(id.len(), 11);
}
#[test]
fn test_entity_remove_property_returns_value() {
let mut e = Entity::new("e1", "Person")
.with_property("age", serde_json::json!(30));
let removed = e.remove_property("age");
assert_eq!(removed, Some(serde_json::json!(30)));
assert!(!e.has_property("age"));
}
#[test]
fn test_entity_remove_property_returns_none_when_absent() {
let mut e = Entity::new("e2", "Person");
assert!(e.remove_property("nonexistent").is_none());
}
#[test]
fn test_entity_property_count() {
let e = Entity::new("e3", "X")
.with_property("a", serde_json::json!(1))
.with_property("b", serde_json::json!(2));
assert_eq!(e.property_count(), 2);
}
#[test]
fn test_entity_properties_is_empty_true_when_none() {
let e = Entity::new("e4", "X");
assert!(e.properties_is_empty());
}
#[test]
fn test_entity_properties_is_empty_false_when_has_props() {
let e = Entity::new("e5", "X").with_property("k", serde_json::json!("v"));
assert!(!e.properties_is_empty());
}
#[test]
fn test_relationship_type_counts_returns_correct_map() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b"); add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
let counts = g.relationship_type_counts().unwrap();
assert_eq!(counts.get("KNOWS"), Some(&2));
assert_eq!(counts.get("LIKES"), Some(&1));
}
#[test]
fn test_relationship_type_counts_empty_graph_returns_empty_map() {
let g = GraphStore::new();
assert!(g.relationship_type_counts().unwrap().is_empty());
}
#[test]
fn test_entities_without_property_returns_nodes_missing_key() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N").with_property("age", serde_json::json!(30))).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
let result = g.entities_without_property("age").unwrap();
assert_eq!(result.len(), 2);
assert!(result.iter().all(|e| !e.properties.contains_key("age")));
}
#[test]
fn test_entities_without_property_empty_when_all_have_key() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N").with_property("role", serde_json::json!("admin"))).unwrap();
assert!(g.entities_without_property("role").unwrap().is_empty());
}
#[test]
fn test_min_out_degree_returns_zero_when_sink_present() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b"); add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
assert_eq!(g.min_out_degree().unwrap(), 0);
}
#[test]
fn test_min_out_degree_returns_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.min_out_degree().unwrap(), 0);
}
#[test]
fn test_min_in_degree_returns_zero_when_source_present() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b");
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
assert_eq!(g.min_in_degree().unwrap(), 0);
}
#[test]
fn test_min_in_degree_returns_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.min_in_degree().unwrap(), 0);
}
#[test]
fn test_relationship_kinds_from_returns_sorted_distinct_kinds() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b"); add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "b", "TRUSTS", 1.0)).unwrap();
let kinds = g.relationship_kinds_from(&EntityId::new("a")).unwrap();
assert_eq!(kinds, vec!["KNOWS", "LIKES", "TRUSTS"]);
}
#[test]
fn test_relationship_kinds_from_returns_empty_for_unknown_entity() {
let g = GraphStore::new();
assert!(g.relationship_kinds_from(&EntityId::new("missing")).unwrap().is_empty());
}
#[test]
fn test_relationship_kinds_from_deduplicates_kinds() {
let g = GraphStore::new();
add(&g, "x"); add(&g, "y"); add(&g, "z");
g.add_relationship(Relationship::new("x", "y", "SAME", 1.0)).unwrap();
g.add_relationship(Relationship::new("x", "z", "SAME", 0.5)).unwrap();
let kinds = g.relationship_kinds_from(&EntityId::new("x")).unwrap();
assert_eq!(kinds, vec!["SAME"]);
}
#[test]
fn test_unique_relationship_types_returns_sorted_deduped_types() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b"); add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "LIKES", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "KNOWS", 0.5)).unwrap();
let types = g.unique_relationship_types().unwrap();
assert_eq!(types, vec!["KNOWS", "LIKES"]);
}
#[test]
fn test_unique_relationship_types_empty_graph_returns_empty() {
let g = GraphStore::new();
assert!(g.unique_relationship_types().unwrap().is_empty());
}
#[test]
fn test_avg_property_count_returns_correct_average() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")
.with_property("x", serde_json::json!(1))
.with_property("y", serde_json::json!(2))).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap(); assert!((g.avg_property_count().unwrap() - 1.0).abs() < 1e-9);
}
#[test]
fn test_avg_property_count_empty_graph_returns_zero() {
let g = GraphStore::new();
assert_eq!(g.avg_property_count().unwrap(), 0.0);
}
#[test]
fn test_property_key_frequency_counts_correctly() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")
.with_property("age", serde_json::json!(30))
.with_property("role", serde_json::json!("admin"))).unwrap();
g.add_entity(Entity::new("b", "N")
.with_property("age", serde_json::json!(25))).unwrap();
let freq = g.property_key_frequency().unwrap();
assert_eq!(freq.get("age"), Some(&2));
assert_eq!(freq.get("role"), Some(&1));
}
#[test]
fn test_property_key_frequency_empty_graph_returns_empty() {
let g = GraphStore::new();
assert!(g.property_key_frequency().unwrap().is_empty());
}
#[test]
fn test_has_edge_returns_true_when_edge_exists() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
assert!(g.has_edge(&EntityId::new("a"), &EntityId::new("b")).unwrap());
}
#[test]
fn test_has_edge_returns_false_when_no_edge() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b");
assert!(!g.has_edge(&EntityId::new("a"), &EntityId::new("b")).unwrap());
}
#[test]
fn test_has_edge_is_directional() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
assert!(!g.has_edge(&EntityId::new("b"), &EntityId::new("a")).unwrap());
}
#[test]
fn test_entity_display_with_props() {
let e = Entity::new("alice", "Person")
.with_property("age", serde_json::json!(30));
let s = e.to_string();
assert!(s.contains("alice") && s.contains("Person") && s.contains("props=1"));
}
#[test]
fn test_entity_display_no_props() {
let e = Entity::new("bob", "Node");
assert_eq!(e.to_string(), "Entity[id='bob', label='Node', props=0]");
}
#[test]
fn test_relationship_display_format() {
let r = Relationship::new("alice", "bob", "KNOWS", 1.5);
let s = r.to_string();
assert!(s.contains("alice") && s.contains("KNOWS") && s.contains("bob") && s.contains("1.50"));
}
#[test]
fn test_graph_total_degree_sum_of_in_and_out() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "e", 1.0)).unwrap();
g.add_relationship(Relationship::new("c", "a", "e", 1.0)).unwrap();
assert_eq!(g.total_degree(&EntityId::new("a")).unwrap(), 2);
}
#[test]
fn test_graph_total_degree_zero_for_isolated_node() {
let g = GraphStore::new();
g.add_entity(Entity::new("iso", "N")).unwrap();
assert_eq!(g.total_degree(&EntityId::new("iso")).unwrap(), 0);
}
#[test]
fn test_graph_entity_property_keys_returns_sorted() {
let g = GraphStore::new();
let e = Entity::new("e1", "X")
.with_property("z", serde_json::json!(1))
.with_property("a", serde_json::json!(2))
.with_property("m", serde_json::json!(3));
g.add_entity(e).unwrap();
assert_eq!(
g.entity_property_keys(&EntityId::new("e1")).unwrap(),
vec!["a", "m", "z"]
);
}
#[test]
fn test_graph_entity_property_keys_empty_for_unknown() {
let g = GraphStore::new();
assert!(g.entity_property_keys(&EntityId::new("missing")).unwrap().is_empty());
}
#[test]
fn test_entity_property_keys_method_sorted() {
let e = Entity::new("p", "T")
.with_property("b", serde_json::json!(0))
.with_property("a", serde_json::json!(0));
let keys = e.property_keys();
assert_eq!(keys, vec!["a", "b"]);
}
#[test]
fn test_entities_sorted_by_label_returns_alphabetical_order() {
let g = GraphStore::new();
g.add_entity(Entity::new("1", "Zebra")).unwrap();
g.add_entity(Entity::new("2", "Apple")).unwrap();
g.add_entity(Entity::new("3", "Mango")).unwrap();
let sorted = g.entities_sorted_by_label().unwrap();
assert_eq!(sorted[0].label, "Apple");
assert_eq!(sorted[1].label, "Mango");
assert_eq!(sorted[2].label, "Zebra");
}
#[test]
fn test_entities_sorted_by_label_empty_graph_returns_empty() {
let g = GraphStore::new();
assert!(g.entities_sorted_by_label().unwrap().is_empty());
}
#[test]
fn test_entities_with_property_returns_entities_with_key() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N").with_property("age", serde_json::json!(30))).unwrap();
g.add_entity(Entity::new("b", "N").with_property("name", serde_json::json!("bob"))).unwrap();
g.add_entity(Entity::new("c", "N").with_property("age", serde_json::json!(25))).unwrap();
let result = g.entities_with_property("age").unwrap();
assert_eq!(result.len(), 2);
assert!(result.iter().all(|e| e.properties.contains_key("age")));
}
#[test]
fn test_entities_with_property_returns_empty_when_no_match() {
let g = GraphStore::new();
add(&g, "a");
assert!(g.entities_with_property("missing_key").unwrap().is_empty());
}
#[test]
fn test_total_relationship_count_returns_edge_count() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b"); add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "LIKES", 1.0)).unwrap();
assert_eq!(g.total_relationship_count().unwrap(), 2);
}
#[test]
fn test_total_relationship_count_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.total_relationship_count().unwrap(), 0);
}
#[test]
fn test_entities_without_outgoing_returns_nodes_with_no_out_edges() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
let no_out = g.entities_without_outgoing().unwrap();
assert_eq!(no_out.len(), 1);
assert_eq!(no_out[0].id, EntityId::new("b"));
}
#[test]
fn test_entities_without_outgoing_all_returned_for_empty_graph() {
let g = GraphStore::new();
add(&g, "x");
let result = g.entities_without_outgoing().unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_entities_without_incoming_returns_nodes_with_no_in_edges() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
let no_in = g.entities_without_incoming().unwrap();
assert_eq!(no_in.len(), 1);
assert_eq!(no_in[0].id, EntityId::new("a"));
}
#[test]
fn test_entities_without_incoming_all_returned_for_isolated_node() {
let g = GraphStore::new();
add(&g, "x");
assert_eq!(g.entities_without_incoming().unwrap().len(), 1);
}
#[test]
fn test_path_to_string_uses_labels_when_entities_exist() {
let g = GraphStore::new();
let mut e1 = Entity::new("a", "Alice");
let mut e2 = Entity::new("b", "Bob");
let mut e3 = Entity::new("c", "Carol");
e1.label = "Alice".into();
e2.label = "Bob".into();
e3.label = "Carol".into();
g.add_entity(e1).unwrap();
g.add_entity(e2).unwrap();
g.add_entity(e3).unwrap();
let path = vec![EntityId::new("a"), EntityId::new("b"), EntityId::new("c")];
let s = g.path_to_string(&path).unwrap();
assert_eq!(s, "Alice \u{2192} Bob \u{2192} Carol");
}
#[test]
fn test_path_to_string_falls_back_to_id_for_unknown_entities() {
let g = GraphStore::new();
let path = vec![EntityId::new("x"), EntityId::new("y")];
let s = g.path_to_string(&path).unwrap();
assert!(s.contains("x") && s.contains("y"));
}
#[test]
fn test_path_to_string_empty_path_returns_empty_string() {
let g = GraphStore::new();
assert_eq!(g.path_to_string(&[]).unwrap(), "");
}
#[test]
fn test_relationship_with_weight_changes_weight() {
let rel = Relationship::new("a", "b", "KNOWS", 1.0).with_weight(0.25);
assert_eq!(rel.weight, 0.25);
assert_eq!(rel.from, EntityId::new("a"));
assert_eq!(rel.kind, "KNOWS");
}
#[test]
fn test_relationship_with_weight_zero_allowed() {
let rel = Relationship::new("x", "y", "EDGE", 5.0).with_weight(0.0);
assert_eq!(rel.weight, 0.0);
}
#[test]
fn test_total_out_degree_sums_all_outgoing_edges() {
let g = GraphStore::new();
add(&g, "a");
add(&g, "b");
add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
assert_eq!(g.total_out_degree().unwrap(), 3);
}
#[test]
fn test_total_out_degree_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.total_out_degree().unwrap(), 0);
}
#[test]
fn test_relationships_with_weight_above_filters_correctly() {
let g = GraphStore::new();
add(&g, "a");
add(&g, "b");
add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "EDGE", 0.5)).unwrap();
g.add_relationship(Relationship::new("a", "c", "EDGE", 1.5)).unwrap();
let heavy = g.relationships_with_weight_above(1.0).unwrap();
assert_eq!(heavy.len(), 1);
assert_eq!(heavy[0].to, EntityId::new("c"));
}
#[test]
fn test_relationships_with_weight_above_returns_empty_when_none_qualify() {
let g = GraphStore::new();
add(&g, "a");
add(&g, "b");
g.add_relationship(Relationship::new("a", "b", "EDGE", 0.3)).unwrap();
assert!(g.relationships_with_weight_above(1.0).unwrap().is_empty());
}
#[test]
fn test_relationships_of_kind_returns_matching_edges() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b"); add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
let knows = g.relationships_of_kind("KNOWS").unwrap();
assert_eq!(knows.len(), 2);
assert!(knows.iter().all(|r| r.kind == "KNOWS"));
}
#[test]
fn test_relationships_of_kind_returns_empty_for_unknown_kind() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
assert!(g.relationships_of_kind("MISSING").unwrap().is_empty());
}
#[test]
fn test_entities_without_label_excludes_matching_entities() {
let g = GraphStore::new();
let mut e1 = Entity::new("a", "Person");
let mut e2 = Entity::new("b", "Company");
let mut e3 = Entity::new("c", "Person");
e1.label = "Person".into();
e2.label = "Company".into();
e3.label = "Person".into();
g.add_entity(e1).unwrap();
g.add_entity(e2).unwrap();
g.add_entity(e3).unwrap();
let non_persons = g.entities_without_label("Person").unwrap();
assert_eq!(non_persons.len(), 1);
assert_eq!(non_persons[0].id, EntityId::new("b"));
}
#[test]
fn test_entities_without_label_returns_all_when_no_match() {
let g = GraphStore::new();
add(&g, "x");
assert_eq!(g.entities_without_label("NonExistent").unwrap().len(), 1);
}
#[test]
fn test_relationships_of_kind_returns_matching_relationships() {
let g = GraphStore::new();
add(&g, "a");
add(&g, "b");
add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
let knows = g.relationships_of_kind("KNOWS").unwrap();
assert_eq!(knows.len(), 1);
assert_eq!(knows[0].to, EntityId::new("b"));
}
#[test]
fn test_relationships_of_kind_empty_when_none_match() {
let g = GraphStore::new();
add(&g, "a");
add(&g, "b");
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
assert!(g.relationships_of_kind("HATES").unwrap().is_empty());
}
#[test]
fn test_entity_count_with_label_counts_matching_entities() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person")).unwrap();
g.add_entity(Entity::new("b", "Person")).unwrap();
g.add_entity(Entity::new("c", "Organization")).unwrap();
assert_eq!(g.entity_count_with_label("Person").unwrap(), 2);
assert_eq!(g.entity_count_with_label("Organization").unwrap(), 1);
}
#[test]
fn test_entity_count_with_label_zero_for_no_match() {
let g = GraphStore::new();
add(&g, "a");
assert_eq!(g.entity_count_with_label("Alien").unwrap(), 0);
}
#[test]
fn test_edge_count_above_weight_counts_heavy_edges() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b"); add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "K", 0.9)).unwrap();
g.add_relationship(Relationship::new("b", "c", "K", 0.3)).unwrap();
g.add_relationship(Relationship::new("a", "c", "K", 1.5)).unwrap();
assert_eq!(g.edge_count_above_weight(0.8).unwrap(), 2); }
#[test]
fn test_edge_count_above_weight_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.edge_count_above_weight(0.0).unwrap(), 0);
}
#[test]
fn test_entities_with_label_prefix_returns_matching_entities() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person")).unwrap();
g.add_entity(Entity::new("b", "Pet")).unwrap();
g.add_entity(Entity::new("c", "Organization")).unwrap();
let result = g.entities_with_label_prefix("Pe").unwrap();
assert_eq!(result.len(), 2);
assert!(result.iter().all(|e| e.label.starts_with("Pe")));
}
#[test]
fn test_entities_with_label_prefix_empty_when_no_match() {
let g = GraphStore::new();
add(&g, "a");
assert!(g.entities_with_label_prefix("XYZ").unwrap().is_empty());
}
#[test]
fn test_bidirectional_pairs_returns_pairs_with_edges_in_both_directions() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b"); add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "K", 1.0)).unwrap();
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();
assert_eq!(pairs.len(), 1);
}
#[test]
fn test_bidirectional_pairs_empty_for_one_directional_graph() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b");
g.add_relationship(Relationship::new("a", "b", "K", 1.0)).unwrap();
assert!(g.bidirectional_pairs().unwrap().is_empty());
}
#[test]
fn test_entities_with_min_out_degree_filters_correctly() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b"); add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "R", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "R", 1.0)).unwrap();
let result = g.entities_with_min_out_degree(2).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].id.as_str(), "a");
}
#[test]
fn test_entities_with_min_out_degree_zero_includes_all() {
let g = GraphStore::new();
add(&g, "x"); add(&g, "y");
assert_eq!(g.entities_with_min_out_degree(0).unwrap().len(), 2);
}
#[test]
fn test_entities_with_min_out_degree_empty_for_empty_graph() {
let g = GraphStore::new();
assert!(g.entities_with_min_out_degree(1).unwrap().is_empty());
}
#[test]
fn test_mean_in_degree_computes_average() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b"); add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "EDGE", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "EDGE", 1.0)).unwrap();
let mean = g.mean_in_degree().unwrap();
assert!((mean - 2.0 / 3.0).abs() < 1e-9);
}
#[test]
fn test_mean_in_degree_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.mean_in_degree().unwrap(), 0.0);
}
#[test]
fn test_entity_count_by_label_prefix_counts_matching_entities() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person-Alice")).unwrap();
g.add_entity(Entity::new("b", "Person-Bob")).unwrap();
g.add_entity(Entity::new("c", "Organization")).unwrap();
assert_eq!(g.entity_count_by_label_prefix("Person").unwrap(), 2);
assert_eq!(g.entity_count_by_label_prefix("Org").unwrap(), 1);
}
#[test]
fn test_entity_count_by_label_prefix_zero_for_no_match() {
let g = GraphStore::new();
add(&g, "x");
assert_eq!(g.entity_count_by_label_prefix("Alien").unwrap(), 0);
}
#[test]
fn test_relationship_weight_sum_sums_all_weights() {
let g = GraphStore::new();
add(&g, "a"); add(&g, "b"); add(&g, "c");
g.add_relationship(Relationship::new("a", "b", "E", 2.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "E", 3.0)).unwrap();
assert!((g.relationship_weight_sum().unwrap() - 5.0).abs() < 1e-6);
}
#[test]
fn test_relationship_weight_sum_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.relationship_weight_sum().unwrap(), 0.0);
}
#[test]
fn test_label_frequency_counts_labels() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person")).unwrap();
g.add_entity(Entity::new("b", "Person")).unwrap();
g.add_entity(Entity::new("c", "Node")).unwrap();
let freq = g.label_frequency().unwrap();
assert_eq!(*freq.get("Person").unwrap(), 2);
assert_eq!(*freq.get("Node").unwrap(), 1);
}
#[test]
fn test_entities_sorted_by_id_returns_alphabetical_order() {
let g = GraphStore::new();
g.add_entity(Entity::new("charlie", "Node")).unwrap();
g.add_entity(Entity::new("alice", "Node")).unwrap();
g.add_entity(Entity::new("bob", "Node")).unwrap();
let sorted = g.entities_sorted_by_id().unwrap();
let ids: Vec<&str> = sorted.iter().map(|e| e.id.as_str()).collect();
assert_eq!(ids, vec!["alice", "bob", "charlie"]);
}
#[test]
fn test_entities_sorted_by_id_empty_for_empty_graph() {
let g = GraphStore::new();
assert!(g.entities_sorted_by_id().unwrap().is_empty());
}
#[test]
fn test_label_frequency_empty_for_empty_graph() {
let g = GraphStore::new();
assert!(g.label_frequency().unwrap().is_empty());
}
#[test]
fn test_entities_with_label_containing_returns_matching_entities() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "PersonA")).unwrap();
g.add_entity(Entity::new("b", "PersonB")).unwrap();
g.add_entity(Entity::new("c", "Location")).unwrap();
let mut result = g.entities_with_label_containing("Person").unwrap();
result.sort_unstable_by(|a, b| a.id.cmp(&b.id));
assert_eq!(result.len(), 2);
assert_eq!(result[0].id.as_str(), "a");
assert_eq!(result[1].id.as_str(), "b");
}
#[test]
fn test_entities_with_label_containing_empty_when_no_match() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Node")).unwrap();
assert!(g.entities_with_label_containing("Person").unwrap().is_empty());
}
#[test]
fn test_entities_with_min_in_degree_returns_correct_entities() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Node")).unwrap();
g.add_entity(Entity::new("b", "Node")).unwrap();
g.add_entity(Entity::new("c", "Node")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
g.add_relationship(Relationship::new("c", "b", "link", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
let result = g.entities_with_min_in_degree(2).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].id.as_str(), "b");
}
#[test]
fn test_entities_with_min_in_degree_zero_includes_all() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Node")).unwrap();
g.add_entity(Entity::new("b", "Node")).unwrap();
assert_eq!(g.entities_with_min_in_degree(0).unwrap().len(), 2);
}
#[test]
fn test_entities_with_min_in_degree_empty_for_empty_graph() {
let g = GraphStore::new();
assert!(g.entities_with_min_in_degree(1).unwrap().is_empty());
}
#[test]
fn test_total_property_count_sums_across_entities() {
let g = GraphStore::new();
g.add_entity(
Entity::new("a", "Node")
.with_property("x", serde_json::json!(1))
.with_property("y", serde_json::json!(2)),
)
.unwrap();
g.add_entity(Entity::new("b", "Node").with_property("z", serde_json::json!(3))).unwrap();
assert_eq!(g.total_property_count().unwrap(), 3);
}
#[test]
fn test_total_property_count_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.total_property_count().unwrap(), 0);
}
#[test]
fn test_entities_with_no_properties_returns_bare_entities() {
let g = GraphStore::new();
g.add_entity(Entity::new("bare", "Node")).unwrap();
g.add_entity(
Entity::new("annotated", "Node").with_property("k", serde_json::json!("v")),
)
.unwrap();
let result = g.entities_with_no_properties().unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].id.as_str(), "bare");
}
#[test]
fn test_entities_with_no_properties_empty_when_all_have_properties() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N").with_property("k", serde_json::json!(1))).unwrap();
assert!(g.entities_with_no_properties().unwrap().is_empty());
}
#[test]
fn test_entity_neighbor_count_returns_out_degree() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "E", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "E", 1.0)).unwrap();
let a_id = EntityId("a".to_string());
assert_eq!(g.entity_neighbor_count(&a_id).unwrap(), 2);
}
#[test]
fn test_entity_neighbor_count_zero_for_isolated_entity() {
let g = GraphStore::new();
g.add_entity(Entity::new("lone", "N")).unwrap();
let id = EntityId("lone".to_string());
assert_eq!(g.entity_neighbor_count(&id).unwrap(), 0);
}
#[test]
fn test_entity_by_label_returns_matching_entity() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person")).unwrap();
let result = g.entity_by_label("Person").unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().id.as_str(), "a");
}
#[test]
fn test_entity_by_label_returns_none_when_not_found() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Node")).unwrap();
assert!(g.entity_by_label("Missing").unwrap().is_none());
}
#[test]
fn test_distinct_relationship_kind_count_counts_unique_kinds() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "FRIEND", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "ENEMY", 1.0)).unwrap();
assert_eq!(g.distinct_relationship_kind_count().unwrap(), 2);
}
#[test]
fn test_distinct_relationship_kind_count_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.distinct_relationship_kind_count().unwrap(), 0);
}
#[test]
fn test_weight_above_threshold_ratio_returns_correct_fraction() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "E", 0.8)).unwrap();
g.add_relationship(Relationship::new("a", "c", "E", 0.3)).unwrap();
let ratio = g.weight_above_threshold_ratio(0.5).unwrap();
assert!((ratio - 0.5).abs() < 1e-9);
}
#[test]
fn test_weight_above_threshold_ratio_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.weight_above_threshold_ratio(0.5).unwrap(), 0.0);
}
#[test]
fn test_entities_sorted_by_out_degree_most_connected_first() {
let g = GraphStore::new();
g.add_entity(Entity::new("hub", "N")).unwrap();
g.add_entity(Entity::new("leaf", "N")).unwrap();
g.add_entity(Entity::new("mid", "N")).unwrap();
g.add_relationship(Relationship::new("hub", "leaf", "E", 1.0)).unwrap();
g.add_relationship(Relationship::new("hub", "mid", "E", 1.0)).unwrap();
g.add_relationship(Relationship::new("mid", "leaf", "E", 1.0)).unwrap();
let sorted = g.entities_sorted_by_out_degree().unwrap();
assert_eq!(sorted[0].id.as_str(), "hub"); assert_eq!(sorted[1].id.as_str(), "mid"); }
#[test]
fn test_entities_sorted_by_out_degree_empty_for_empty_graph() {
let g = GraphStore::new();
assert!(g.entities_sorted_by_out_degree().unwrap().is_empty());
}
#[test]
fn test_entity_labels_sorted_returns_unique_labels_in_order() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Zebra")).unwrap();
g.add_entity(Entity::new("b", "Apple")).unwrap();
g.add_entity(Entity::new("c", "Zebra")).unwrap(); let labels = g.entity_labels_sorted().unwrap();
assert_eq!(labels, vec!["Apple", "Zebra"]);
}
#[test]
fn test_entity_labels_sorted_empty_for_empty_graph() {
let g = GraphStore::new();
assert!(g.entity_labels_sorted().unwrap().is_empty());
}
#[test]
fn test_relationship_type_count_counts_distinct_kinds() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
assert_eq!(g.relationship_type_count().unwrap(), 2);
}
#[test]
fn test_relationship_type_count_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.relationship_type_count().unwrap(), 0);
}
#[test]
fn test_has_entity_with_label_true_when_present() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person")).unwrap();
assert!(g.has_entity_with_label("Person").unwrap());
}
#[test]
fn test_has_entity_with_label_false_when_absent() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person")).unwrap();
assert!(!g.has_entity_with_label("Robot").unwrap());
}
#[test]
fn test_has_entity_with_label_false_for_empty_graph() {
let g = GraphStore::new();
assert!(!g.has_entity_with_label("Anything").unwrap());
}
#[test]
fn test_min_weight_returns_smallest_weight() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "k", 0.5)).unwrap();
g.add_relationship(Relationship::new("b", "c", "k", 2.0)).unwrap();
assert_eq!(g.min_weight().unwrap(), Some(0.5));
}
#[test]
fn test_min_weight_none_for_graph_with_no_relationships() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
assert_eq!(g.min_weight().unwrap(), None);
}
#[test]
fn test_entities_with_exact_label_returns_matching_entities() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person")).unwrap();
g.add_entity(Entity::new("b", "Person")).unwrap();
g.add_entity(Entity::new("c", "Robot")).unwrap();
let people = g.entities_with_exact_label("Person").unwrap();
assert_eq!(people.len(), 2);
assert!(people.iter().all(|e| e.label == "Person"));
}
#[test]
fn test_entities_with_exact_label_empty_when_no_match() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person")).unwrap();
assert!(g.entities_with_exact_label("Robot").unwrap().is_empty());
}
#[test]
fn test_entities_with_exact_label_empty_for_empty_graph() {
let g = GraphStore::new();
assert!(g.entities_with_exact_label("Person").unwrap().is_empty());
}
#[test]
fn test_average_weight_returns_mean_of_all_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "k", 3.0)).unwrap();
assert_eq!(g.average_weight().unwrap(), 2.0);
}
#[test]
fn test_average_weight_zero_for_graph_with_no_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
assert_eq!(g.average_weight().unwrap(), 0.0);
}
#[test]
fn test_max_weight_returns_largest_weight() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "k", 0.5)).unwrap();
g.add_relationship(Relationship::new("b", "c", "k", 10.0)).unwrap();
assert_eq!(g.max_weight().unwrap(), Some(10.0));
}
#[test]
fn test_max_weight_none_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.max_weight().unwrap(), None);
}
#[test]
fn test_relationships_of_kind_count_returns_correct_count() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "FOLLOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "FOLLOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "LIKES", 1.0)).unwrap();
assert_eq!(g.relationships_of_kind_count("FOLLOWS").unwrap(), 2);
assert_eq!(g.relationships_of_kind_count("LIKES").unwrap(), 1);
}
#[test]
fn test_relationships_of_kind_count_zero_for_absent_kind() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
assert_eq!(g.relationships_of_kind_count("MISSING").unwrap(), 0);
}
#[test]
fn test_entities_with_incoming_returns_targets() {
let g = GraphStore::new();
g.add_entity(Entity::new("src", "N")).unwrap();
g.add_entity(Entity::new("dst", "N")).unwrap();
g.add_entity(Entity::new("iso", "N")).unwrap();
g.add_relationship(Relationship::new("src", "dst", "E", 1.0)).unwrap();
let with_in: Vec<_> = g.entities_with_incoming().unwrap();
let ids: Vec<&str> = with_in.iter().map(|e| e.id.as_str()).collect();
assert!(ids.contains(&"dst"));
assert!(!ids.contains(&"src"));
assert!(!ids.contains(&"iso"));
}
#[test]
fn test_entities_with_incoming_empty_for_no_relationships() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
assert!(g.entities_with_incoming().unwrap().is_empty());
}
#[test]
fn test_entities_with_no_relationships_returns_sink_nodes() {
let g = GraphStore::new();
g.add_entity(Entity::new("src", "N")).unwrap();
g.add_entity(Entity::new("sink", "N")).unwrap();
g.add_relationship(Relationship::new("src", "sink", "E", 1.0)).unwrap();
let sinks = g.entities_with_no_relationships().unwrap();
let ids: Vec<&str> = sinks.iter().map(|e| e.id.as_str()).collect();
assert!(ids.contains(&"sink"));
assert!(!ids.contains(&"src"));
}
#[test]
fn test_entities_with_no_relationships_all_when_no_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
assert_eq!(g.entities_with_no_relationships().unwrap().len(), 2);
}
#[test]
fn test_entities_with_no_relationships_empty_for_empty_graph() {
let g = GraphStore::new();
assert!(g.entities_with_no_relationships().unwrap().is_empty());
}
#[test]
fn test_entity_ids_sorted_returns_alphabetical_ids() {
let g = GraphStore::new();
g.add_entity(Entity::new("zebra", "N")).unwrap();
g.add_entity(Entity::new("alpha", "N")).unwrap();
g.add_entity(Entity::new("mango", "N")).unwrap();
let ids = g.entity_ids_sorted().unwrap();
assert_eq!(ids[0].0, "alpha");
assert_eq!(ids[1].0, "mango");
assert_eq!(ids[2].0, "zebra");
}
#[test]
fn test_entity_ids_sorted_empty_for_empty_graph() {
let g = GraphStore::new();
assert!(g.entity_ids_sorted().unwrap().is_empty());
}
#[test]
fn test_relationship_count_for_returns_out_degree() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "k", 1.0)).unwrap();
let a_id = EntityId("a".to_string());
assert_eq!(g.relationship_count_for(&a_id).unwrap(), 2);
}
#[test]
fn test_relationship_count_for_zero_for_unknown_entity() {
let g = GraphStore::new();
let id = EntityId("unknown".to_string());
assert_eq!(g.relationship_count_for(&id).unwrap(), 0);
}
#[test]
fn test_entity_pair_has_relationship_true_when_edge_exists() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
let a = EntityId("a".to_string());
let b = EntityId("b".to_string());
assert!(g.entity_pair_has_relationship(&a, &b).unwrap());
}
#[test]
fn test_entity_pair_has_relationship_false_when_no_edge() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
let a = EntityId("a".to_string());
let b = EntityId("b".to_string());
assert!(!g.entity_pair_has_relationship(&a, &b).unwrap());
}
#[test]
fn test_nodes_reachable_from_returns_all_reachable_nodes() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_entity(Entity::new("d", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "E", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "E", 1.0)).unwrap();
let start = EntityId("a".to_string());
let reachable = g.nodes_reachable_from(&start).unwrap();
let ids: Vec<&str> = reachable.iter().map(|id| id.0.as_str()).collect();
assert!(ids.contains(&"b"));
assert!(ids.contains(&"c"));
assert!(!ids.contains(&"a"));
assert!(!ids.contains(&"d"));
}
#[test]
fn test_nodes_reachable_from_empty_for_isolated_node() {
let g = GraphStore::new();
g.add_entity(Entity::new("iso", "N")).unwrap();
let id = EntityId("iso".to_string());
assert!(g.nodes_reachable_from(&id).unwrap().is_empty());
}
#[test]
fn test_self_loops_returns_loop_relationships() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "b", "other", 1.0)).unwrap();
let loops = g.self_loops().unwrap();
assert_eq!(loops.len(), 1);
assert_eq!(loops[0].from.as_str(), "a");
assert_eq!(loops[0].to.as_str(), "a");
}
#[test]
fn test_self_loops_empty_when_no_loops() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "E", 1.0)).unwrap();
assert!(g.self_loops().unwrap().is_empty());
}
#[test]
fn test_entities_with_label_and_property_returns_matching() {
let g = GraphStore::new();
let mut e = Entity::new("a", "Person");
e.properties.insert("age".to_string(), serde_json::json!("30"));
g.add_entity(e).unwrap();
g.add_entity(Entity::new("b", "Person")).unwrap();
g.add_entity(Entity::new("c", "Robot")).unwrap();
let result = g.entities_with_label_and_property("Person", "age").unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].id.as_str(), "a");
}
#[test]
fn test_entities_with_label_and_property_empty_when_no_match() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "Person")).unwrap();
assert!(g.entities_with_label_and_property("Person", "age").unwrap().is_empty());
}
#[test]
fn test_total_edge_weight_sums_all_weights() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "E", 2.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "E", 3.0)).unwrap();
let total = g.total_edge_weight().unwrap();
assert!((total - 5.0).abs() < 1e-9);
}
#[test]
fn test_total_edge_weight_zero_for_no_relationships() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
assert!((g.total_edge_weight().unwrap()).abs() < 1e-9);
}
#[test]
fn test_entity_with_max_out_degree_returns_highest_degree_entity() {
let g = GraphStore::new();
g.add_entity(Entity::new("hub", "N")).unwrap();
g.add_entity(Entity::new("spoke1", "N")).unwrap();
g.add_entity(Entity::new("spoke2", "N")).unwrap();
g.add_entity(Entity::new("leaf", "N")).unwrap();
g.add_relationship(Relationship::new("hub", "spoke1", "E", 1.0)).unwrap();
g.add_relationship(Relationship::new("hub", "spoke2", "E", 1.0)).unwrap();
g.add_relationship(Relationship::new("spoke1", "leaf", "E", 1.0)).unwrap();
let top = g.entity_with_max_out_degree().unwrap().unwrap();
assert_eq!(top.id.as_str(), "hub");
}
#[test]
fn test_entity_with_max_out_degree_none_for_empty_graph() {
let g = GraphStore::new();
assert!(g.entity_with_max_out_degree().unwrap().is_none());
}
#[test]
fn test_edge_weight_between_returns_weight_when_edge_exists() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "k", 2.5)).unwrap();
let a = EntityId("a".to_string());
let b = EntityId("b".to_string());
assert_eq!(g.edge_weight_between(&a, &b).unwrap(), Some(2.5));
}
#[test]
fn test_edge_weight_between_none_when_no_edge() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
let a = EntityId("a".to_string());
let b = EntityId("b".to_string());
assert_eq!(g.edge_weight_between(&a, &b).unwrap(), None);
}
#[test]
fn test_total_relationship_weight_sums_all_weights() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "k", 3.0)).unwrap();
assert_eq!(g.total_relationship_weight().unwrap(), 4.0);
}
#[test]
fn test_total_relationship_weight_zero_for_no_edges() {
let g = GraphStore::new();
assert_eq!(g.total_relationship_weight().unwrap(), 0.0);
}
#[test]
fn test_avg_weight_for_kind_returns_mean() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "KNOWS", 3.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "LIKES", 5.0)).unwrap();
assert_eq!(g.avg_weight_for_kind("KNOWS").unwrap(), 2.0);
}
#[test]
fn test_avg_weight_for_kind_zero_for_absent_kind() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "KNOWS", 1.0)).unwrap();
assert_eq!(g.avg_weight_for_kind("MISSING").unwrap(), 0.0);
}
#[test]
fn test_entity_degree_ratio_returns_fraction_of_total() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_entity(Entity::new("d", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "k", 1.0)).unwrap();
let a_id = EntityId("a".to_string());
assert_eq!(g.entity_degree_ratio(&a_id).unwrap(), 0.5);
}
#[test]
fn test_entity_degree_ratio_zero_for_empty_graph() {
let g = GraphStore::new();
let id = EntityId("x".to_string());
assert_eq!(g.entity_degree_ratio(&id).unwrap(), 0.0);
}
#[test]
fn test_entity_has_outgoing_edge_true_when_edge_exists() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
let id = EntityId("a".to_string());
assert!(g.entity_has_outgoing_edge(&id).unwrap());
}
#[test]
fn test_entity_has_outgoing_edge_false_for_sink_node() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
let id = EntityId("b".to_string());
assert!(!g.entity_has_outgoing_edge(&id).unwrap());
}
#[test]
fn test_count_nodes_with_self_loop_counts_correctly() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "a", "fwd", 1.0)).unwrap();
assert_eq!(g.count_nodes_with_self_loop().unwrap(), 1);
}
#[test]
fn test_count_nodes_with_self_loop_zero_for_no_loops() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
assert_eq!(g.count_nodes_with_self_loop().unwrap(), 0);
}
#[test]
fn test_relationship_count_for_entity_correct() {
let g = GraphStore::new();
g.add_entity(Entity::new("hub", "N")).unwrap();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("hub", "a", "E", 1.0)).unwrap();
g.add_relationship(Relationship::new("hub", "b", "E", 1.0)).unwrap();
let hub_id = EntityId("hub".to_string());
assert_eq!(g.relationship_count_for_entity(&hub_id).unwrap(), 2);
}
#[test]
fn test_relationship_count_for_entity_zero_for_unknown() {
let g = GraphStore::new();
let id = EntityId("nobody".to_string());
assert_eq!(g.relationship_count_for_entity(&id).unwrap(), 0);
}
#[test]
fn test_entities_by_label_returns_matching_ids() {
let g = GraphStore::new();
g.add_entity(Entity::new("a1", "Person")).unwrap();
g.add_entity(Entity::new("a2", "Person")).unwrap();
g.add_entity(Entity::new("a3", "Place")).unwrap();
let mut ids = g.entities_by_label("Person").unwrap();
ids.sort_by(|a, b| a.as_str().cmp(b.as_str()));
assert_eq!(ids.len(), 2);
assert_eq!(ids[0].as_str(), "a1");
assert_eq!(ids[1].as_str(), "a2");
}
#[test]
fn test_entities_by_label_empty_for_unknown_label() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "Thing")).unwrap();
let ids = g.entities_by_label("Nonexistent").unwrap();
assert!(ids.is_empty());
}
#[test]
fn test_edge_count_between_returns_count() {
let g = GraphStore::new();
g.add_entity(Entity::new("u", "N")).unwrap();
g.add_entity(Entity::new("v", "N")).unwrap();
g.add_relationship(Relationship::new("u", "v", "LINKS", 1.0)).unwrap();
g.add_relationship(Relationship::new("u", "v", "ALSO", 1.0)).unwrap();
let from = EntityId::new("u");
let to = EntityId::new("v");
assert_eq!(g.edge_count_between(&from, &to).unwrap(), 2);
}
#[test]
fn test_edge_count_between_zero_when_no_edge() {
let g = GraphStore::new();
g.add_entity(Entity::new("u2", "N")).unwrap();
g.add_entity(Entity::new("v2", "N")).unwrap();
let from = EntityId::new("u2");
let to = EntityId::new("v2");
assert_eq!(g.edge_count_between(&from, &to).unwrap(), 0);
}
#[test]
fn test_is_connected_true_when_edge_exists() {
let g = GraphStore::new();
g.add_entity(Entity::new("p", "N")).unwrap();
g.add_entity(Entity::new("q", "N")).unwrap();
g.add_relationship(Relationship::new("p", "q", "LINKS", 1.0)).unwrap();
let p = EntityId::new("p");
let q = EntityId::new("q");
assert!(g.is_connected(&p, &q).unwrap());
}
#[test]
fn test_is_connected_false_when_no_edge() {
let g = GraphStore::new();
g.add_entity(Entity::new("p2", "N")).unwrap();
g.add_entity(Entity::new("q2", "N")).unwrap();
let p2 = EntityId::new("p2");
let q2 = EntityId::new("q2");
assert!(!g.is_connected(&p2, &q2).unwrap());
}
#[test]
fn test_graph_is_empty_true_for_new_store() {
let g = GraphStore::new();
assert!(g.graph_is_empty().unwrap());
}
#[test]
fn test_graph_is_empty_false_after_adding_entity() {
let g = GraphStore::new();
g.add_entity(Entity::new("e", "N")).unwrap();
assert!(!g.graph_is_empty().unwrap());
}
#[test]
fn test_unique_relationship_kinds_returns_sorted_kinds() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "N")).unwrap();
g.add_entity(Entity::new("y", "N")).unwrap();
g.add_entity(Entity::new("z", "N")).unwrap();
g.add_relationship(Relationship::new("x", "y", "ZEBRA", 1.0)).unwrap();
g.add_relationship(Relationship::new("x", "z", "APPLE", 1.0)).unwrap();
g.add_relationship(Relationship::new("y", "z", "APPLE", 1.0)).unwrap();
let kinds = g.unique_relationship_kinds().unwrap();
assert_eq!(kinds, vec!["APPLE".to_string(), "ZEBRA".to_string()]);
}
#[test]
fn test_unique_relationship_kinds_empty_for_new_graph() {
let g = GraphStore::new();
assert!(g.unique_relationship_kinds().unwrap().is_empty());
}
#[test]
fn test_has_any_relationships_true_when_edge_added() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "R", 1.0)).unwrap();
assert!(g.has_any_relationships().unwrap());
}
#[test]
fn test_has_any_relationships_false_for_new_graph() {
let g = GraphStore::new();
assert!(!g.has_any_relationships().unwrap());
}
#[test]
fn test_avg_edge_weight_correct() {
let g = GraphStore::new();
g.add_entity(Entity::new("n1", "N")).unwrap();
g.add_entity(Entity::new("n2", "N")).unwrap();
g.add_relationship(Relationship::new("n1", "n2", "E", 2.0)).unwrap();
g.add_relationship(Relationship::new("n1", "n2", "F", 4.0)).unwrap();
assert!((g.avg_edge_weight().unwrap() - 3.0).abs() < 1e-6);
}
#[test]
fn test_avg_edge_weight_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.avg_edge_weight().unwrap(), 0.0);
}
#[test]
fn test_nodes_with_no_outgoing_returns_sink_nodes() {
let g = GraphStore::new();
g.add_entity(Entity::new("src", "N")).unwrap();
g.add_entity(Entity::new("sink", "N")).unwrap();
g.add_entity(Entity::new("iso", "N")).unwrap();
g.add_relationship(Relationship::new("src", "sink", "E", 1.0)).unwrap();
let sinks = g.nodes_with_no_outgoing().unwrap();
let ids: Vec<&str> = sinks.iter().map(|e| e.id.as_str()).collect();
assert!(ids.contains(&"sink"));
assert!(ids.contains(&"iso"));
assert!(!ids.contains(&"src"));
}
#[test]
fn test_nodes_with_no_outgoing_all_for_graph_with_no_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
assert_eq!(g.nodes_with_no_outgoing().unwrap().len(), 2);
}
#[test]
fn test_relationship_kinds_returns_sorted_unique_kinds() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "follows", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "likes", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "follows", 1.0)).unwrap();
let kinds = g.relationship_kinds().unwrap();
assert_eq!(kinds, vec!["follows", "likes"]);
}
#[test]
fn test_relationship_kinds_empty_for_empty_graph() {
let g = GraphStore::new();
assert!(g.relationship_kinds().unwrap().is_empty());
}
#[test]
fn test_max_out_degree_returns_correct_max() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "k", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "k", 1.0)).unwrap();
assert_eq!(g.max_out_degree().unwrap(), 2);
}
#[test]
fn test_in_degree_of_counts_incoming_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("src1", "N")).unwrap();
g.add_entity(Entity::new("src2", "N")).unwrap();
g.add_entity(Entity::new("dst", "N")).unwrap();
g.add_relationship(Relationship::new("src1", "dst", "k", 1.0)).unwrap();
g.add_relationship(Relationship::new("src2", "dst", "k", 1.0)).unwrap();
let dst = EntityId("dst".to_string());
assert_eq!(g.in_degree_of(&dst).unwrap(), 2);
}
#[test]
fn test_in_degree_of_zero_for_node_with_no_incoming() {
let g = GraphStore::new();
g.add_entity(Entity::new("alone", "N")).unwrap();
let id = EntityId("alone".to_string());
assert_eq!(g.in_degree_of(&id).unwrap(), 0);
}
#[test]
fn test_total_weight_for_kind_sums_correctly() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 2.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "link", 4.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "other", 1.0)).unwrap();
let sum = g.total_weight_for_kind("link").unwrap();
assert!((sum - 6.0).abs() < 1e-9);
}
#[test]
fn test_total_weight_for_kind_zero_for_unknown_kind() {
let g = GraphStore::new();
assert_eq!(g.total_weight_for_kind("nope").unwrap(), 0.0);
}
#[test]
fn test_entity_ids_with_label_returns_matching_ids() {
let g = GraphStore::new();
g.add_entity(Entity::new("e1", "Person")).unwrap();
g.add_entity(Entity::new("e2", "Person")).unwrap();
g.add_entity(Entity::new("e3", "Place")).unwrap();
let mut ids = g.entity_ids_with_label("Person").unwrap();
ids.sort_by(|a, b| a.0.cmp(&b.0));
let strs: Vec<&str> = ids.iter().map(|id| id.0.as_str()).collect();
assert_eq!(strs, vec!["e1", "e2"]);
}
#[test]
fn test_entity_ids_with_label_empty_for_unknown_label() {
let g = GraphStore::new();
assert!(g.entity_ids_with_label("Unknown").unwrap().is_empty());
}
#[test]
fn test_out_degree_of_returns_edge_count() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "k", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "k", 1.0)).unwrap();
let id = EntityId("a".to_string());
assert_eq!(g.out_degree_of(&id).unwrap(), 2);
}
#[test]
fn test_out_degree_of_zero_for_sink_node() {
let g = GraphStore::new();
g.add_entity(Entity::new("sink", "N")).unwrap();
let id = EntityId("sink".to_string());
assert_eq!(g.out_degree_of(&id).unwrap(), 0);
}
#[test]
fn test_has_relationship_with_kind_true_when_kind_present() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "follows", 1.0)).unwrap();
assert!(g.has_relationship_with_kind("follows").unwrap());
}
#[test]
fn test_has_relationship_with_kind_false_for_absent_kind() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "likes", 1.0)).unwrap();
assert!(!g.has_relationship_with_kind("follows").unwrap());
}
#[test]
fn test_labels_unique_count_returns_distinct_count() {
let g = GraphStore::new();
g.add_entity(Entity::new("e1", "Person")).unwrap();
g.add_entity(Entity::new("e2", "Person")).unwrap();
g.add_entity(Entity::new("e3", "Place")).unwrap();
assert_eq!(g.labels_unique_count().unwrap(), 2);
}
#[test]
fn test_labels_unique_count_zero_for_empty_graph() {
let g = GraphStore::new();
assert_eq!(g.labels_unique_count().unwrap(), 0);
}
#[test]
fn test_relationships_from_returns_outgoing_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "b", "link", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "c", "link", 1.0)).unwrap();
let from = EntityId("a".to_string());
assert_eq!(g.relationships_from(&from).unwrap().len(), 2);
}
#[test]
fn test_relationships_from_empty_for_node_with_no_edges() {
let g = GraphStore::new();
g.add_entity(Entity::new("lone", "N")).unwrap();
let id = EntityId("lone".to_string());
assert!(g.relationships_from(&id).unwrap().is_empty());
}
#[test]
fn test_cycle_count_counts_self_loops() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_relationship(Relationship::new("a", "a", "self", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "b", "self", 1.0)).unwrap();
g.add_relationship(Relationship::new("a", "b", "edge", 1.0)).unwrap();
assert_eq!(g.cycle_count().unwrap(), 2);
}
#[test]
fn test_cycle_count_zero_when_no_self_loops() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "N")).unwrap();
g.add_entity(Entity::new("y", "N")).unwrap();
g.add_relationship(Relationship::new("x", "y", "link", 1.0)).unwrap();
assert_eq!(g.cycle_count().unwrap(), 0);
}
#[test]
fn test_entities_with_property_key_returns_matching_entities() {
let g = GraphStore::new();
let e1 = Entity::new("e1", "N").with_property("color", serde_json::json!("red"));
let e2 = Entity::new("e2", "N");
g.add_entity(e1).unwrap();
g.add_entity(e2).unwrap();
let result = g.entities_with_property_key("color").unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].id.0, "e1");
}
#[test]
fn test_entity_properties_count_returns_count_of_matching_entities() {
let g = GraphStore::new();
let e1 = Entity::new("p1", "N").with_property("tag", serde_json::json!("x"));
let e2 = Entity::new("p2", "N").with_property("tag", serde_json::json!("y"));
let e3 = Entity::new("p3", "N");
g.add_entity(e1).unwrap();
g.add_entity(e2).unwrap();
g.add_entity(e3).unwrap();
assert_eq!(g.entity_properties_count("tag").unwrap(), 2);
}
#[test]
fn test_all_entities_have_properties_true_when_all_have_props() {
let g = GraphStore::new();
let e1 = Entity::new("a", "N").with_property("k", "v".into());
let e2 = Entity::new("b", "N").with_property("k", "v".into());
g.add_entity(e1).unwrap();
g.add_entity(e2).unwrap();
assert!(g.all_entities_have_properties().unwrap());
}
#[test]
fn test_all_entities_have_properties_false_when_one_has_none() {
let g = GraphStore::new();
let e1 = Entity::new("a", "N").with_property("k", "v".into());
let e2 = Entity::new("b", "N");
g.add_entity(e1).unwrap();
g.add_entity(e2).unwrap();
assert!(!g.all_entities_have_properties().unwrap());
}
#[test]
fn test_all_entities_have_properties_true_for_empty_graph() {
let g = GraphStore::new();
assert!(g.all_entities_have_properties().unwrap());
}
#[test]
fn test_edges_to_returns_incoming_relationships() {
let g = GraphStore::new();
g.add_entity(Entity::new("a", "N")).unwrap();
g.add_entity(Entity::new("b", "N")).unwrap();
g.add_entity(Entity::new("c", "N")).unwrap();
g.add_relationship(Relationship::new("a", "c", "EDGE", 1.0)).unwrap();
g.add_relationship(Relationship::new("b", "c", "EDGE", 1.0)).unwrap();
let incoming = g.edges_to(&EntityId("c".into())).unwrap();
assert_eq!(incoming.len(), 2);
}
#[test]
fn test_edges_to_empty_for_no_incoming() {
let g = GraphStore::new();
g.add_entity(Entity::new("x", "N")).unwrap();
assert!(g.edges_to(&EntityId("x".into())).unwrap().is_empty());
}
#[test]
fn test_entity_has_property_value_true_when_matches() {
let g = GraphStore::new();
g.add_entity(Entity::new("e1", "N").with_property("color", serde_json::json!("blue"))).unwrap();
assert!(g.entity_has_property_value(&EntityId("e1".into()), "color", "blue").unwrap());
}
#[test]
fn test_entity_has_property_value_false_for_unknown_entity() {
let g = GraphStore::new();
assert!(!g.entity_has_property_value(&EntityId("nope".into()), "color", "blue").unwrap());
}
}