fraiseql_core/schema/dependency_graph/
analysis.rs1use std::collections::HashSet;
4
5use super::{
6 graph::SchemaDependencyGraph,
7 types::{ChangeImpact, CyclePath},
8};
9
10impl SchemaDependencyGraph {
11 #[must_use]
16 pub fn find_cycles(&self) -> Vec<CyclePath> {
17 let mut visited = HashSet::new();
18 let mut rec_stack = HashSet::new();
19 let mut cycles = Vec::new();
20
21 for type_name in &self.all_types {
22 if !visited.contains(type_name) {
23 self.dfs_find_cycles(
24 type_name,
25 &mut visited,
26 &mut rec_stack,
27 &mut Vec::new(),
28 &mut cycles,
29 );
30 }
31 }
32
33 Self::normalize_cycles(cycles)
35 }
36
37 fn dfs_find_cycles(
39 &self,
40 node: &str,
41 visited: &mut HashSet<String>,
42 rec_stack: &mut HashSet<String>,
43 path: &mut Vec<String>,
44 cycles: &mut Vec<CyclePath>,
45 ) {
46 visited.insert(node.to_string());
47 rec_stack.insert(node.to_string());
48 path.push(node.to_string());
49
50 if let Some(deps) = self.outgoing.get(node) {
51 for dep in deps {
52 if !visited.contains(dep) {
53 self.dfs_find_cycles(dep, visited, rec_stack, path, cycles);
54 } else if rec_stack.contains(dep) {
55 if let Some(start_idx) = path.iter().position(|x| x == dep) {
57 let cycle_nodes: Vec<String> = path[start_idx..].to_vec();
58 cycles.push(CyclePath::new(cycle_nodes));
59 }
60 }
61 }
62 }
63
64 rec_stack.remove(node);
65 path.pop();
66 }
67
68 fn normalize_cycles(mut cycles: Vec<CyclePath>) -> Vec<CyclePath> {
71 for cycle in &mut cycles {
73 if cycle.nodes.is_empty() {
74 continue;
75 }
76 let min_pos = cycle
78 .nodes
79 .iter()
80 .enumerate()
81 .min_by_key(|(_, name)| *name)
82 .map_or(0, |(i, _)| i);
83
84 cycle.nodes.rotate_left(min_pos);
86 }
87
88 cycles.sort_by(|a, b| a.nodes.cmp(&b.nodes));
90
91 cycles.dedup();
93
94 cycles
95 }
96
97 #[must_use]
102 pub fn find_unused(&self) -> Vec<String> {
103 let mut unused = Vec::new();
104
105 for type_name in &self.all_types {
106 if self.root_types.contains(type_name) {
108 continue;
109 }
110
111 let has_references = self.incoming.get(type_name).is_some_and(|refs| !refs.is_empty());
113
114 if !has_references {
115 unused.push(type_name.clone());
116 }
117 }
118
119 unused.sort();
120 unused
121 }
122
123 #[must_use]
128 pub fn impact_of_deletion(&self, type_name: &str) -> ChangeImpact {
129 let mut affected = HashSet::new();
130 let mut breaking_changes = Vec::new();
131
132 let mut to_visit = vec![type_name.to_string()];
134 let mut visited = HashSet::new();
135
136 while let Some(current) = to_visit.pop() {
137 if !visited.insert(current.clone()) {
138 continue;
139 }
140
141 if let Some(dependents) = self.incoming.get(¤t) {
143 for dependent in dependents {
144 if !visited.contains(dependent) {
145 affected.insert(dependent.clone());
146 to_visit.push(dependent.clone());
147 breaking_changes.push(format!(
148 "Type '{}' references '{}' which would be deleted",
149 dependent, type_name
150 ));
151 }
152 }
153 }
154 }
155
156 affected.remove(type_name);
158
159 ChangeImpact::new(affected, breaking_changes)
160 }
161
162 #[must_use]
164 pub fn transitive_dependencies(&self, type_name: &str) -> HashSet<String> {
165 let mut visited = HashSet::new();
166 let mut to_visit = vec![type_name.to_string()];
167
168 while let Some(current) = to_visit.pop() {
169 if !visited.insert(current.clone()) {
170 continue;
171 }
172
173 if let Some(deps) = self.outgoing.get(¤t) {
174 for dep in deps {
175 if !visited.contains(dep) {
176 to_visit.push(dep.clone());
177 }
178 }
179 }
180 }
181
182 visited.remove(type_name);
184 visited
185 }
186
187 #[must_use]
189 pub fn transitive_dependents(&self, type_name: &str) -> HashSet<String> {
190 let mut visited = HashSet::new();
191 let mut to_visit = vec![type_name.to_string()];
192
193 while let Some(current) = to_visit.pop() {
194 if !visited.insert(current.clone()) {
195 continue;
196 }
197
198 if let Some(refs) = self.incoming.get(¤t) {
199 for ref_type in refs {
200 if !visited.contains(ref_type) {
201 to_visit.push(ref_type.clone());
202 }
203 }
204 }
205 }
206
207 visited.remove(type_name);
209 visited
210 }
211}