Skip to main content

pgevolve_core/plan/
graph.rs

1//! Directed graph and topological sort over arbitrary node identifiers.
2//!
3//! Edge direction convention: `A -> B` means **`A` depends on `B`**.
4//! Topological order therefore visits dependencies before their dependents
5//! (i.e., leaves first), which is what `creates_and_adds` ordering requires.
6//!
7//! Sort is deterministic: when multiple nodes are simultaneously eligible,
8//! the smallest one (by `Ord`) wins. Identical input ⇒ byte-identical output.
9
10use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
11use std::hash::Hash;
12
13use crate::plan::edges::{DepEdge, DepSource, NodeId};
14
15/// A directed graph over nodes of type `N`.
16#[derive(Debug, Clone)]
17pub struct Graph<N> {
18    nodes: BTreeSet<N>,
19    /// `edges[A]` = the set of `B` such that A depends on B.
20    edges: BTreeMap<N, BTreeSet<N>>,
21    /// Per-edge provenance. Absent entries default to [`DepSource::Structural`].
22    /// Internal only; not exposed through the public API.
23    edge_sources: BTreeMap<(N, N), DepSource>,
24}
25
26/// A cycle reported by [`Graph::topological_sort`] / [`Graph::reverse_topological_sort`].
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Cycle<N> {
29    /// Nodes that participate in at least one cycle, in deterministic order.
30    pub nodes: Vec<N>,
31}
32
33impl<N> Default for Graph<N>
34where
35    N: Hash + Eq + Clone + Ord,
36{
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl<N> Graph<N>
43where
44    N: Hash + Eq + Clone + Ord,
45{
46    /// Construct an empty graph.
47    pub const fn new() -> Self {
48        Self {
49            nodes: BTreeSet::new(),
50            edges: BTreeMap::new(),
51            edge_sources: BTreeMap::new(),
52        }
53    }
54
55    /// Add a node. No-op if already present.
56    pub fn add_node(&mut self, n: N) {
57        self.nodes.insert(n);
58    }
59
60    /// Add an edge `from -> to`, meaning `from` depends on `to`.
61    /// Both endpoints are added as nodes if not already present.
62    ///
63    /// The edge carries no explicit provenance; `dep_edges()` will report it as
64    /// [`DepSource::Structural`] by default. Use `add_dep_edge` on
65    /// `Graph<NodeId>` to record explicit provenance for v0.2 edges.
66    pub fn add_edge(&mut self, from: N, to: N) {
67        self.add_edge_internal(from, to);
68    }
69
70    /// Internal: register adjacency without touching `edge_sources`.
71    fn add_edge_internal(&mut self, from: N, to: N) {
72        self.nodes.insert(from.clone());
73        self.nodes.insert(to.clone());
74        self.edges.entry(from).or_default().insert(to);
75    }
76
77    /// Remove an edge `from -> to`. No-op if absent.
78    ///
79    /// **Does not** touch `edge_sources`. If the edge was inserted via
80    /// [`Graph::add_dep_edge`] on a `Graph<NodeId>`, the stale provenance entry
81    /// will survive and be returned by [`Graph::dep_edges`] if the edge is
82    /// re-added later. Use [`Graph::remove_dep_edge`] instead when provenance
83    /// correctness matters.
84    pub fn remove_edge(&mut self, from: &N, to: &N) {
85        if let Some(set) = self.edges.get_mut(from) {
86            set.remove(to);
87            if set.is_empty() {
88                self.edges.remove(from);
89            }
90        }
91    }
92
93    /// Number of nodes in the graph.
94    pub fn node_count(&self) -> usize {
95        self.nodes.len()
96    }
97
98    /// Iterate over all nodes in `Ord` order.
99    pub fn nodes(&self) -> impl Iterator<Item = &N> {
100        self.nodes.iter()
101    }
102
103    /// Iterate the dependencies of `n` (the set `edges[n]`).
104    pub fn dependencies_of<'a>(&'a self, n: &N) -> impl Iterator<Item = &'a N> + 'a + use<'a, N> {
105        self.edges.get(n).into_iter().flat_map(BTreeSet::iter)
106    }
107
108    /// Topological sort using Kahn's algorithm.
109    ///
110    /// Returns nodes with no remaining dependencies first. Ties are broken by
111    /// the smallest node per `Ord`, so the result is deterministic.
112    pub fn topological_sort(&self) -> Result<Vec<N>, Cycle<N>> {
113        // Build reverse adjacency: dependents[B] = nodes that depend on B.
114        // We compute in-degree based on outgoing dependency edges.
115        let mut in_degree: BTreeMap<N, usize> =
116            self.nodes.iter().map(|n| (n.clone(), 0_usize)).collect();
117        let mut dependents: BTreeMap<N, Vec<N>> = BTreeMap::new();
118
119        for (from, deps) in &self.edges {
120            // `from` depends on each `to`, so `from` has out-edges to each `to`.
121            // In a "dependencies first" topo sort, we treat the *dependency* edge
122            // as `from -> to` and want `to` emitted before `from`.
123            // Kahn's algorithm: in-degree of `from` = number of unresolved deps.
124            *in_degree.entry(from.clone()).or_insert(0) += deps.len();
125            for to in deps {
126                dependents.entry(to.clone()).or_default().push(from.clone());
127            }
128        }
129
130        // Min-heap by Ord (BinaryHeap is max-heap, so wrap with Reverse).
131        let mut ready: BinaryHeap<std::cmp::Reverse<N>> = in_degree
132            .iter()
133            .filter(|(_, d)| **d == 0)
134            .map(|(n, _)| std::cmp::Reverse(n.clone()))
135            .collect();
136
137        let mut out = Vec::with_capacity(self.nodes.len());
138
139        while let Some(std::cmp::Reverse(n)) = ready.pop() {
140            out.push(n.clone());
141            if let Some(parents) = dependents.get(&n) {
142                for p in parents {
143                    if let Some(d) = in_degree.get_mut(p) {
144                        *d -= 1;
145                        if *d == 0 {
146                            ready.push(std::cmp::Reverse(p.clone()));
147                        }
148                    }
149                }
150            }
151        }
152
153        if out.len() == self.nodes.len() {
154            Ok(out)
155        } else {
156            // Remaining nodes (in-degree > 0) participate in at least one cycle.
157            let cycle_nodes: Vec<N> = in_degree
158                .into_iter()
159                .filter_map(|(n, d)| (d > 0).then_some(n))
160                .collect();
161            Err(Cycle { nodes: cycle_nodes })
162        }
163    }
164
165    /// Reverse topological sort: dependents first, dependencies last.
166    /// Used for drop ordering (drop the index before the table it indexes).
167    pub fn reverse_topological_sort(&self) -> Result<Vec<N>, Cycle<N>> {
168        let mut v = self.topological_sort()?;
169        v.reverse();
170        Ok(v)
171    }
172
173    /// Iterate all edges as `(from, to)` pairs in ascending `(from, to)` order by `Ord`.
174    pub fn edges(&self) -> impl Iterator<Item = (&N, &N)> {
175        self.edges
176            .iter()
177            .flat_map(|(from, targets)| targets.iter().map(move |to| (from, to)))
178    }
179}
180
181impl Graph<NodeId> {
182    /// Add an edge `from -> to` and record its [`DepSource`].
183    ///
184    /// If an edge between these two nodes already exists (inserted via
185    /// [`Graph::add_edge`] or a prior call to this method), the source is
186    /// overwritten with the new value. Both endpoints are added as nodes if
187    /// not already present.
188    ///
189    /// Use this instead of [`Graph::add_edge`] when populating v0.2 edges from
190    /// AST walks (`AstExtracted`) or `-- @pgevolve dep:` directives (`AstDeclared`).
191    pub fn add_dep_edge(&mut self, from: NodeId, to: NodeId, source: DepSource) {
192        self.add_edge_internal(from.clone(), to.clone());
193        self.edge_sources.insert((from, to), source);
194    }
195
196    /// Remove an edge `from -> to` and its [`DepSource`] from the source map.
197    /// No-op if the adjacency (or source entry) is absent.
198    ///
199    /// Prefer this over [`Graph::remove_edge`] whenever the edge may have been
200    /// inserted via [`Self::add_dep_edge`]; it prevents stale provenance from
201    /// surviving a remove-then-re-add cycle.
202    pub fn remove_dep_edge(&mut self, from: &NodeId, to: &NodeId) {
203        self.remove_edge(from, to);
204        self.edge_sources.remove(&(from.clone(), to.clone()));
205    }
206
207    /// Iterate edges as [`DepEdge`] records with per-edge provenance.
208    ///
209    /// Edges inserted via [`Self::add_dep_edge`] carry their recorded
210    /// [`DepSource`]. Edges inserted via [`Graph::add_edge`] default to
211    /// [`DepSource::Structural`], preserving v0.1 behaviour.
212    pub fn dep_edges(&self) -> impl Iterator<Item = DepEdge> + '_ {
213        self.edges().map(|(from, to)| {
214            let source = self
215                .edge_sources
216                .get(&(from.clone(), to.clone()))
217                .copied()
218                .unwrap_or(DepSource::Structural);
219            DepEdge {
220                from: from.clone(),
221                to: to.clone(),
222                source,
223            }
224        })
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn empty_graph_sorts_to_empty() {
234        let g: Graph<i32> = Graph::new();
235        assert_eq!(g.topological_sort().unwrap(), Vec::<i32>::new());
236    }
237
238    #[test]
239    fn single_node_sorts_to_self() {
240        let mut g: Graph<i32> = Graph::new();
241        g.add_node(7);
242        assert_eq!(g.topological_sort().unwrap(), vec![7]);
243    }
244
245    #[test]
246    fn linear_chain_sorts_in_dependency_order() {
247        // a -> b -> c  (a depends on b depends on c)
248        // Output: c, b, a
249        let mut g: Graph<&'static str> = Graph::new();
250        g.add_edge("a", "b");
251        g.add_edge("b", "c");
252        assert_eq!(g.topological_sort().unwrap(), vec!["c", "b", "a"]);
253    }
254
255    #[test]
256    fn diamond_sorts_with_deterministic_tie_break() {
257        // a -> b, a -> c, b -> d, c -> d
258        // d emitted first, then b/c (tie → smaller "b" first), then a
259        let mut g: Graph<&'static str> = Graph::new();
260        g.add_edge("a", "b");
261        g.add_edge("a", "c");
262        g.add_edge("b", "d");
263        g.add_edge("c", "d");
264        assert_eq!(g.topological_sort().unwrap(), vec!["d", "b", "c", "a"]);
265    }
266
267    #[test]
268    fn disconnected_components_sort_deterministically() {
269        let mut g: Graph<&'static str> = Graph::new();
270        g.add_edge("a", "b");
271        g.add_edge("x", "y");
272        // Leaves first (b, y); after popping b, a unblocks. Among {a, y} the
273        // smaller is a, then y unblocks no one, then x.
274        assert_eq!(g.topological_sort().unwrap(), vec!["b", "a", "y", "x"]);
275    }
276
277    #[test]
278    fn isolated_nodes_appear_in_order() {
279        let mut g: Graph<i32> = Graph::new();
280        g.add_node(3);
281        g.add_node(1);
282        g.add_node(2);
283        assert_eq!(g.topological_sort().unwrap(), vec![1, 2, 3]);
284    }
285
286    #[test]
287    fn cycle_of_two_detected() {
288        let mut g: Graph<&'static str> = Graph::new();
289        g.add_edge("a", "b");
290        g.add_edge("b", "a");
291        let err = g.topological_sort().unwrap_err();
292        assert_eq!(err.nodes, vec!["a", "b"]);
293    }
294
295    #[test]
296    fn cycle_of_three_detected() {
297        let mut g: Graph<&'static str> = Graph::new();
298        g.add_edge("a", "b");
299        g.add_edge("b", "c");
300        g.add_edge("c", "a");
301        let err = g.topological_sort().unwrap_err();
302        assert_eq!(err.nodes, vec!["a", "b", "c"]);
303    }
304
305    #[test]
306    fn cycle_does_not_block_acyclic_part() {
307        // Acyclic part: x -> y. Cycle: a <-> b. Both are reported correctly:
308        // sort errors with cycle nodes; we only check that.
309        let mut g: Graph<&'static str> = Graph::new();
310        g.add_edge("x", "y");
311        g.add_edge("a", "b");
312        g.add_edge("b", "a");
313        let err = g.topological_sort().unwrap_err();
314        assert_eq!(err.nodes, vec!["a", "b"]);
315    }
316
317    #[test]
318    fn reverse_sort_inverts_order() {
319        let mut g: Graph<&'static str> = Graph::new();
320        g.add_edge("a", "b");
321        g.add_edge("b", "c");
322        assert_eq!(g.reverse_topological_sort().unwrap(), vec!["a", "b", "c"]);
323    }
324
325    #[test]
326    fn remove_edge_breaks_cycle() {
327        let mut g: Graph<&'static str> = Graph::new();
328        g.add_edge("a", "b");
329        g.add_edge("b", "a");
330        assert!(g.topological_sort().is_err());
331        g.remove_edge(&"b", &"a");
332        assert_eq!(g.topological_sort().unwrap(), vec!["b", "a"]);
333    }
334
335    #[test]
336    fn deterministic_under_insertion_order_changes() {
337        let mut g1: Graph<&'static str> = Graph::new();
338        g1.add_edge("a", "b");
339        g1.add_edge("a", "c");
340        g1.add_edge("b", "d");
341        g1.add_edge("c", "d");
342
343        let mut g2: Graph<&'static str> = Graph::new();
344        g2.add_edge("c", "d");
345        g2.add_edge("a", "c");
346        g2.add_edge("b", "d");
347        g2.add_edge("a", "b");
348
349        assert_eq!(g1.topological_sort(), g2.topological_sort());
350    }
351
352    #[test]
353    fn dependencies_of_returns_in_ord_order() {
354        let mut g: Graph<&'static str> = Graph::new();
355        g.add_edge("a", "z");
356        g.add_edge("a", "b");
357        g.add_edge("a", "m");
358        let deps: Vec<&&str> = g.dependencies_of(&"a").collect();
359        assert_eq!(deps, vec![&"b", &"m", &"z"]);
360    }
361
362    #[test]
363    fn edges_yields_correct_pair_for_single_edge() {
364        // Build a Graph<NodeId> with one edge and assert that edges() returns
365        // exactly that (from, to) pair. add_edge registers both endpoints as
366        // nodes, so no prior add_node calls are needed.
367        use crate::identifier::Identifier;
368        let mut g: Graph<NodeId> = Graph::new();
369        let schema_a = NodeId::Schema(Identifier::from_unquoted("a").unwrap());
370        let schema_b = NodeId::Schema(Identifier::from_unquoted("b").unwrap());
371        g.add_edge(schema_a.clone(), schema_b.clone());
372        let pairs: Vec<(&NodeId, &NodeId)> = g.edges().collect();
373        assert_eq!(pairs, vec![(&schema_a, &schema_b)]);
374    }
375}