use crate::models::Relationship;
use anyhow::Result;
use petgraph::{Directed, Graph};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize)]
#[must_use = "validation results should be checked for circular dependencies and self-references"]
pub struct RelationshipValidationResult {
pub circular_dependencies: Vec<CircularDependency>,
pub self_references: Vec<SelfReference>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CircularDependency {
pub relationship_id: Uuid,
pub cycle_path: Vec<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelfReference {
pub relationship_id: Uuid,
pub table_id: Uuid,
}
#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
pub enum RelationshipValidationError {
#[error("Validation error: {0}")]
ValidationError(String),
}
#[derive(Default)]
pub struct RelationshipValidator;
impl RelationshipValidator {
pub fn new() -> Self {
Self
}
pub fn check_circular_dependency(
&self,
relationships: &[Relationship],
source_table_id: Uuid,
target_table_id: Uuid,
) -> Result<(bool, Option<Vec<Uuid>>), RelationshipValidationError> {
let mut graph = Graph::<Uuid, Uuid, Directed>::new();
let mut node_map = std::collections::HashMap::new();
for rel in relationships {
let source_node = *node_map
.entry(rel.source_table_id)
.or_insert_with(|| graph.add_node(rel.source_table_id));
let target_node = *node_map
.entry(rel.target_table_id)
.or_insert_with(|| graph.add_node(rel.target_table_id));
graph.add_edge(source_node, target_node, rel.id);
}
let source_node = *node_map
.entry(source_table_id)
.or_insert_with(|| graph.add_node(source_table_id));
let target_node = *node_map
.entry(target_table_id)
.or_insert_with(|| graph.add_node(target_table_id));
let edge_id = Uuid::new_v4();
graph.add_edge(source_node, target_node, edge_id);
if self.can_reach(&graph, &node_map, target_table_id, source_table_id) {
let cycle_path = self
.find_path(&graph, &node_map, target_table_id, source_table_id)
.unwrap_or_default();
return Ok((true, Some(cycle_path)));
}
Ok((false, None))
}
fn can_reach(
&self,
graph: &Graph<Uuid, Uuid, Directed>,
node_map: &std::collections::HashMap<Uuid, petgraph::graph::NodeIndex>,
from: Uuid,
to: Uuid,
) -> bool {
if let (Some(&from_idx), Some(&to_idx)) = (node_map.get(&from), node_map.get(&to)) {
let mut visited = std::collections::HashSet::new();
let mut stack = vec![from_idx];
while let Some(node) = stack.pop() {
if node == to_idx {
return true;
}
if visited.insert(node) {
for neighbor in graph.neighbors(node) {
if !visited.contains(&neighbor) {
stack.push(neighbor);
}
}
}
}
}
false
}
fn find_path(
&self,
graph: &Graph<Uuid, Uuid, Directed>,
node_map: &std::collections::HashMap<Uuid, petgraph::graph::NodeIndex>,
from: Uuid,
to: Uuid,
) -> Option<Vec<Uuid>> {
if let (Some(&from_idx), Some(&to_idx)) = (node_map.get(&from), node_map.get(&to)) {
let mut visited = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
let mut parent = std::collections::HashMap::new();
queue.push_back(from_idx);
visited.insert(from_idx);
while let Some(node) = queue.pop_front() {
if node == to_idx {
let mut path = Vec::new();
let mut current = Some(to_idx);
while let Some(node_idx) = current {
path.push(graph[node_idx]);
current = parent.get(&node_idx).copied();
}
path.reverse();
return Some(path);
}
for neighbor in graph.neighbors(node) {
if !visited.contains(&neighbor) {
visited.insert(neighbor);
parent.insert(neighbor, node);
queue.push_back(neighbor);
}
}
}
}
None
}
pub fn validate_no_self_reference(
&self,
source_table_id: Uuid,
target_table_id: Uuid,
) -> Result<(), SelfReference> {
if source_table_id == target_table_id {
return Err(SelfReference {
relationship_id: Uuid::new_v4(),
table_id: source_table_id,
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_self_reference() {
let id = Uuid::new_v4();
let err = RelationshipValidator::new()
.validate_no_self_reference(id, id)
.unwrap_err();
assert_eq!(err.table_id, id);
}
#[test]
fn detects_cycle() {
let a = Uuid::new_v4();
let b = Uuid::new_v4();
let rel1 = Relationship::new(a, b);
let validator = RelationshipValidator::new();
let (has_cycle, path) = validator.check_circular_dependency(&[rel1], b, a).unwrap();
assert!(has_cycle);
assert!(path.is_some());
}
}