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 ColumnDependencyEdge {
23    pub table_id: ObjectId,
24    pub column: String,
25    pub depends_on_table: ObjectId,
26    pub depends_on_column: String,
27}
28
29#[derive(Debug, Clone, PartialEq)]
30pub struct IndexEdge {
31    pub index_id: ObjectId,
32    pub relation_id: ObjectId,
33    pub using_method: Option<String>,
34    pub has_predicate: bool,
35    pub is_concurrent: bool,
36    pub is_unique: bool,
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct RenameEdge {
41    pub from: ObjectId,
42    pub to: ObjectId,
43}
44
45#[derive(Debug, Clone, PartialEq)]
46pub struct PartitionEdge {
47    pub parent: ObjectId,
48    pub child: ObjectId,
49}
50
51#[derive(Debug, Clone, PartialEq)]
52pub struct SequenceEdge {
53    pub sequence_id: ObjectId,
54    pub table_id: ObjectId,
55    pub column: String,
56}
57
58#[derive(Debug, Clone, PartialEq)]
59pub struct TriggerEdge {
60    pub trigger_id: ObjectId,
61    pub table_id: ObjectId,
62    pub function_id: ObjectId,
63}
64
65#[derive(Debug, Clone, PartialEq)]
66pub struct PublicationEdge {
67    pub publication_name: String,
68    pub table_id: ObjectId,
69}
70
71#[derive(Debug, Clone, Default)]
72pub struct DependencyGraph {
73    pub foreign_keys: Vec<FkEdge>,
74    pub views: Vec<ViewEdge>,
75    pub indexes: Vec<IndexEdge>,
76    pub renames: Vec<RenameEdge>,
77    pub partitions: Vec<PartitionEdge>,
78    pub sequences: Vec<SequenceEdge>,
79    pub column_dependencies: Vec<ColumnDependencyEdge>,
80    pub trigger_dependencies: Vec<TriggerEdge>,
81    pub publication_dependencies: Vec<PublicationEdge>,
82}
83
84impl DependencyGraph {
85    pub fn new() -> Self {
86        Self::default()
87    }
88
89    // Phase 3 FIX (BUG-004): Traverse rename chains dynamically for accurate topology reads
90    pub fn is_referenced_by_view(&self, id: &ObjectId) -> Vec<&ObjectId> {
91        let target = self.resolve_rename(id);
92        self.views
93            .iter()
94            .filter(|v| {
95                v.depends_on
96                    .iter()
97                    .any(|dep| self.resolve_rename(dep) == target || dep == id)
98            })
99            .map(|v| self.resolve_rename(&v.view_id))
100            .collect()
101    }
102
103    pub fn is_referenced_by_fk(&self, id: &ObjectId) -> Vec<(&ObjectId, u64)> {
104        let target = self.resolve_rename(id);
105        self.foreign_keys
106            .iter()
107            .filter(|fk| self.resolve_rename(&fk.to_table) == target || &fk.to_table == id)
108            .map(|fk| (self.resolve_rename(&fk.from_table), fk.from_generation))
109            .collect()
110    }
111
112    pub fn is_referenced_by_index(&self, id: &ObjectId) -> Vec<&ObjectId> {
113        let target = self.resolve_rename(id);
114        self.indexes
115            .iter()
116            .filter(|ix| self.resolve_rename(&ix.relation_id) == target || &ix.relation_id == id)
117            .map(|ix| self.resolve_rename(&ix.index_id))
118            .collect()
119    }
120
121    pub fn partitions_of(&self, id: &ObjectId) -> Vec<&ObjectId> {
122        let target = self.resolve_rename(id);
123        self.partitions
124            .iter()
125            .filter(|p| self.resolve_rename(&p.parent) == target || &p.parent == id)
126            .map(|p| self.resolve_rename(&p.child))
127            .collect()
128    }
129
130    pub fn resolve_rename<'a>(&'a self, id: &'a ObjectId) -> &'a ObjectId {
131        let mut current = id;
132        loop {
133            match self.renames.iter().find(|r| &r.from == current) {
134                Some(edge) => current = &edge.to,
135                None => return current,
136            }
137        }
138    }
139
140    // Phase 3 FIX (BUG-012): Reject cycle topologies
141    pub fn check_partition_cycle(&self, parent: &ObjectId, child: &ObjectId) -> bool {
142        let resolved_parent = self.resolve_rename(parent);
143        let resolved_child = self.resolve_rename(child);
144        if resolved_parent == resolved_child {
145            return true;
146        }
147
148        let mut current_parent = resolved_parent;
149        loop {
150            let maybe_edge = self
151                .partitions
152                .iter()
153                .find(|p| self.resolve_rename(&p.child) == current_parent);
154            if let Some(edge) = maybe_edge {
155                let p = self.resolve_rename(&edge.parent);
156                if p == resolved_child {
157                    return true;
158                }
159                current_parent = p;
160            } else {
161                break;
162            }
163        }
164        false
165    }
166
167    pub fn propagate_rename(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
168        for idx in &mut self.indexes {
169            if idx.index_id == *old_id {
170                idx.index_id = new_id.clone();
171            }
172            if idx.relation_id == *old_id {
173                idx.relation_id = new_id.clone();
174            }
175        }
176        for view in &mut self.views {
177            if view.view_id == *old_id {
178                view.view_id = new_id.clone();
179            }
180            view.depends_on.iter_mut().for_each(|dep| {
181                if *dep == *old_id {
182                    *dep = new_id.clone();
183                }
184            });
185        }
186        for fk in &mut self.foreign_keys {
187            if fk.from_table == *old_id {
188                fk.from_table = new_id.clone();
189            }
190            if fk.to_table == *old_id {
191                fk.to_table = new_id.clone();
192            }
193        }
194        for part in &mut self.partitions {
195            if part.parent == *old_id {
196                part.parent = new_id.clone();
197            }
198            if part.child == *old_id {
199                part.child = new_id.clone();
200            }
201        }
202        for seq in &mut self.sequences {
203            if seq.sequence_id == *old_id {
204                seq.sequence_id = new_id.clone();
205            }
206            if seq.table_id == *old_id {
207                seq.table_id = new_id.clone();
208            }
209        }
210        for col_dep in &mut self.column_dependencies {
211            if col_dep.table_id == *old_id {
212                col_dep.table_id = new_id.clone();
213            }
214            if col_dep.depends_on_table == *old_id {
215                col_dep.depends_on_table = new_id.clone();
216            }
217        }
218        for trg in &mut self.trigger_dependencies {
219            if trg.table_id == *old_id {
220                trg.table_id = new_id.clone();
221            }
222            if trg.trigger_id == *old_id {
223                trg.trigger_id = new_id.clone();
224            }
225            if trg.function_id == *old_id {
226                trg.function_id = new_id.clone();
227            }
228        }
229        for publ in &mut self.publication_dependencies {
230            if publ.table_id == *old_id {
231                publ.table_id = new_id.clone();
232            }
233        }
234    }
235
236    pub fn triggers_on(&self, table_id: &ObjectId) -> Vec<&TriggerEdge> {
237        self.trigger_dependencies
238            .iter()
239            .filter(|t| &t.table_id == table_id)
240            .collect()
241    }
242
243    pub fn triggers_for_function(&self, function_id: &ObjectId) -> Vec<&TriggerEdge> {
244        let normalize = |id: &ObjectId| -> ObjectId {
245            let name = if let Some(idx) = id.name.find('(') {
246                format!("{}()", &id.name[..idx])
247            } else {
248                id.name.clone()
249            };
250            ObjectId {
251                schema: id.schema.clone(),
252                name,
253                inferred_schema: id.inferred_schema,
254            }
255        };
256        let target_id = normalize(function_id);
257        self.trigger_dependencies
258            .iter()
259            .filter(|t| normalize(&t.function_id) == target_id)
260            .collect()
261    }
262}