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 enum DependencyKind {
6    ForeignKey {
7        constraint_name: Option<String>,
8        from_columns: Vec<String>,
9        to_columns: Vec<String>,
10        from_generation: u64,
11    },
12    ViewDependency {
13        view_generation: u64,
14    },
15    IndexOnRelation {
16        using_method: Option<String>,
17        has_predicate: bool,
18        is_concurrent: bool,
19        is_unique: bool,
20    },
21    RenameTo,
22    PartitionOf,
23    SequenceOwnedBy {
24        column: String,
25    },
26    ColumnGeneratedFrom {
27        column: String,
28        depends_on_column: String,
29    },
30    TriggerOnTable {
31        trigger_id: ObjectId,
32        function_id: ObjectId,
33    },
34    PublicationIncludes {
35        publication_name: String,
36    },
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct DependencyEdge {
41    pub dependent: ObjectId,
42    pub referenced: ObjectId,
43    pub kind: DependencyKind,
44}
45
46impl DependencyEdge {
47    pub fn new(dependent: ObjectId, referenced: ObjectId, kind: DependencyKind) -> Self {
48        Self {
49            dependent,
50            referenced,
51            kind,
52        }
53    }
54}
55
56#[derive(Debug, Clone, Default)]
57pub struct DependencyGraph {
58    pub edges: Vec<DependencyEdge>,
59}
60
61impl DependencyGraph {
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    // Phase 3 FIX (BUG-004): Traverse rename chains dynamically for accurate topology reads
67    pub fn is_referenced_by_view(&self, id: &ObjectId) -> Vec<&ObjectId> {
68        let target = self.resolve_rename(id);
69        self.edges
70            .iter()
71            .filter(|e| {
72                matches!(e.kind, DependencyKind::ViewDependency { .. })
73                    && (self.resolve_rename(&e.referenced) == target || &e.referenced == id)
74            })
75            .map(|e| self.resolve_rename(&e.dependent))
76            .collect()
77    }
78
79    pub fn is_referenced_by_fk(&self, id: &ObjectId) -> Vec<(&ObjectId, u64)> {
80        let target = self.resolve_rename(id);
81        self.edges
82            .iter()
83            .filter_map(|e| {
84                if let DependencyKind::ForeignKey {
85                    from_generation, ..
86                } = &e.kind
87                    && (self.resolve_rename(&e.referenced) == target || &e.referenced == id)
88                {
89                    Some((self.resolve_rename(&e.dependent), *from_generation))
90                } else {
91                    None
92                }
93            })
94            .collect()
95    }
96
97    pub fn is_referenced_by_index(&self, id: &ObjectId) -> Vec<&ObjectId> {
98        let target = self.resolve_rename(id);
99        self.edges
100            .iter()
101            .filter(|e| {
102                matches!(e.kind, DependencyKind::IndexOnRelation { .. })
103                    && (self.resolve_rename(&e.referenced) == target || &e.referenced == id)
104            })
105            .map(|e| self.resolve_rename(&e.dependent))
106            .collect()
107    }
108
109    pub fn partitions_of(&self, id: &ObjectId) -> Vec<&ObjectId> {
110        let target = self.resolve_rename(id);
111        self.edges
112            .iter()
113            .filter(|e| {
114                matches!(e.kind, DependencyKind::PartitionOf)
115                    && (self.resolve_rename(&e.referenced) == target || &e.referenced == id)
116            })
117            .map(|e| self.resolve_rename(&e.dependent))
118            .collect()
119    }
120
121    pub fn resolve_rename<'a>(&'a self, id: &'a ObjectId) -> &'a ObjectId {
122        let mut current = id;
123        loop {
124            match self
125                .edges
126                .iter()
127                .find(|e| matches!(e.kind, DependencyKind::RenameTo) && &e.dependent == current)
128            {
129                Some(edge) => current = &edge.referenced,
130                None => return current,
131            }
132        }
133    }
134
135    // Phase 3 FIX (BUG-012): Reject cycle topologies
136    pub fn check_partition_cycle(&self, parent: &ObjectId, child: &ObjectId) -> bool {
137        let resolved_parent = self.resolve_rename(parent);
138        let resolved_child = self.resolve_rename(child);
139        if resolved_parent == resolved_child {
140            return true;
141        }
142
143        let mut current_parent = resolved_parent;
144        loop {
145            let maybe_edge = self.edges.iter().find(|e| {
146                matches!(e.kind, DependencyKind::PartitionOf)
147                    && self.resolve_rename(&e.dependent) == current_parent
148            });
149            if let Some(edge) = maybe_edge {
150                let p = self.resolve_rename(&edge.referenced);
151                if p == resolved_child {
152                    return true;
153                }
154                current_parent = p;
155            } else {
156                break;
157            }
158        }
159        false
160    }
161
162    pub fn propagate_rename(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
163        for edge in &mut self.edges {
164            if matches!(edge.kind, DependencyKind::RenameTo) {
165                continue;
166            }
167            if edge.dependent == *old_id {
168                edge.dependent = new_id.clone();
169            }
170            if edge.referenced == *old_id {
171                edge.referenced = new_id.clone();
172            }
173            if let DependencyKind::TriggerOnTable {
174                trigger_id,
175                function_id,
176            } = &mut edge.kind
177            {
178                if *trigger_id == *old_id {
179                    *trigger_id = new_id.clone();
180                }
181                if *function_id == *old_id {
182                    *function_id = new_id.clone();
183                }
184            }
185        }
186    }
187
188    pub fn triggers_on(&self, table_id: &ObjectId) -> Vec<&DependencyEdge> {
189        self.edges
190            .iter()
191            .filter(|e| {
192                matches!(e.kind, DependencyKind::TriggerOnTable { .. }) && &e.referenced == table_id
193            })
194            .collect()
195    }
196
197    pub fn triggers_for_function(&self, function_id: &ObjectId) -> Vec<&DependencyEdge> {
198        let normalize = |id: &ObjectId| -> ObjectId {
199            let name = if let Some(idx) = id.name.find('(') {
200                format!("{}()", &id.name[..idx])
201            } else {
202                id.name.clone()
203            };
204            ObjectId {
205                schema: id.schema.clone(),
206                name,
207                inferred_schema: id.inferred_schema,
208            }
209        };
210        let target_id = normalize(function_id);
211        self.edges
212            .iter()
213            .filter(|e| {
214                if let DependencyKind::TriggerOnTable {
215                    function_id: fid, ..
216                } = &e.kind
217                {
218                    normalize(fid) == target_id
219                } else {
220                    false
221                }
222            })
223            .collect()
224    }
225}