use std::collections::{HashMap, HashSet, VecDeque};
use crate::error::{Result, WaypointError};
use crate::migration::ResolvedMigration;
pub struct DependencyGraph {
edges: HashMap<String, HashSet<String>>,
reverse_edges: HashMap<String, HashSet<String>>,
all_versions: Vec<String>,
}
impl DependencyGraph {
pub fn build(migrations: &[&ResolvedMigration], implicit_chain: bool) -> Result<Self> {
let mut edges: HashMap<String, HashSet<String>> = HashMap::new();
let mut reverse_edges: HashMap<String, HashSet<String>> = HashMap::new();
let mut all_versions: Vec<String> = Vec::new();
let mut versioned: Vec<&ResolvedMigration> = migrations
.iter()
.filter(|m| m.is_versioned())
.copied()
.collect();
versioned.sort_by(|a, b| a.version().unwrap().cmp(b.version().unwrap()));
for m in &versioned {
let version = m.version().unwrap().raw.clone();
edges.entry(version.clone()).or_default();
reverse_edges.entry(version.clone()).or_default();
all_versions.push(version);
}
for m in &versioned {
let version = &m.version().unwrap().raw;
for dep in &m.directives.depends {
if !edges.contains_key(dep) {
return Err(WaypointError::MissingDependency {
version: version.clone(),
dependency: dep.clone(),
});
}
edges.get_mut(version.as_str()).unwrap().insert(dep.clone());
reverse_edges
.get_mut(dep.as_str())
.unwrap()
.insert(version.clone());
}
}
if implicit_chain {
for i in 1..all_versions.len() {
let current = &all_versions[i];
let previous = &all_versions[i - 1];
if edges.get(current).is_none_or(|deps| deps.is_empty()) {
edges.get_mut(current).unwrap().insert(previous.clone());
reverse_edges
.get_mut(previous)
.unwrap()
.insert(current.clone());
}
}
}
Ok(DependencyGraph {
edges,
reverse_edges,
all_versions,
})
}
pub fn topological_sort(&self) -> Result<Vec<String>> {
let mut in_degree: HashMap<&str, usize> = HashMap::new();
for v in &self.all_versions {
in_degree.insert(v, self.edges.get(v).map_or(0, |deps| deps.len()));
}
let mut queue: VecDeque<&str> = VecDeque::new();
for v in &self.all_versions {
if *in_degree.get(v.as_str()).unwrap_or(&0) == 0 {
queue.push_back(v);
}
}
let mut sorted = Vec::new();
while let Some(node) = queue.pop_front() {
sorted.push(node.to_string());
if let Some(dependents) = self.reverse_edges.get(node) {
for dep in dependents {
let deg = in_degree.get_mut(dep.as_str()).unwrap();
*deg -= 1;
if *deg == 0 {
queue.push_back(dep);
}
}
}
}
if sorted.len() != self.all_versions.len() {
let owned_in_degree: HashMap<String, usize> = in_degree
.iter()
.map(|(&k, &v)| (k.to_string(), v))
.collect();
let cycle_path = self.trace_cycle(&owned_in_degree);
return Err(WaypointError::DependencyCycle { path: cycle_path });
}
Ok(sorted)
}
fn trace_cycle(&self, in_degree: &HashMap<String, usize>) -> String {
let start = self
.all_versions
.iter()
.find(|v| *in_degree.get(*v).unwrap_or(&0) > 0);
let Some(start) = start else {
return "unknown cycle".to_string();
};
let mut path = vec![start.clone()];
let mut current = start.clone();
let mut visited = std::collections::HashSet::new();
visited.insert(current.clone());
loop {
let next = self
.edges
.get(¤t)
.and_then(|deps| deps.iter().find(|d| *in_degree.get(*d).unwrap_or(&0) > 0));
match next {
Some(n) => {
if !visited.insert(n.clone()) {
path.push(n.clone());
if let Some(pos) = path.iter().position(|v| v == n) {
let cycle: Vec<String> = path[pos..].to_vec();
return cycle.join(" -> ");
}
return path.join(" -> ");
}
path.push(n.clone());
current = n.clone();
}
None => {
let in_cycle: Vec<String> = self
.all_versions
.iter()
.filter(|v| *in_degree.get(*v).unwrap_or(&0) > 0)
.cloned()
.collect();
return format!("cycle involving: {}", in_cycle.join(", "));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::directive::MigrationDirectives;
use crate::migration::{MigrationKind, MigrationVersion, ResolvedMigration};
fn make_migration(version: &str, depends: Vec<&str>) -> ResolvedMigration {
ResolvedMigration {
kind: MigrationKind::Versioned(MigrationVersion::parse(version).unwrap()),
description: format!("V{}", version),
script: format!("V{}__test.sql", version),
checksum: 0,
sql: String::new(),
directives: MigrationDirectives {
depends: depends.into_iter().map(String::from).collect(),
env: vec![],
..Default::default()
},
}
}
#[test]
fn test_simple_chain() {
let m1 = make_migration("1", vec![]);
let m2 = make_migration("2", vec![]);
let m3 = make_migration("3", vec![]);
let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2, &m3];
let graph = DependencyGraph::build(&migrations, true).unwrap();
let order = graph.topological_sort().unwrap();
assert_eq!(order, vec!["1", "2", "3"]);
}
#[test]
fn test_explicit_dependency() {
let m1 = make_migration("1", vec![]);
let m2 = make_migration("2", vec![]);
let m3 = make_migration("3", vec!["1"]); let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2, &m3];
let graph = DependencyGraph::build(&migrations, false).unwrap();
let order = graph.topological_sort().unwrap();
let pos1 = order.iter().position(|v| v == "1").unwrap();
let pos3 = order.iter().position(|v| v == "3").unwrap();
assert!(pos1 < pos3);
}
#[test]
fn test_cycle_detection() {
let m1 = make_migration("1", vec!["2"]);
let m2 = make_migration("2", vec!["1"]);
let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2];
let graph = DependencyGraph::build(&migrations, false).unwrap();
assert!(graph.topological_sort().is_err());
}
#[test]
fn test_missing_dependency() {
let m1 = make_migration("1", vec!["99"]);
let migrations: Vec<&ResolvedMigration> = vec![&m1];
assert!(DependencyGraph::build(&migrations, false).is_err());
}
#[test]
fn test_cycle_error_shows_path() {
let m1 = make_migration("1", vec!["3"]);
let m2 = make_migration("2", vec!["1"]);
let m3 = make_migration("3", vec!["2"]);
let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2, &m3];
let graph = DependencyGraph::build(&migrations, false).unwrap();
let err = graph.topological_sort().unwrap_err();
let msg = err.to_string();
assert!(msg.contains("->"), "Cycle error should show path: {}", msg);
}
#[test]
fn test_empty_migrations() {
let migrations: Vec<&ResolvedMigration> = vec![];
let graph = DependencyGraph::build(&migrations, true).unwrap();
let order = graph.topological_sort().unwrap();
assert!(order.is_empty());
}
#[test]
fn test_single_migration() {
let m1 = make_migration("1", vec![]);
let migrations: Vec<&ResolvedMigration> = vec![&m1];
let graph = DependencyGraph::build(&migrations, true).unwrap();
let order = graph.topological_sort().unwrap();
assert_eq!(order, vec!["1"]);
}
#[test]
fn test_diamond_dependency() {
let m1 = make_migration("1", vec![]);
let m2 = make_migration("2", vec!["1"]);
let m3 = make_migration("3", vec!["1"]);
let m4 = make_migration("4", vec!["2", "3"]);
let migrations: Vec<&ResolvedMigration> = vec![&m1, &m2, &m3, &m4];
let graph = DependencyGraph::build(&migrations, false).unwrap();
let order = graph.topological_sort().unwrap();
assert_eq!(order[0], "1");
assert_eq!(order[3], "4");
}
#[test]
fn test_self_referencing_cycle() {
let m1 = make_migration("1", vec!["1"]);
let migrations: Vec<&ResolvedMigration> = vec![&m1];
let graph = DependencyGraph::build(&migrations, false).unwrap();
assert!(graph.topological_sort().is_err());
}
}