reddb_server/application/
migration_graph.rs1use std::collections::{HashMap, HashSet, VecDeque};
8
9#[derive(Debug, PartialEq, Eq)]
10pub struct CycleError {
11 pub involved: Vec<String>,
12}
13
14impl std::fmt::Display for CycleError {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 write!(
17 f,
18 "dependency cycle detected among: {}",
19 self.involved.join(", ")
20 )
21 }
22}
23
24pub fn topological_sort(
29 migrations: &[String],
30 edges: &[(String, String)],
31) -> Result<Vec<String>, CycleError> {
32 let node_set: HashSet<&str> = migrations.iter().map(|s| s.as_str()).collect();
35
36 let mut in_degree: HashMap<&str, usize> =
37 migrations.iter().map(|s| (s.as_str(), 0usize)).collect();
38
39 let mut dependents: HashMap<&str, Vec<&str>> = HashMap::new();
41
42 for (migration, dep) in edges {
43 if !node_set.contains(migration.as_str()) || !node_set.contains(dep.as_str()) {
45 continue;
46 }
47 *in_degree.entry(migration.as_str()).or_insert(0) += 1;
48 dependents
49 .entry(dep.as_str())
50 .or_default()
51 .push(migration.as_str());
52 }
53
54 let mut queue: VecDeque<&str> = in_degree
56 .iter()
57 .filter(|(_, °)| deg == 0)
58 .map(|(&name, _)| name)
59 .collect();
60
61 let mut queue_vec: Vec<&str> = queue.drain(..).collect();
63 queue_vec.sort();
64 queue.extend(queue_vec);
65
66 let mut sorted: Vec<String> = Vec::with_capacity(migrations.len());
67
68 while let Some(node) = queue.pop_front() {
69 sorted.push(node.to_string());
70 if let Some(deps) = dependents.get(node) {
71 let mut next: Vec<&str> = Vec::new();
72 for &dependent in deps {
73 let deg = in_degree.entry(dependent).or_insert(0);
74 *deg = deg.saturating_sub(1);
75 if *deg == 0 {
76 next.push(dependent);
77 }
78 }
79 next.sort();
80 queue.extend(next);
81 }
82 }
83
84 if sorted.len() != migrations.len() {
85 let mut involved: Vec<String> = in_degree
87 .iter()
88 .filter(|(_, °)| deg > 0)
89 .map(|(&name, _)| name.to_string())
90 .collect();
91 involved.sort();
92 return Err(CycleError { involved });
93 }
94
95 Ok(sorted)
96}
97
98pub fn would_create_cycle(existing_edges: &[(String, String)], from: &str, to: &str) -> bool {
104 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
106 for (m, dep) in existing_edges {
107 adj.entry(m.as_str()).or_default().push(dep.as_str());
108 }
109
110 let mut visited: HashSet<&str> = HashSet::new();
112 let mut stack = vec![to];
113 while let Some(node) = stack.pop() {
114 if node == from {
115 return true;
116 }
117 if visited.insert(node) {
118 if let Some(deps) = adj.get(node) {
119 stack.extend(deps.iter().copied());
120 }
121 }
122 }
123 false
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 fn s(s: &str) -> String {
131 s.to_string()
132 }
133
134 #[test]
135 fn sorts_linear_chain() {
136 let migrations = vec![s("c"), s("b"), s("a")];
137 let edges = vec![(s("b"), s("a")), (s("c"), s("b"))];
138 let result = topological_sort(&migrations, &edges).unwrap();
139 assert_eq!(result, vec!["a", "b", "c"]);
140 }
141
142 #[test]
143 fn sorts_independent_nodes_alphabetically() {
144 let migrations = vec![s("z"), s("a"), s("m")];
145 let edges = vec![];
146 let result = topological_sort(&migrations, &edges).unwrap();
147 assert_eq!(result, vec!["a", "m", "z"]);
148 }
149
150 #[test]
151 fn detects_cycle() {
152 let migrations = vec![s("a"), s("b"), s("c")];
153 let edges = vec![(s("a"), s("b")), (s("b"), s("c")), (s("c"), s("a"))];
154 let err = topological_sort(&migrations, &edges).unwrap_err();
155 assert!(err.involved.contains(&s("a")));
156 assert!(err.involved.contains(&s("b")));
157 assert!(err.involved.contains(&s("c")));
158 }
159
160 #[test]
161 fn detects_self_cycle() {
162 let migrations = vec![s("a")];
163 let edges = vec![(s("a"), s("a"))];
164 let err = topological_sort(&migrations, &edges).unwrap_err();
165 assert!(err.involved.contains(&s("a")));
166 }
167
168 #[test]
169 fn would_create_cycle_detects_indirect() {
170 let edges = vec![(s("a"), s("b")), (s("b"), s("c"))];
172 assert!(would_create_cycle(&edges, "c", "a"));
173 }
174
175 #[test]
176 fn would_create_cycle_allows_valid_edge() {
177 let edges = vec![(s("b"), s("a"))];
178 assert!(!would_create_cycle(&edges, "c", "b"));
179 }
180
181 #[test]
182 fn multi_root_dag() {
183 let migrations = vec![s("a"), s("b"), s("c"), s("d")];
185 let edges = vec![(s("c"), s("a")), (s("c"), s("b")), (s("d"), s("c"))];
186 let result = topological_sort(&migrations, &edges).unwrap();
187 let pos = |name: &str| result.iter().position(|x| x == name).unwrap();
189 assert!(pos("a") < pos("c"));
190 assert!(pos("b") < pos("c"));
191 assert!(pos("c") < pos("d"));
192 }
193}