use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet, VecDeque}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct RelationshipHop { pub from_table: String, pub to_table: String, pub fk_source_table: String, /// The link column's real name, the one the database answers to. Aliases /// change; a path stored in aliases would have to be rewritten every time /// one did, and it is read straight into SQL. pub fk_column: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct RelationshipLink { pub source_table: String, pub target_table: String, /// The link column's real name. A table may link to one target several /// times, so the target's name does not identify the column. pub fk_column: String, } pub fn resolve_relationship_path( start: &str, end: &str, links: &[RelationshipLink], ) -> Result, String> { if start == end { return Ok(Vec::new()); } let mut queue = VecDeque::from([(start.to_string(), Vec::new())]); let mut best_depth: HashMap = HashMap::from([(start.to_string(), 0)]); let mut matches = Vec::new(); let mut match_depth = None; while let Some((table, path)) = queue.pop_front() { if match_depth.is_some_and(|depth| path.len() >= depth) { continue; } for link in links { let next = if link.source_table == table { Some(link.target_table.as_str()) } else if link.target_table == table { Some(link.source_table.as_str()) } else { None }; let Some(next) = next else { continue }; if path .iter() .any(|hop: &RelationshipHop| hop.from_table == next) || next == start { continue; } let mut next_path = path.clone(); next_path.push(RelationshipHop { from_table: table.clone(), to_table: next.to_string(), fk_source_table: link.source_table.clone(), fk_column: link.fk_column.clone(), }); if next == end { match_depth = Some(next_path.len()); matches.push(next_path); continue; } let depth = next_path.len(); if best_depth.get(next).is_none_or(|known| depth <= *known) { best_depth.insert(next.to_string(), depth); queue.push_back((next.to_string(), next_path)); } } } match matches.as_slice() { [path] => Ok(path.clone()), [] => Err(format!("No FK path from '{}' to '{}'", start, end)), _ => Err(format!( "Multiple equally short FK paths from '{}' to '{}'", start, end )), } } pub fn resolve_anchored_relationship_path( source: &str, target: &str, anchor: &str, links: &[RelationshipLink], ) -> Result, String> { let mut path = resolve_relationship_path(source, anchor, links)?; path.extend(resolve_relationship_path(anchor, target, links)?); let mut tables = HashSet::from([source.to_string()]); if path.iter().any(|hop| !tables.insert(hop.to_table.clone())) { return Err(format!( "The FK route from '{}' to '{}' via '{}' revisits a table", source, target, anchor )); } Ok(path) }