Skip to main content

reddb_server/application/
migration_graph.rs

1//! Dependency graph resolution for native migrations.
2//!
3//! Pure logic — no runtime, no I/O. Input is a list of migration names and
4//! their dependency edges; output is a topologically-sorted application order
5//! or a `CycleError` naming the involved migrations.
6
7use 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
24/// Sort `migrations` in topological order given `edges` (migration → depends_on).
25///
26/// Returns `Err(CycleError)` if a cycle exists.
27/// Migrations not present in `edges` are treated as having no dependencies.
28pub fn topological_sort(
29    migrations: &[String],
30    edges: &[(String, String)],
31) -> Result<Vec<String>, CycleError> {
32    // Build adjacency: node → nodes that depend on it (reverse for Kahn).
33    // in_degree: node → count of dependencies not yet satisfied.
34    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    // dependents[dep] = list of migrations that depend on dep
40    let mut dependents: HashMap<&str, Vec<&str>> = HashMap::new();
41
42    for (migration, dep) in edges {
43        // Only consider edges where both nodes are in our migration set.
44        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    // Kahn's algorithm
55    let mut queue: VecDeque<&str> = in_degree
56        .iter()
57        .filter(|(_, &deg)| deg == 0)
58        .map(|(&name, _)| name)
59        .collect();
60
61    // Deterministic order within same in-degree level
62    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        // Cycle: find involved nodes (those still with in_degree > 0)
86        let mut involved: Vec<String> = in_degree
87            .iter()
88            .filter(|(_, &deg)| 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
98/// Check whether adding edge `from → to` would create a cycle in the existing
99/// graph. Returns the cycle path if detected.
100///
101/// Uses DFS reachability: a cycle exists iff `from` is reachable from `to`
102/// through existing edges.
103pub fn would_create_cycle(existing_edges: &[(String, String)], from: &str, to: &str) -> bool {
104    // Build adjacency: node → its dependencies
105    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    // Check if `from` is reachable from `to` (meaning adding from→to creates a cycle)
111    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        // a → b, b → c. Adding c → a would create a cycle.
171        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        // a, b independent; c depends on both; d depends on c
184        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        // a and b must come before c; c before d
188        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}