Skip to main content

safe_migrate/analysis/
graph.rs

1// FILE: src/analysis/graph.rs
2use crate::ast::identifiers::ObjectId;
3
4#[derive(Debug, Clone, PartialEq)]
5pub struct FkEdge {
6    pub constraint_name: Option<String>,
7    pub from_table: ObjectId,
8    pub from_columns: Vec<String>,
9    pub to_table: ObjectId,
10    pub to_columns: Vec<String>,
11    pub from_generation: u64,
12}
13
14#[derive(Debug, Clone, PartialEq)]
15pub struct ViewEdge {
16    pub view_id: ObjectId,
17    pub depends_on: Vec<ObjectId>,
18    pub view_generation: u64,
19}
20
21#[derive(Debug, Clone, PartialEq)]
22pub struct IndexEdge {
23    pub index_id: ObjectId,
24    pub relation_id: ObjectId,
25    pub using_method: Option<String>,
26    pub has_predicate: bool,
27    pub is_concurrent: bool,
28}
29
30#[derive(Debug, Clone, PartialEq)]
31pub struct RenameEdge {
32    pub from: ObjectId,
33    pub to: ObjectId,
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub struct PartitionEdge {
38    pub parent: ObjectId,
39    pub child: ObjectId,
40}
41
42#[derive(Debug, Clone, PartialEq)]
43pub struct SequenceEdge {
44    pub sequence_id: ObjectId,
45    pub table_id: ObjectId,
46    pub column: String,
47}
48
49#[derive(Debug, Clone, Default)]
50pub struct DependencyGraph {
51    pub foreign_keys: Vec<FkEdge>,
52    pub views: Vec<ViewEdge>,
53    pub indexes: Vec<IndexEdge>,
54    pub renames: Vec<RenameEdge>,
55    pub partitions: Vec<PartitionEdge>,
56    pub sequences: Vec<SequenceEdge>,
57}
58
59impl DependencyGraph {
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    // Phase 3 FIX (BUG-004): Traverse rename chains dynamically for accurate topology reads
65    pub fn is_referenced_by_view(&self, id: &ObjectId) -> Vec<&ObjectId> {
66        let target = self.resolve_rename(id);
67        self.views
68            .iter()
69            .filter(|v| {
70                v.depends_on
71                    .iter()
72                    .any(|dep| self.resolve_rename(dep) == target || dep == id)
73            })
74            .map(|v| self.resolve_rename(&v.view_id))
75            .collect()
76    }
77
78    pub fn is_referenced_by_fk(&self, id: &ObjectId) -> Vec<(&ObjectId, u64)> {
79        let target = self.resolve_rename(id);
80        self.foreign_keys
81            .iter()
82            .filter(|fk| self.resolve_rename(&fk.to_table) == target || &fk.to_table == id)
83            .map(|fk| (self.resolve_rename(&fk.from_table), fk.from_generation))
84            .collect()
85    }
86
87    pub fn is_referenced_by_index(&self, id: &ObjectId) -> Vec<&ObjectId> {
88        let target = self.resolve_rename(id);
89        self.indexes
90            .iter()
91            .filter(|ix| self.resolve_rename(&ix.relation_id) == target || &ix.relation_id == id)
92            .map(|ix| self.resolve_rename(&ix.index_id))
93            .collect()
94    }
95
96    pub fn partitions_of(&self, id: &ObjectId) -> Vec<&ObjectId> {
97        let target = self.resolve_rename(id);
98        self.partitions
99            .iter()
100            .filter(|p| self.resolve_rename(&p.parent) == target || &p.parent == id)
101            .map(|p| self.resolve_rename(&p.child))
102            .collect()
103    }
104
105    pub fn resolve_rename<'a>(&'a self, id: &'a ObjectId) -> &'a ObjectId {
106        let mut current = id;
107        loop {
108            match self.renames.iter().find(|r| &r.from == current) {
109                Some(edge) => current = &edge.to,
110                None => return current,
111            }
112        }
113    }
114
115    // Phase 3 FIX (BUG-012): Reject cycle topologies
116    pub fn check_partition_cycle(&self, parent: &ObjectId, child: &ObjectId) -> bool {
117        let resolved_parent = self.resolve_rename(parent);
118        let resolved_child = self.resolve_rename(child);
119        if resolved_parent == resolved_child {
120            return true;
121        }
122
123        let mut current_parent = resolved_parent;
124        loop {
125            let maybe_edge = self
126                .partitions
127                .iter()
128                .find(|p| self.resolve_rename(&p.child) == current_parent);
129            if let Some(edge) = maybe_edge {
130                let p = self.resolve_rename(&edge.parent);
131                if p == resolved_child {
132                    return true;
133                }
134                current_parent = p;
135            } else {
136                break;
137            }
138        }
139        false
140    }
141}