use crate::lineage::graph::{LineageEdge, LineageGraph, LineageNodeId};
use std::collections::{HashSet, VecDeque};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImpactEdge {
pub from: String,
pub to: String,
pub via: String,
}
impl ImpactEdge {
fn new(from: &LineageNodeId, to: &LineageNodeId, via: &str) -> Self {
Self {
from: format!("{}.{}", from.table, from.column),
to: format!("{}.{}", to.table, to.column),
via: via.to_string(),
}
}
}
pub fn downstream_impact(
graph: &LineageGraph,
node: &LineageNodeId,
max_depth: usize,
) -> Vec<ImpactEdge> {
bfs_edges(graph, node, max_depth, true)
}
pub fn upstream_trace(
graph: &LineageGraph,
node: &LineageNodeId,
max_depth: usize,
) -> Vec<ImpactEdge> {
bfs_edges(graph, node, max_depth, false)
}
fn bfs_edges(
graph: &LineageGraph,
node: &LineageNodeId,
max_depth: usize,
downstream: bool,
) -> Vec<ImpactEdge> {
let mut result = Vec::new();
let mut visited: HashSet<LineageNodeId> = HashSet::new();
let mut queue: VecDeque<(LineageNodeId, usize)> = VecDeque::new();
queue.push_back((node.clone(), 0));
visited.insert(node.clone());
while let Some((current, depth)) = queue.pop_front() {
if max_depth > 0 && depth >= max_depth {
continue;
}
let edges: Vec<&LineageEdge> = if downstream {
graph.outgoing_edges(¤t)
} else {
graph.incoming_edges(¤t)
};
for edge in edges {
let next = if downstream {
edge.target.clone()
} else {
edge.source.clone()
};
result.push(ImpactEdge::new(
&edge.source,
&edge.target,
edge_type_str(&edge.edge_type),
));
if visited.insert(next.clone()) {
queue.push_back((next, depth + 1));
}
}
}
result
}
fn edge_type_str(edge_type: &crate::lineage::graph::EdgeType) -> &'static str {
match edge_type {
crate::lineage::graph::EdgeType::DirectDependency => "DirectDependency",
crate::lineage::graph::EdgeType::Derived => "Derived",
crate::lineage::graph::EdgeType::Join => "Join",
crate::lineage::graph::EdgeType::Filter => "Filter",
crate::lineage::graph::EdgeType::Projection => "Projection",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lineage::graph::{EdgeType, LineageNode, NodeType};
use crate::lineage::LineageGraph;
fn node(table: &str, column: &str) -> LineageNodeId {
LineageNodeId::new(table, column)
}
fn build_chain() -> LineageGraph {
let mut g = LineageGraph::new();
for (t, c) in [
("users", "id"),
("orders", "user_id"),
("orders", "total"),
("report", "amount"),
] {
g.add_node(LineageNode::new(node(t, c), NodeType::Column));
}
g.add_edge(LineageEdge::new(
node("users", "id"),
node("orders", "user_id"),
EdgeType::Join,
))
.unwrap();
g.add_edge(LineageEdge::new(
node("orders", "user_id"),
node("orders", "total"),
EdgeType::Derived,
))
.unwrap();
g.add_edge(LineageEdge::new(
node("orders", "total"),
node("report", "amount"),
EdgeType::Derived,
))
.unwrap();
g
}
#[test]
fn downstream_full_depth() {
let g = build_chain();
let edges = downstream_impact(&g, &node("users", "id"), 0);
assert_eq!(edges.len(), 3);
assert!(edges.iter().any(|e| e.to == "orders.user_id"));
assert!(edges.iter().any(|e| e.to == "orders.total"));
assert!(edges.iter().any(|e| e.to == "report.amount"));
}
#[test]
fn downstream_depth_limited() {
let g = build_chain();
let edges = downstream_impact(&g, &node("users", "id"), 1);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].to, "orders.user_id");
}
#[test]
fn upstream_trace_full() {
let g = build_chain();
let edges = upstream_trace(&g, &node("report", "amount"), 0);
assert_eq!(edges.len(), 3);
assert!(edges.iter().any(|e| e.from == "users.id"));
assert!(edges.iter().any(|e| e.from == "orders.user_id"));
assert!(edges.iter().any(|e| e.from == "orders.total"));
}
#[test]
fn upstream_depth_limited() {
let g = build_chain();
let edges = upstream_trace(&g, &node("report", "amount"), 2);
assert_eq!(edges.len(), 2); assert!(edges.iter().all(|e| e.from != "users.id"));
}
#[test]
fn impact_edge_contains_dependency_type() {
let g = build_chain();
let edges = downstream_impact(&g, &node("users", "id"), 1);
assert_eq!(edges[0].via, "Join");
}
#[test]
fn unknown_node_returns_empty() {
let g = build_chain();
let edges = downstream_impact(&g, &node("nonexistent", "x"), 0);
assert!(edges.is_empty());
}
}