1use std::collections::{HashMap, HashSet};
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>> {
153 let rank: HashMap<&str, usize> = self
157 .all_versions
158 .iter()
159 .enumerate()
160 .map(|(i, v)| (v.as_str(), i))
161 .collect();
162
163 let mut in_degree: HashMap<&str, usize> = HashMap::new();
165 for v in &self.all_versions {
166 in_degree.insert(v, self.edges.get(v).map_or(0, |deps| deps.len()));
167 }
168
169 let mut ready: std::collections::BTreeSet<(usize, &str)> = self
173 .all_versions
174 .iter()
175 .filter(|v| *in_degree.get(v.as_str()).unwrap_or(&0) == 0)
176 .map(|v| (rank[v.as_str()], v.as_str()))
177 .collect();
178
179 let mut sorted = Vec::new();
180
181 while let Some(&(_, node)) = ready.iter().next() {
182 ready.remove(&(rank[node], node));
183 sorted.push(node.to_string());
184
185 if let Some(dependents) = self.reverse_edges.get(node) {
187 for dep in dependents {
188 let deg = in_degree.get_mut(dep.as_str()).unwrap();
189 *deg -= 1;
190 if *deg == 0 {
191 ready.insert((rank[dep.as_str()], dep.as_str()));
192 }
193 }
194 }
195 }
196
197 if sorted.len() != self.all_versions.len() {
198 let owned_in_degree: HashMap<String, usize> = in_degree
200 .iter()
201 .map(|(&k, &v)| (k.to_string(), v))
202 .collect();
203 let cycle_path = self.trace_cycle(&owned_in_degree);
204 return Err(WaypointError::DependencyCycle { path: cycle_path });
205 }
206
207 Ok(sorted)
208 }
209
210 fn trace_cycle(&self, in_degree: &HashMap<String, usize>) -> String {
212 let start = self
214 .all_versions
215 .iter()
216 .find(|v| *in_degree.get(*v).unwrap_or(&0) > 0);
217
218 let Some(start) = start else {
219 return "unknown cycle".to_string();
220 };
221
222 let mut path = vec![start.clone()];
224 let mut current = start.clone();
225 let mut visited = std::collections::HashSet::new();
226 visited.insert(current.clone());
227
228 loop {
229 let next = self
231 .edges
232 .get(¤t)
233 .and_then(|deps| deps.iter().find(|d| *in_degree.get(*d).unwrap_or(&0) > 0));
234
235 match next {
236 Some(n) => {
237 if !visited.insert(n.clone()) {
238 path.push(n.clone());
240 if let Some(pos) = path.iter().position(|v| v == n) {
242 let cycle: Vec<String> = path[pos..].to_vec();
243 return cycle.join(" -> ");
244 }
245 return path.join(" -> ");
246 }
247 path.push(n.clone());
248 current = n.clone();
249 }
250 None => {
251 let in_cycle: Vec<String> = self
253 .all_versions
254 .iter()
255 .filter(|v| *in_degree.get(*v).unwrap_or(&0) > 0)
256 .cloned()
257 .collect();
258 return format!("cycle involving: {}", in_cycle.join(", "));
259 }
260 }
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::directive::MigrationDirectives;
269 use crate::migration::{MigrationKind, MigrationVersion, ResolvedMigration};
270
271 fn make_migration(version: &str, depends: Vec<&str>) -> ResolvedMigration {
272 ResolvedMigration {
273 kind: MigrationKind::Versioned(MigrationVersion::parse(version).unwrap()),
274 description: format!("V{}", version),
275 script: format!("V{}__test.sql", version),
276 checksum: 0,
277 sql: String::new(),
278 directives: MigrationDirectives {
279 depends: depends.into_iter().map(String::from).collect(),
280 env: vec![],
281 ..Default::default()
282 },
283 }
284 }
285
286 #[test]
287 fn test_simple_chain() {
288 let m1 = make_migration("1", vec![]);
289 let m2 = make_migration("2", vec![]);
290 let m3 = make_migration("3", vec![]);
291 let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2, &m3];
292
293 let graph = DependencyGraph::build(&migrations, true).unwrap();
294 let order = graph.topological_sort().unwrap();
295 assert_eq!(order, vec!["1", "2", "3"]);
296 }
297
298 #[test]
299 fn test_explicit_dependency() {
300 let m1 = make_migration("1", vec![]);
301 let m2 = make_migration("2", vec![]);
302 let m3 = make_migration("3", vec!["1"]); let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2, &m3];
304
305 let graph = DependencyGraph::build(&migrations, false).unwrap();
306 let order = graph.topological_sort().unwrap();
307 let pos1 = order.iter().position(|v| v == "1").unwrap();
309 let pos3 = order.iter().position(|v| v == "3").unwrap();
310 assert!(pos1 < pos3);
311 }
312
313 #[test]
314 fn test_cycle_detection() {
315 let m1 = make_migration("1", vec!["2"]);
316 let m2 = make_migration("2", vec!["1"]);
317 let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2];
318
319 let graph = DependencyGraph::build(&migrations, false).unwrap();
320 assert!(graph.topological_sort().is_err());
321 }
322
323 #[test]
324 fn test_missing_dependency() {
325 let m1 = make_migration("1", vec!["99"]);
326 let migrations: Vec<&ResolvedMigration> = vec![&m1];
327
328 assert!(DependencyGraph::build(&migrations, false).is_err());
329 }
330
331 #[test]
332 fn test_cycle_error_shows_path() {
333 let m1 = make_migration("1", vec!["3"]);
334 let m2 = make_migration("2", vec!["1"]);
335 let m3 = make_migration("3", vec!["2"]);
336 let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2, &m3];
337
338 let graph = DependencyGraph::build(&migrations, false).unwrap();
339 let err = graph.topological_sort().unwrap_err();
340 let msg = err.to_string();
341 assert!(msg.contains("->"), "Cycle error should show path: {}", msg);
343 }
344
345 #[test]
346 fn test_empty_migrations() {
347 let migrations: Vec<&ResolvedMigration> = vec![];
348 let graph = DependencyGraph::build(&migrations, true).unwrap();
349 let order = graph.topological_sort().unwrap();
350 assert!(order.is_empty());
351 }
352
353 #[test]
354 fn test_single_migration() {
355 let m1 = make_migration("1", vec![]);
356 let migrations: Vec<&ResolvedMigration> = vec![&m1];
357 let graph = DependencyGraph::build(&migrations, true).unwrap();
358 let order = graph.topological_sort().unwrap();
359 assert_eq!(order, vec!["1"]);
360 }
361
362 #[test]
363 fn test_diamond_dependency() {
364 let m1 = make_migration("1", vec![]);
365 let m2 = make_migration("2", vec!["1"]);
366 let m3 = make_migration("3", vec!["1"]);
367 let m4 = make_migration("4", vec!["2", "3"]);
368 let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2, &m3, &m4];
369
370 let graph = DependencyGraph::build(&migrations, false).unwrap();
371 let order = graph.topological_sort().unwrap();
372
373 assert_eq!(order[0], "1");
375 assert_eq!(order[3], "4");
376 }
377
378 #[test]
379 fn test_self_referencing_cycle() {
380 let m1 = make_migration("1", vec!["1"]);
381 let migrations: Vec<&ResolvedMigration> = vec![&m1];
382
383 let graph = DependencyGraph::build(&migrations, false).unwrap();
384 assert!(graph.topological_sort().is_err());
385 }
386
387 #[test]
388 fn test_topological_sort_is_deterministic_for_independent_migrations() {
389 let migs: Vec<ResolvedMigration> = std::iter::once(make_migration("1", vec![]))
395 .chain((2..=6).map(|v| make_migration(&v.to_string(), vec!["1"])))
396 .collect();
397 let refs: Vec<&ResolvedMigration> = migs.iter().collect();
398 let graph = DependencyGraph::build(&refs, true).unwrap();
399 let order = graph.topological_sort().unwrap();
400 assert_eq!(
401 order,
402 vec!["1", "2", "3", "4", "5", "6"],
403 "independent migrations must apply in version order, not hash order"
404 );
405 }
406}