1use std::collections::{HashMap, HashSet};
30
31use super::{CompiledSchema, FieldType};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct CyclePath {
38 pub nodes: Vec<String>,
41}
42
43impl CyclePath {
44 #[must_use]
46 pub fn new(nodes: Vec<String>) -> Self {
47 Self { nodes }
48 }
49
50 #[must_use]
61 pub fn path_string(&self) -> String {
62 if self.nodes.is_empty() {
63 return String::new();
64 }
65 let mut path = self.nodes.join(" → ");
66 path.push_str(" → ");
68 path.push_str(&self.nodes[0]);
69 path
70 }
71
72 #[must_use]
74 pub fn is_self_reference(&self) -> bool {
75 self.nodes.len() == 1
76 }
77
78 #[must_use]
80 pub fn len(&self) -> usize {
81 self.nodes.len()
82 }
83
84 #[must_use]
86 pub fn is_empty(&self) -> bool {
87 self.nodes.is_empty()
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct ChangeImpact {
94 pub affected_types: HashSet<String>,
96 pub breaking_changes: Vec<String>,
98}
99
100impl ChangeImpact {
101 #[must_use]
103 pub fn new(affected_types: HashSet<String>, breaking_changes: Vec<String>) -> Self {
104 Self {
105 affected_types,
106 breaking_changes,
107 }
108 }
109
110 #[must_use]
112 pub fn has_impact(&self) -> bool {
113 !self.affected_types.is_empty()
114 }
115}
116
117#[derive(Debug, Clone)]
124pub struct SchemaDependencyGraph {
125 outgoing: HashMap<String, HashSet<String>>,
127 incoming: HashMap<String, HashSet<String>>,
129 all_types: HashSet<String>,
131 root_types: HashSet<String>,
133}
134
135impl SchemaDependencyGraph {
136 #[must_use]
141 pub fn build(schema: &CompiledSchema) -> Self {
142 let mut outgoing: HashMap<String, HashSet<String>> = HashMap::new();
143 let mut incoming: HashMap<String, HashSet<String>> = HashMap::new();
144 let mut all_types: HashSet<String> = HashSet::new();
145 let mut root_types: HashSet<String> = HashSet::new();
146
147 for type_def in &schema.types {
149 all_types.insert(type_def.name.clone());
150 outgoing.entry(type_def.name.clone()).or_default();
151 incoming.entry(type_def.name.clone()).or_default();
152 }
153
154 for enum_def in &schema.enums {
155 all_types.insert(enum_def.name.clone());
156 outgoing.entry(enum_def.name.clone()).or_default();
157 incoming.entry(enum_def.name.clone()).or_default();
158 }
159
160 for input_def in &schema.input_types {
161 all_types.insert(input_def.name.clone());
162 outgoing.entry(input_def.name.clone()).or_default();
163 incoming.entry(input_def.name.clone()).or_default();
164 }
165
166 for interface_def in &schema.interfaces {
167 all_types.insert(interface_def.name.clone());
168 outgoing.entry(interface_def.name.clone()).or_default();
169 incoming.entry(interface_def.name.clone()).or_default();
170 }
171
172 for union_def in &schema.unions {
173 all_types.insert(union_def.name.clone());
174 outgoing.entry(union_def.name.clone()).or_default();
175 incoming.entry(union_def.name.clone()).or_default();
176 }
177
178 if !schema.queries.is_empty() {
180 root_types.insert("Query".to_string());
181 all_types.insert("Query".to_string());
182 outgoing.entry("Query".to_string()).or_default();
183 incoming.entry("Query".to_string()).or_default();
184 }
185 if !schema.mutations.is_empty() {
186 root_types.insert("Mutation".to_string());
187 all_types.insert("Mutation".to_string());
188 outgoing.entry("Mutation".to_string()).or_default();
189 incoming.entry("Mutation".to_string()).or_default();
190 }
191 if !schema.subscriptions.is_empty() {
192 root_types.insert("Subscription".to_string());
193 all_types.insert("Subscription".to_string());
194 outgoing.entry("Subscription".to_string()).or_default();
195 incoming.entry("Subscription".to_string()).or_default();
196 }
197
198 for type_def in &schema.types {
200 for field in &type_def.fields {
201 if let Some(ref_type) = Self::extract_referenced_type(&field.field_type) {
202 if all_types.contains(&ref_type) {
203 outgoing.entry(type_def.name.clone()).or_default().insert(ref_type.clone());
204 incoming.entry(ref_type.clone()).or_default().insert(type_def.name.clone());
205 }
206 }
207 }
208
209 for interface_name in &type_def.implements {
211 if all_types.contains(interface_name) {
212 outgoing
213 .entry(type_def.name.clone())
214 .or_default()
215 .insert(interface_name.clone());
216 incoming
217 .entry(interface_name.clone())
218 .or_default()
219 .insert(type_def.name.clone());
220 }
221 }
222 }
223
224 for interface_def in &schema.interfaces {
226 for field in &interface_def.fields {
227 if let Some(ref_type) = Self::extract_referenced_type(&field.field_type) {
228 if all_types.contains(&ref_type) {
229 outgoing
230 .entry(interface_def.name.clone())
231 .or_default()
232 .insert(ref_type.clone());
233 incoming
234 .entry(ref_type.clone())
235 .or_default()
236 .insert(interface_def.name.clone());
237 }
238 }
239 }
240 }
241
242 for union_def in &schema.unions {
244 for member_type in &union_def.member_types {
245 if all_types.contains(member_type) {
246 outgoing.entry(union_def.name.clone()).or_default().insert(member_type.clone());
247 incoming.entry(member_type.clone()).or_default().insert(union_def.name.clone());
248 }
249 }
250 }
251
252 for input_def in &schema.input_types {
254 for field in &input_def.fields {
255 let parsed = FieldType::parse(&field.field_type, &all_types);
257 if let Some(ref_type) = Self::extract_referenced_type(&parsed) {
258 if all_types.contains(&ref_type) {
259 outgoing
260 .entry(input_def.name.clone())
261 .or_default()
262 .insert(ref_type.clone());
263 incoming
264 .entry(ref_type.clone())
265 .or_default()
266 .insert(input_def.name.clone());
267 }
268 }
269 }
270 }
271
272 for query in &schema.queries {
274 let parsed = FieldType::parse(&query.return_type, &all_types);
275 if let Some(ref_type) = Self::extract_referenced_type(&parsed) {
276 if all_types.contains(&ref_type) {
277 outgoing.entry("Query".to_string()).or_default().insert(ref_type.clone());
278 incoming.entry(ref_type.clone()).or_default().insert("Query".to_string());
279 }
280 }
281 }
282
283 for mutation in &schema.mutations {
285 let parsed = FieldType::parse(&mutation.return_type, &all_types);
286 if let Some(ref_type) = Self::extract_referenced_type(&parsed) {
287 if all_types.contains(&ref_type) {
288 outgoing.entry("Mutation".to_string()).or_default().insert(ref_type.clone());
289 incoming.entry(ref_type.clone()).or_default().insert("Mutation".to_string());
290 }
291 }
292 }
293
294 for subscription in &schema.subscriptions {
296 let parsed = FieldType::parse(&subscription.return_type, &all_types);
297 if let Some(ref_type) = Self::extract_referenced_type(&parsed) {
298 if all_types.contains(&ref_type) {
299 outgoing
300 .entry("Subscription".to_string())
301 .or_default()
302 .insert(ref_type.clone());
303 incoming
304 .entry(ref_type.clone())
305 .or_default()
306 .insert("Subscription".to_string());
307 }
308 }
309 }
310
311 Self {
312 outgoing,
313 incoming,
314 all_types,
315 root_types,
316 }
317 }
318
319 fn extract_referenced_type(field_type: &FieldType) -> Option<String> {
321 match field_type {
322 FieldType::Object(name)
323 | FieldType::Enum(name)
324 | FieldType::Input(name)
325 | FieldType::Interface(name)
326 | FieldType::Union(name) => Some(name.clone()),
327 FieldType::List(inner) => Self::extract_referenced_type(inner),
328 _ => None, }
330 }
331
332 #[must_use]
334 pub fn dependencies_of(&self, type_name: &str) -> Vec<String> {
335 self.outgoing
336 .get(type_name)
337 .map(|deps| {
338 let mut v: Vec<_> = deps.iter().cloned().collect();
339 v.sort();
340 v
341 })
342 .unwrap_or_default()
343 }
344
345 #[must_use]
347 pub fn dependents_of(&self, type_name: &str) -> Vec<String> {
348 self.incoming
349 .get(type_name)
350 .map(|refs| {
351 let mut v: Vec<_> = refs.iter().cloned().collect();
352 v.sort();
353 v
354 })
355 .unwrap_or_default()
356 }
357
358 #[must_use]
360 pub fn all_types(&self) -> Vec<String> {
361 let mut types: Vec<_> = self.all_types.iter().cloned().collect();
362 types.sort();
363 types
364 }
365
366 #[must_use]
368 pub fn type_count(&self) -> usize {
369 self.all_types.len()
370 }
371
372 #[must_use]
374 pub fn has_type(&self, type_name: &str) -> bool {
375 self.all_types.contains(type_name)
376 }
377
378 #[must_use]
383 pub fn find_cycles(&self) -> Vec<CyclePath> {
384 let mut visited = HashSet::new();
385 let mut rec_stack = HashSet::new();
386 let mut cycles = Vec::new();
387
388 for type_name in &self.all_types {
389 if !visited.contains(type_name) {
390 self.dfs_find_cycles(
391 type_name,
392 &mut visited,
393 &mut rec_stack,
394 &mut Vec::new(),
395 &mut cycles,
396 );
397 }
398 }
399
400 Self::normalize_cycles(cycles)
402 }
403
404 fn dfs_find_cycles(
406 &self,
407 node: &str,
408 visited: &mut HashSet<String>,
409 rec_stack: &mut HashSet<String>,
410 path: &mut Vec<String>,
411 cycles: &mut Vec<CyclePath>,
412 ) {
413 visited.insert(node.to_string());
414 rec_stack.insert(node.to_string());
415 path.push(node.to_string());
416
417 if let Some(deps) = self.outgoing.get(node) {
418 for dep in deps {
419 if !visited.contains(dep) {
420 self.dfs_find_cycles(dep, visited, rec_stack, path, cycles);
421 } else if rec_stack.contains(dep) {
422 if let Some(start_idx) = path.iter().position(|x| x == dep) {
424 let cycle_nodes: Vec<String> = path[start_idx..].to_vec();
425 cycles.push(CyclePath::new(cycle_nodes));
426 }
427 }
428 }
429 }
430
431 rec_stack.remove(node);
432 path.pop();
433 }
434
435 fn normalize_cycles(mut cycles: Vec<CyclePath>) -> Vec<CyclePath> {
438 for cycle in &mut cycles {
440 if cycle.nodes.is_empty() {
441 continue;
442 }
443 let min_pos = cycle
445 .nodes
446 .iter()
447 .enumerate()
448 .min_by_key(|(_, name)| *name)
449 .map(|(i, _)| i)
450 .unwrap_or(0);
451
452 cycle.nodes.rotate_left(min_pos);
454 }
455
456 cycles.sort_by(|a, b| a.nodes.cmp(&b.nodes));
458
459 cycles.dedup();
461
462 cycles
463 }
464
465 #[must_use]
470 pub fn find_unused(&self) -> Vec<String> {
471 let mut unused = Vec::new();
472
473 for type_name in &self.all_types {
474 if self.root_types.contains(type_name) {
476 continue;
477 }
478
479 let has_references =
481 self.incoming.get(type_name).map(|refs| !refs.is_empty()).unwrap_or(false);
482
483 if !has_references {
484 unused.push(type_name.clone());
485 }
486 }
487
488 unused.sort();
489 unused
490 }
491
492 #[must_use]
497 pub fn impact_of_deletion(&self, type_name: &str) -> ChangeImpact {
498 let mut affected = HashSet::new();
499 let mut breaking_changes = Vec::new();
500
501 let mut to_visit = vec![type_name.to_string()];
503 let mut visited = HashSet::new();
504
505 while let Some(current) = to_visit.pop() {
506 if !visited.insert(current.clone()) {
507 continue;
508 }
509
510 if let Some(dependents) = self.incoming.get(¤t) {
512 for dependent in dependents {
513 if !visited.contains(dependent) {
514 affected.insert(dependent.clone());
515 to_visit.push(dependent.clone());
516 breaking_changes.push(format!(
517 "Type '{}' references '{}' which would be deleted",
518 dependent, type_name
519 ));
520 }
521 }
522 }
523 }
524
525 affected.remove(type_name);
527
528 ChangeImpact::new(affected, breaking_changes)
529 }
530
531 #[must_use]
533 pub fn transitive_dependencies(&self, type_name: &str) -> HashSet<String> {
534 let mut visited = HashSet::new();
535 let mut to_visit = vec![type_name.to_string()];
536
537 while let Some(current) = to_visit.pop() {
538 if !visited.insert(current.clone()) {
539 continue;
540 }
541
542 if let Some(deps) = self.outgoing.get(¤t) {
543 for dep in deps {
544 if !visited.contains(dep) {
545 to_visit.push(dep.clone());
546 }
547 }
548 }
549 }
550
551 visited.remove(type_name);
553 visited
554 }
555
556 #[must_use]
558 pub fn transitive_dependents(&self, type_name: &str) -> HashSet<String> {
559 let mut visited = HashSet::new();
560 let mut to_visit = vec![type_name.to_string()];
561
562 while let Some(current) = to_visit.pop() {
563 if !visited.insert(current.clone()) {
564 continue;
565 }
566
567 if let Some(refs) = self.incoming.get(¤t) {
568 for ref_type in refs {
569 if !visited.contains(ref_type) {
570 to_visit.push(ref_type.clone());
571 }
572 }
573 }
574 }
575
576 visited.remove(type_name);
578 visited
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use crate::schema::{
586 EnumDefinition, EnumValueDefinition, FieldDefinition, InputFieldDefinition,
587 InputObjectDefinition, InterfaceDefinition, MutationDefinition, QueryDefinition,
588 SubscriptionDefinition, TypeDefinition, UnionDefinition,
589 };
590
591 fn make_type(name: &str, fields: Vec<(&str, FieldType)>) -> TypeDefinition {
593 TypeDefinition {
594 name: name.to_string(),
595 sql_source: format!("v_{}", name.to_lowercase()),
596 jsonb_column: "data".to_string(),
597 fields: fields
598 .into_iter()
599 .map(|(n, ft)| FieldDefinition::new(n, ft))
600 .collect(),
601 description: None,
602 sql_projection_hint: None,
603 implements: vec![],
604 is_error: false,
605 }
606 }
607
608 #[test]
613 fn test_empty_schema() {
614 let schema = CompiledSchema::default();
615 let graph = SchemaDependencyGraph::build(&schema);
616
617 assert_eq!(graph.type_count(), 0);
618 assert!(graph.find_cycles().is_empty());
619 assert!(graph.find_unused().is_empty());
620 }
621
622 #[test]
623 fn test_single_type_no_dependencies() {
624 let schema = CompiledSchema {
625 types: vec![make_type(
626 "User",
627 vec![
628 ("id", FieldType::Id),
629 ("name", FieldType::String),
630 ("email", FieldType::String),
631 ],
632 )],
633 queries: vec![QueryDefinition::new("users", "User").returning_list()],
634 ..Default::default()
635 };
636
637 let graph = SchemaDependencyGraph::build(&schema);
638
639 assert!(graph.has_type("User"));
640 assert!(graph.has_type("Query"));
641 assert_eq!(graph.dependencies_of("User").len(), 0);
642 assert_eq!(graph.dependents_of("User"), vec!["Query"]);
643 }
644
645 #[test]
646 fn test_type_with_object_reference() {
647 let schema = CompiledSchema {
648 types: vec![
649 make_type(
650 "User",
651 vec![
652 ("id", FieldType::Id),
653 ("profile", FieldType::Object("Profile".to_string())),
654 ],
655 ),
656 make_type("Profile", vec![("bio", FieldType::String)]),
657 ],
658 queries: vec![QueryDefinition::new("users", "User").returning_list()],
659 ..Default::default()
660 };
661
662 let graph = SchemaDependencyGraph::build(&schema);
663
664 assert_eq!(graph.dependencies_of("User"), vec!["Profile"]);
666 assert_eq!(graph.dependents_of("Profile"), vec!["User"]);
668 }
669
670 #[test]
671 fn test_type_with_list_reference() {
672 let schema = CompiledSchema {
673 types: vec![
674 make_type(
675 "User",
676 vec![
677 ("id", FieldType::Id),
678 ("posts", FieldType::List(Box::new(FieldType::Object("Post".to_string())))),
679 ],
680 ),
681 make_type("Post", vec![("title", FieldType::String)]),
682 ],
683 queries: vec![QueryDefinition::new("users", "User").returning_list()],
684 ..Default::default()
685 };
686
687 let graph = SchemaDependencyGraph::build(&schema);
688
689 assert_eq!(graph.dependencies_of("User"), vec!["Post"]);
691 assert_eq!(graph.dependents_of("Post"), vec!["User"]);
692 }
693
694 #[test]
695 fn test_enum_reference() {
696 let schema = CompiledSchema {
697 types: vec![make_type(
698 "User",
699 vec![
700 ("id", FieldType::Id),
701 ("status", FieldType::Enum("UserStatus".to_string())),
702 ],
703 )],
704 enums: vec![EnumDefinition {
705 name: "UserStatus".to_string(),
706 values: vec![
707 EnumValueDefinition::new("ACTIVE"),
708 EnumValueDefinition::new("INACTIVE"),
709 ],
710 description: None,
711 }],
712 queries: vec![QueryDefinition::new("users", "User").returning_list()],
713 ..Default::default()
714 };
715
716 let graph = SchemaDependencyGraph::build(&schema);
717
718 assert!(graph.has_type("UserStatus"));
719 assert_eq!(graph.dependencies_of("User"), vec!["UserStatus"]);
720 assert_eq!(graph.dependents_of("UserStatus"), vec!["User"]);
721 }
722
723 #[test]
728 fn test_no_cycles() {
729 let schema = CompiledSchema {
730 types: vec![
731 make_type("User", vec![("profile", FieldType::Object("Profile".to_string()))]),
732 make_type("Profile", vec![("bio", FieldType::String)]),
733 ],
734 queries: vec![QueryDefinition::new("users", "User").returning_list()],
735 ..Default::default()
736 };
737
738 let graph = SchemaDependencyGraph::build(&schema);
739 let cycles = graph.find_cycles();
740
741 assert!(cycles.is_empty());
742 }
743
744 #[test]
745 fn test_self_referencing_cycle() {
746 let schema = CompiledSchema {
747 types: vec![make_type(
748 "Node",
749 vec![
750 ("id", FieldType::Id),
751 ("next", FieldType::Object("Node".to_string())),
752 ],
753 )],
754 queries: vec![QueryDefinition::new("nodes", "Node").returning_list()],
755 ..Default::default()
756 };
757
758 let graph = SchemaDependencyGraph::build(&schema);
759 let cycles = graph.find_cycles();
760
761 assert_eq!(cycles.len(), 1);
762 assert_eq!(cycles[0].nodes, vec!["Node"]);
763 assert!(cycles[0].is_self_reference());
764 assert_eq!(cycles[0].path_string(), "Node → Node");
765 }
766
767 #[test]
768 fn test_two_node_cycle() {
769 let schema = CompiledSchema {
770 types: vec![
771 make_type("A", vec![("b", FieldType::Object("B".to_string()))]),
772 make_type("B", vec![("a", FieldType::Object("A".to_string()))]),
773 ],
774 queries: vec![QueryDefinition::new("items", "A").returning_list()],
775 ..Default::default()
776 };
777
778 let graph = SchemaDependencyGraph::build(&schema);
779 let cycles = graph.find_cycles();
780
781 assert_eq!(cycles.len(), 1);
782 assert_eq!(cycles[0].len(), 2);
783 assert_eq!(cycles[0].nodes, vec!["A", "B"]);
785 assert_eq!(cycles[0].path_string(), "A → B → A");
786 }
787
788 #[test]
789 fn test_three_node_cycle() {
790 let schema = CompiledSchema {
791 types: vec![
792 make_type("A", vec![("b", FieldType::Object("B".to_string()))]),
793 make_type("B", vec![("c", FieldType::Object("C".to_string()))]),
794 make_type("C", vec![("a", FieldType::Object("A".to_string()))]),
795 ],
796 queries: vec![QueryDefinition::new("items", "A").returning_list()],
797 ..Default::default()
798 };
799
800 let graph = SchemaDependencyGraph::build(&schema);
801 let cycles = graph.find_cycles();
802
803 assert_eq!(cycles.len(), 1);
804 assert_eq!(cycles[0].len(), 3);
805 assert_eq!(cycles[0].nodes, vec!["A", "B", "C"]);
806 assert_eq!(cycles[0].path_string(), "A → B → C → A");
807 }
808
809 #[test]
810 fn test_multiple_independent_cycles() {
811 let schema = CompiledSchema {
812 types: vec![
813 make_type("A", vec![("b", FieldType::Object("B".to_string()))]),
815 make_type("B", vec![("a", FieldType::Object("A".to_string()))]),
816 make_type("X", vec![("y", FieldType::Object("Y".to_string()))]),
818 make_type("Y", vec![("x", FieldType::Object("X".to_string()))]),
819 ],
820 queries: vec![
821 QueryDefinition::new("aItems", "A").returning_list(),
822 QueryDefinition::new("xItems", "X").returning_list(),
823 ],
824 ..Default::default()
825 };
826
827 let graph = SchemaDependencyGraph::build(&schema);
828 let cycles = graph.find_cycles();
829
830 assert_eq!(cycles.len(), 2);
831 }
832
833 #[test]
838 fn test_no_unused_types() {
839 let schema = CompiledSchema {
840 types: vec![
841 make_type("User", vec![("profile", FieldType::Object("Profile".to_string()))]),
842 make_type("Profile", vec![("bio", FieldType::String)]),
843 ],
844 queries: vec![QueryDefinition::new("users", "User").returning_list()],
845 ..Default::default()
846 };
847
848 let graph = SchemaDependencyGraph::build(&schema);
849 let unused = graph.find_unused();
850
851 assert!(unused.is_empty());
852 }
853
854 #[test]
855 fn test_unused_type_no_references() {
856 let schema = CompiledSchema {
857 types: vec![
858 make_type("User", vec![("name", FieldType::String)]),
859 make_type("OrphanType", vec![("data", FieldType::String)]),
860 ],
861 queries: vec![QueryDefinition::new("users", "User").returning_list()],
862 ..Default::default()
863 };
864
865 let graph = SchemaDependencyGraph::build(&schema);
866 let unused = graph.find_unused();
867
868 assert_eq!(unused, vec!["OrphanType"]);
869 }
870
871 #[test]
872 fn test_multiple_unused_types() {
873 let schema = CompiledSchema {
874 types: vec![
875 make_type("User", vec![("name", FieldType::String)]),
876 make_type("Orphan1", vec![("data", FieldType::String)]),
877 make_type("Orphan2", vec![("data", FieldType::String)]),
878 ],
879 queries: vec![QueryDefinition::new("users", "User").returning_list()],
880 ..Default::default()
881 };
882
883 let graph = SchemaDependencyGraph::build(&schema);
884 let unused = graph.find_unused();
885
886 assert_eq!(unused, vec!["Orphan1", "Orphan2"]);
887 }
888
889 #[test]
890 fn test_root_types_never_unused() {
891 let schema = CompiledSchema {
893 types: vec![make_type("User", vec![("name", FieldType::String)])],
894 queries: vec![QueryDefinition::new("users", "User").returning_list()],
895 ..Default::default()
896 };
897
898 let graph = SchemaDependencyGraph::build(&schema);
899 let unused = graph.find_unused();
900
901 assert!(!unused.contains(&"Query".to_string()));
903 }
904
905 #[test]
910 fn test_impact_of_deletion_no_dependents() {
911 let schema = CompiledSchema {
912 types: vec![
913 make_type("User", vec![("profile", FieldType::Object("Profile".to_string()))]),
914 make_type("Profile", vec![("bio", FieldType::String)]),
915 ],
916 queries: vec![QueryDefinition::new("users", "User").returning_list()],
917 ..Default::default()
918 };
919
920 let graph = SchemaDependencyGraph::build(&schema);
921
922 let impact = graph.impact_of_deletion("Profile");
924 assert!(impact.has_impact());
925 assert!(impact.affected_types.contains("User"));
926 }
927
928 #[test]
929 fn test_impact_of_deletion_chain() {
930 let schema = CompiledSchema {
931 types: vec![
932 make_type("A", vec![("b", FieldType::Object("B".to_string()))]),
933 make_type("B", vec![("c", FieldType::Object("C".to_string()))]),
934 make_type("C", vec![("d", FieldType::Object("D".to_string()))]),
935 make_type("D", vec![("value", FieldType::String)]),
936 ],
937 queries: vec![QueryDefinition::new("items", "A").returning_list()],
938 ..Default::default()
939 };
940
941 let graph = SchemaDependencyGraph::build(&schema);
942
943 let impact = graph.impact_of_deletion("D");
945 assert!(impact.affected_types.contains("C"));
946 assert!(impact.affected_types.contains("B"));
947 assert!(impact.affected_types.contains("A"));
948 }
949
950 #[test]
955 fn test_transitive_dependencies() {
956 let schema = CompiledSchema {
957 types: vec![
958 make_type("A", vec![("b", FieldType::Object("B".to_string()))]),
959 make_type("B", vec![("c", FieldType::Object("C".to_string()))]),
960 make_type("C", vec![("value", FieldType::String)]),
961 ],
962 queries: vec![QueryDefinition::new("items", "A").returning_list()],
963 ..Default::default()
964 };
965
966 let graph = SchemaDependencyGraph::build(&schema);
967
968 let deps = graph.transitive_dependencies("A");
969 assert!(deps.contains("B"));
970 assert!(deps.contains("C"));
971 assert!(!deps.contains("A")); }
973
974 #[test]
975 fn test_transitive_dependents() {
976 let schema = CompiledSchema {
977 types: vec![
978 make_type("A", vec![("b", FieldType::Object("B".to_string()))]),
979 make_type("B", vec![("c", FieldType::Object("C".to_string()))]),
980 make_type("C", vec![("value", FieldType::String)]),
981 ],
982 queries: vec![QueryDefinition::new("items", "A").returning_list()],
983 ..Default::default()
984 };
985
986 let graph = SchemaDependencyGraph::build(&schema);
987
988 let refs = graph.transitive_dependents("C");
989 assert!(refs.contains("B"));
990 assert!(refs.contains("A"));
991 assert!(refs.contains("Query"));
992 assert!(!refs.contains("C")); }
994
995 #[test]
1000 fn test_interface_dependencies() {
1001 let schema = CompiledSchema {
1002 types: vec![TypeDefinition {
1003 name: "User".to_string(),
1004 sql_source: "v_user".to_string(),
1005 jsonb_column: "data".to_string(),
1006 fields: vec![FieldDefinition::new("id", FieldType::Id)],
1007 description: None,
1008 sql_projection_hint: None,
1009 implements: vec!["Node".to_string()],
1010 is_error: false,
1011 }],
1012 interfaces: vec![InterfaceDefinition {
1013 name: "Node".to_string(),
1014 fields: vec![FieldDefinition::new("id", FieldType::Id)],
1015 description: None,
1016 }],
1017 queries: vec![QueryDefinition::new("users", "User").returning_list()],
1018 ..Default::default()
1019 };
1020
1021 let graph = SchemaDependencyGraph::build(&schema);
1022
1023 assert!(graph.dependencies_of("User").contains(&"Node".to_string()));
1025 assert!(graph.dependents_of("Node").contains(&"User".to_string()));
1027 }
1028
1029 #[test]
1030 fn test_union_dependencies() {
1031 let schema = CompiledSchema {
1032 types: vec![
1033 make_type("User", vec![("name", FieldType::String)]),
1034 make_type("Post", vec![("title", FieldType::String)]),
1035 ],
1036 unions: vec![UnionDefinition {
1037 name: "SearchResult".to_string(),
1038 member_types: vec!["User".to_string(), "Post".to_string()],
1039 description: None,
1040 }],
1041 queries: vec![QueryDefinition::new("search", "SearchResult").returning_list()],
1042 ..Default::default()
1043 };
1044
1045 let graph = SchemaDependencyGraph::build(&schema);
1046
1047 let deps = graph.dependencies_of("SearchResult");
1049 assert!(deps.contains(&"User".to_string()));
1050 assert!(deps.contains(&"Post".to_string()));
1051 }
1052
1053 #[test]
1058 fn test_input_type_dependencies() {
1059 let schema = CompiledSchema {
1060 types: vec![make_type("User", vec![("name", FieldType::String)])],
1061 input_types: vec![
1062 InputObjectDefinition {
1063 name: "UserFilter".to_string(),
1064 fields: vec![InputFieldDefinition::new("status", "UserStatus")],
1065 description: None,
1066 metadata: None,
1067 },
1068 InputObjectDefinition {
1069 name: "UserStatus".to_string(),
1070 fields: vec![InputFieldDefinition::new("active", "Boolean")],
1071 description: None,
1072 metadata: None,
1073 },
1074 ],
1075 queries: vec![QueryDefinition::new("users", "User").returning_list()],
1076 ..Default::default()
1077 };
1078
1079 let graph = SchemaDependencyGraph::build(&schema);
1080
1081 assert!(graph.has_type("UserFilter"));
1083 assert!(graph.has_type("UserStatus"));
1084 assert!(graph.dependencies_of("UserFilter").contains(&"UserStatus".to_string()));
1085 }
1086
1087 #[test]
1092 fn test_mutation_return_type_dependency() {
1093 let schema = CompiledSchema {
1094 types: vec![make_type("User", vec![("name", FieldType::String)])],
1095 mutations: vec![MutationDefinition::new("createUser", "User")],
1096 ..Default::default()
1097 };
1098
1099 let graph = SchemaDependencyGraph::build(&schema);
1100
1101 assert!(graph.has_type("Mutation"));
1102 assert!(graph.dependencies_of("Mutation").contains(&"User".to_string()));
1103 assert!(graph.dependents_of("User").contains(&"Mutation".to_string()));
1104 }
1105
1106 #[test]
1107 fn test_subscription_return_type_dependency() {
1108 let schema = CompiledSchema {
1109 types: vec![make_type("User", vec![("name", FieldType::String)])],
1110 subscriptions: vec![SubscriptionDefinition::new("userCreated", "User")],
1111 ..Default::default()
1112 };
1113
1114 let graph = SchemaDependencyGraph::build(&schema);
1115
1116 assert!(graph.has_type("Subscription"));
1117 assert!(graph.dependencies_of("Subscription").contains(&"User".to_string()));
1118 }
1119
1120 #[test]
1125 fn test_cycle_path_formatting() {
1126 let cycle = CyclePath::new(vec!["A".to_string(), "B".to_string(), "C".to_string()]);
1127 assert_eq!(cycle.path_string(), "A → B → C → A");
1128 assert_eq!(cycle.len(), 3);
1129 assert!(!cycle.is_self_reference());
1130 assert!(!cycle.is_empty());
1131 }
1132
1133 #[test]
1134 fn test_cycle_path_self_reference() {
1135 let cycle = CyclePath::new(vec!["Node".to_string()]);
1136 assert_eq!(cycle.path_string(), "Node → Node");
1137 assert!(cycle.is_self_reference());
1138 }
1139
1140 #[test]
1141 fn test_cycle_path_empty() {
1142 let cycle = CyclePath::new(vec![]);
1143 assert_eq!(cycle.path_string(), "");
1144 assert!(cycle.is_empty());
1145 }
1146}