1use std::collections::{HashMap, HashSet, VecDeque};
7
8use crate::error::{Result, WaypointError};
9use crate::migration::ResolvedMigration;
10
11fn depends_transitively(
16 edges: &HashMap<String, HashSet<String>>,
17 from: &str,
18 target: &str,
19) -> bool {
20 let mut stack = vec![from];
21 let mut seen: HashSet<&str> = HashSet::new();
22 while let Some(node) = stack.pop() {
23 if node == target {
24 return true;
25 }
26 if !seen.insert(node) {
27 continue;
28 }
29 if let Some(deps) = edges.get(node) {
30 stack.extend(deps.iter().map(|s| s.as_str()));
31 }
32 }
33 false
34}
35
36pub struct DependencyGraph {
38 edges: HashMap<String, HashSet<String>>,
40 reverse_edges: HashMap<String, HashSet<String>>,
42 all_versions: Vec<String>,
44}
45
46impl DependencyGraph {
47 pub fn build(migrations: &[&ResolvedMigration], implicit_chain: bool) -> Result<Self> {
52 let mut edges: HashMap<String, HashSet<String>> = HashMap::new();
53 let mut reverse_edges: HashMap<String, HashSet<String>> = HashMap::new();
54 let mut all_versions: Vec<String> = Vec::new();
55
56 let mut versioned: Vec<&ResolvedMigration> = migrations
58 .iter()
59 .filter(|m| m.is_versioned())
60 .copied()
61 .collect();
62 versioned.sort_by(|a, b| a.version().unwrap().cmp(b.version().unwrap()));
63
64 for m in &versioned {
65 let version = m.version().unwrap().raw.clone();
66 edges.entry(version.clone()).or_default();
67 reverse_edges.entry(version.clone()).or_default();
68 all_versions.push(version);
69 }
70
71 for m in &versioned {
73 let version = &m.version().unwrap().raw;
74 for dep in &m.directives.depends {
75 if !edges.contains_key(dep) {
76 return Err(WaypointError::MissingDependency {
77 version: version.clone(),
78 dependency: dep.clone(),
79 });
80 }
81 edges.get_mut(version.as_str()).unwrap().insert(dep.clone());
82 reverse_edges
83 .get_mut(dep.as_str())
84 .unwrap()
85 .insert(version.clone());
86 }
87 }
88
89 if implicit_chain {
103 for i in 1..all_versions.len() {
104 let current = all_versions[i].clone();
105 let previous = all_versions[i - 1].clone();
106
107 let has_explicit_deps = edges.get(¤t).is_some_and(|deps| !deps.is_empty());
108 if has_explicit_deps {
109 continue;
110 }
111 if depends_transitively(&edges, &previous, ¤t) {
112 log::debug!(
113 "Skipping implicit dependency {} -> {}: {} already depends on {}",
114 current,
115 previous,
116 previous,
117 current
118 );
119 continue;
120 }
121
122 edges
123 .entry(current.clone())
124 .or_default()
125 .insert(previous.clone());
126 reverse_edges.entry(previous).or_default().insert(current);
127 }
128 }
129
130 Ok(DependencyGraph {
131 edges,
132 reverse_edges,
133 all_versions,
134 })
135 }
136
137 pub fn topological_sort(&self) -> Result<Vec<String>> {
142 let mut in_degree: HashMap<&str, usize> = HashMap::new();
144 for v in &self.all_versions {
145 in_degree.insert(v, self.edges.get(v).map_or(0, |deps| deps.len()));
146 }
147
148 let mut queue: VecDeque<&str> = VecDeque::new();
150 for v in &self.all_versions {
151 if *in_degree.get(v.as_str()).unwrap_or(&0) == 0 {
152 queue.push_back(v);
153 }
154 }
155
156 let mut sorted = Vec::new();
157
158 while let Some(node) = queue.pop_front() {
159 sorted.push(node.to_string());
160
161 if let Some(dependents) = self.reverse_edges.get(node) {
163 for dep in dependents {
164 let deg = in_degree.get_mut(dep.as_str()).unwrap();
165 *deg -= 1;
166 if *deg == 0 {
167 queue.push_back(dep);
168 }
169 }
170 }
171 }
172
173 if sorted.len() != self.all_versions.len() {
174 let owned_in_degree: HashMap<String, usize> = in_degree
176 .iter()
177 .map(|(&k, &v)| (k.to_string(), v))
178 .collect();
179 let cycle_path = self.trace_cycle(&owned_in_degree);
180 return Err(WaypointError::DependencyCycle { path: cycle_path });
181 }
182
183 Ok(sorted)
184 }
185
186 fn trace_cycle(&self, in_degree: &HashMap<String, usize>) -> String {
188 let start = self
190 .all_versions
191 .iter()
192 .find(|v| *in_degree.get(*v).unwrap_or(&0) > 0);
193
194 let Some(start) = start else {
195 return "unknown cycle".to_string();
196 };
197
198 let mut path = vec![start.clone()];
200 let mut current = start.clone();
201 let mut visited = std::collections::HashSet::new();
202 visited.insert(current.clone());
203
204 loop {
205 let next = self
207 .edges
208 .get(¤t)
209 .and_then(|deps| deps.iter().find(|d| *in_degree.get(*d).unwrap_or(&0) > 0));
210
211 match next {
212 Some(n) => {
213 if !visited.insert(n.clone()) {
214 path.push(n.clone());
216 if let Some(pos) = path.iter().position(|v| v == n) {
218 let cycle: Vec<String> = path[pos..].to_vec();
219 return cycle.join(" -> ");
220 }
221 return path.join(" -> ");
222 }
223 path.push(n.clone());
224 current = n.clone();
225 }
226 None => {
227 let in_cycle: Vec<String> = self
229 .all_versions
230 .iter()
231 .filter(|v| *in_degree.get(*v).unwrap_or(&0) > 0)
232 .cloned()
233 .collect();
234 return format!("cycle involving: {}", in_cycle.join(", "));
235 }
236 }
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::directive::MigrationDirectives;
245 use crate::migration::{MigrationKind, MigrationVersion, ResolvedMigration};
246
247 fn make_migration(version: &str, depends: Vec<&str>) -> ResolvedMigration {
248 ResolvedMigration {
249 kind: MigrationKind::Versioned(MigrationVersion::parse(version).unwrap()),
250 description: format!("V{}", version),
251 script: format!("V{}__test.sql", version),
252 checksum: 0,
253 sql: String::new(),
254 directives: MigrationDirectives {
255 depends: depends.into_iter().map(String::from).collect(),
256 env: vec![],
257 ..Default::default()
258 },
259 }
260 }
261
262 #[test]
263 fn test_simple_chain() {
264 let m1 = make_migration("1", vec![]);
265 let m2 = make_migration("2", vec![]);
266 let m3 = make_migration("3", vec![]);
267 let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2, &m3];
268
269 let graph = DependencyGraph::build(&migrations, true).unwrap();
270 let order = graph.topological_sort().unwrap();
271 assert_eq!(order, vec!["1", "2", "3"]);
272 }
273
274 #[test]
275 fn test_explicit_dependency() {
276 let m1 = make_migration("1", vec![]);
277 let m2 = make_migration("2", vec![]);
278 let m3 = make_migration("3", vec!["1"]); let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2, &m3];
280
281 let graph = DependencyGraph::build(&migrations, false).unwrap();
282 let order = graph.topological_sort().unwrap();
283 let pos1 = order.iter().position(|v| v == "1").unwrap();
285 let pos3 = order.iter().position(|v| v == "3").unwrap();
286 assert!(pos1 < pos3);
287 }
288
289 #[test]
290 fn test_cycle_detection() {
291 let m1 = make_migration("1", vec!["2"]);
292 let m2 = make_migration("2", vec!["1"]);
293 let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2];
294
295 let graph = DependencyGraph::build(&migrations, false).unwrap();
296 assert!(graph.topological_sort().is_err());
297 }
298
299 #[test]
300 fn test_missing_dependency() {
301 let m1 = make_migration("1", vec!["99"]);
302 let migrations: Vec<&ResolvedMigration> = vec![&m1];
303
304 assert!(DependencyGraph::build(&migrations, false).is_err());
305 }
306
307 #[test]
308 fn test_cycle_error_shows_path() {
309 let m1 = make_migration("1", vec!["3"]);
310 let m2 = make_migration("2", vec!["1"]);
311 let m3 = make_migration("3", vec!["2"]);
312 let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2, &m3];
313
314 let graph = DependencyGraph::build(&migrations, false).unwrap();
315 let err = graph.topological_sort().unwrap_err();
316 let msg = err.to_string();
317 assert!(msg.contains("->"), "Cycle error should show path: {}", msg);
319 }
320
321 #[test]
322 fn test_empty_migrations() {
323 let migrations: Vec<&ResolvedMigration> = vec![];
324 let graph = DependencyGraph::build(&migrations, true).unwrap();
325 let order = graph.topological_sort().unwrap();
326 assert!(order.is_empty());
327 }
328
329 #[test]
330 fn test_single_migration() {
331 let m1 = make_migration("1", vec![]);
332 let migrations: Vec<&ResolvedMigration> = vec![&m1];
333 let graph = DependencyGraph::build(&migrations, true).unwrap();
334 let order = graph.topological_sort().unwrap();
335 assert_eq!(order, vec!["1"]);
336 }
337
338 #[test]
339 fn test_diamond_dependency() {
340 let m1 = make_migration("1", vec![]);
341 let m2 = make_migration("2", vec!["1"]);
342 let m3 = make_migration("3", vec!["1"]);
343 let m4 = make_migration("4", vec!["2", "3"]);
344 let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2, &m3, &m4];
345
346 let graph = DependencyGraph::build(&migrations, false).unwrap();
347 let order = graph.topological_sort().unwrap();
348
349 assert_eq!(order[0], "1");
351 assert_eq!(order[3], "4");
352 }
353
354 #[test]
355 fn test_self_referencing_cycle() {
356 let m1 = make_migration("1", vec!["1"]);
357 let migrations: Vec<&ResolvedMigration> = vec![&m1];
358
359 let graph = DependencyGraph::build(&migrations, false).unwrap();
360 assert!(graph.topological_sort().is_err());
361 }
362}