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