data_modelling_sdk/validation/
relationships.rs1use crate::models::Relationship;
8use anyhow::Result;
9use petgraph::{Directed, Graph};
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13#[derive(Debug, Serialize, Deserialize)]
17#[must_use = "validation results should be checked for circular dependencies and self-references"]
18pub struct RelationshipValidationResult {
19 pub circular_dependencies: Vec<CircularDependency>,
21 pub self_references: Vec<SelfReference>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CircularDependency {
28 pub relationship_id: Uuid,
29 pub cycle_path: Vec<Uuid>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SelfReference {
35 pub relationship_id: Uuid,
36 pub table_id: Uuid,
37}
38
39#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
41pub enum RelationshipValidationError {
42 #[error("Validation error: {0}")]
43 ValidationError(String),
44}
45
46#[derive(Default)]
48pub struct RelationshipValidator;
49
50impl RelationshipValidator {
51 pub fn new() -> Self {
61 Self
62 }
63
64 pub fn check_circular_dependency(
101 &self,
102 relationships: &[Relationship],
103 source_table_id: Uuid,
104 target_table_id: Uuid,
105 ) -> Result<(bool, Option<Vec<Uuid>>), RelationshipValidationError> {
106 let mut graph = Graph::<Uuid, Uuid, Directed>::new();
108 let mut node_map = std::collections::HashMap::new();
109
110 for rel in relationships {
112 let source_node = *node_map
113 .entry(rel.source_table_id)
114 .or_insert_with(|| graph.add_node(rel.source_table_id));
115 let target_node = *node_map
116 .entry(rel.target_table_id)
117 .or_insert_with(|| graph.add_node(rel.target_table_id));
118 graph.add_edge(source_node, target_node, rel.id);
119 }
120
121 let source_node = *node_map
123 .entry(source_table_id)
124 .or_insert_with(|| graph.add_node(source_table_id));
125 let target_node = *node_map
126 .entry(target_table_id)
127 .or_insert_with(|| graph.add_node(target_table_id));
128 let edge_id = Uuid::new_v4();
130 graph.add_edge(source_node, target_node, edge_id);
131
132 if self.can_reach(&graph, &node_map, target_table_id, source_table_id) {
136 let cycle_path = self
138 .find_path(&graph, &node_map, target_table_id, source_table_id)
139 .unwrap_or_default();
140 return Ok((true, Some(cycle_path)));
141 }
142
143 Ok((false, None))
144 }
145
146 fn can_reach(
148 &self,
149 graph: &Graph<Uuid, Uuid, Directed>,
150 node_map: &std::collections::HashMap<Uuid, petgraph::graph::NodeIndex>,
151 from: Uuid,
152 to: Uuid,
153 ) -> bool {
154 if let (Some(&from_idx), Some(&to_idx)) = (node_map.get(&from), node_map.get(&to)) {
155 let mut visited = std::collections::HashSet::new();
157 let mut stack = vec![from_idx];
158
159 while let Some(node) = stack.pop() {
160 if node == to_idx {
161 return true;
162 }
163 if visited.insert(node) {
164 for neighbor in graph.neighbors(node) {
165 if !visited.contains(&neighbor) {
166 stack.push(neighbor);
167 }
168 }
169 }
170 }
171 }
172 false
173 }
174
175 fn find_path(
177 &self,
178 graph: &Graph<Uuid, Uuid, Directed>,
179 node_map: &std::collections::HashMap<Uuid, petgraph::graph::NodeIndex>,
180 from: Uuid,
181 to: Uuid,
182 ) -> Option<Vec<Uuid>> {
183 if let (Some(&from_idx), Some(&to_idx)) = (node_map.get(&from), node_map.get(&to)) {
184 let mut visited = std::collections::HashSet::new();
186 let mut queue = std::collections::VecDeque::new();
187 let mut parent = std::collections::HashMap::new();
188
189 queue.push_back(from_idx);
190 visited.insert(from_idx);
191
192 while let Some(node) = queue.pop_front() {
193 if node == to_idx {
194 let mut path = Vec::new();
196 let mut current = Some(to_idx);
197 while let Some(node_idx) = current {
198 path.push(graph[node_idx]);
199 current = parent.get(&node_idx).copied();
200 }
201 path.reverse();
202 return Some(path);
203 }
204
205 for neighbor in graph.neighbors(node) {
206 if !visited.contains(&neighbor) {
207 visited.insert(neighbor);
208 parent.insert(neighbor, node);
209 queue.push_back(neighbor);
210 }
211 }
212 }
213 }
214 None
215 }
216
217 pub fn validate_no_self_reference(
219 &self,
220 source_table_id: Uuid,
221 target_table_id: Uuid,
222 ) -> Result<(), SelfReference> {
223 if source_table_id == target_table_id {
224 return Err(SelfReference {
225 relationship_id: Uuid::new_v4(),
226 table_id: source_table_id,
227 });
228 }
229 Ok(())
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn detects_self_reference() {
239 let id = Uuid::new_v4();
240 let err = RelationshipValidator::new()
241 .validate_no_self_reference(id, id)
242 .unwrap_err();
243 assert_eq!(err.table_id, id);
244 }
245
246 #[test]
247 fn detects_cycle() {
248 let a = Uuid::new_v4();
249 let b = Uuid::new_v4();
250 let rel1 = Relationship::new(a, b);
251 let validator = RelationshipValidator::new();
252 let (has_cycle, path) = validator.check_circular_dependency(&[rel1], b, a).unwrap();
254 assert!(has_cycle);
255 assert!(path.is_some());
256 }
257}