Skip to main content

safe_migrate/analysis/
graph.rs

1use crate::ast::identifiers::ObjectId;
2use std::collections::HashSet;
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        eligibility_known: bool,
21    },
22    RenameTo,
23    PartitionOf,
24    SequenceOwnedBy {
25        column: String,
26    },
27    ColumnGeneratedFrom {
28        column: String,
29        depends_on_column: String,
30    },
31    TriggerOnTable {
32        trigger_id: ObjectId,
33        function_id: ObjectId,
34    },
35    PublicationIncludes {
36        publication_name: String,
37    },
38}
39
40#[derive(Debug, Clone, PartialEq)]
41pub struct DependencyEdge {
42    pub dependent: ObjectId,
43    pub referenced: ObjectId,
44    pub kind: DependencyKind,
45}
46
47impl DependencyEdge {
48    pub fn new(dependent: ObjectId, referenced: ObjectId, kind: DependencyKind) -> Self {
49        Self {
50            dependent,
51            referenced,
52            kind,
53        }
54    }
55}
56
57#[derive(Debug, Clone, Default)]
58pub struct DependencyGraph {
59    pub edges: Vec<DependencyEdge>,
60}
61
62impl DependencyGraph {
63    pub fn new() -> Self {
64        Self::default()
65    }
66
67    // Dependency lookups follow the current end of a rename chain.
68    pub fn is_referenced_by_view(&self, id: &ObjectId) -> Vec<&ObjectId> {
69        let target = self.resolve_rename(id);
70        self.edges
71            .iter()
72            .filter(|e| {
73                matches!(e.kind, DependencyKind::ViewDependency { .. })
74                    && (self.resolve_rename(&e.referenced) == target || &e.referenced == id)
75            })
76            .map(|e| self.resolve_rename(&e.dependent))
77            .collect()
78    }
79
80    pub fn is_referenced_by_fk(&self, id: &ObjectId) -> Vec<(&ObjectId, u64)> {
81        let target = self.resolve_rename(id);
82        self.edges
83            .iter()
84            .filter_map(|e| {
85                if let DependencyKind::ForeignKey {
86                    from_generation, ..
87                } = &e.kind
88                    && (self.resolve_rename(&e.referenced) == target || &e.referenced == id)
89                {
90                    Some((self.resolve_rename(&e.dependent), *from_generation))
91                } else {
92                    None
93                }
94            })
95            .collect()
96    }
97
98    pub fn is_referenced_by_index(&self, id: &ObjectId) -> Vec<&ObjectId> {
99        let target = self.resolve_rename(id);
100        self.edges
101            .iter()
102            .filter(|e| {
103                matches!(e.kind, DependencyKind::IndexOnRelation { .. })
104                    && (self.resolve_rename(&e.referenced) == target || &e.referenced == id)
105            })
106            .map(|e| self.resolve_rename(&e.dependent))
107            .collect()
108    }
109
110    pub fn partitions_of(&self, id: &ObjectId) -> Vec<&ObjectId> {
111        let target = self.resolve_rename(id);
112        self.edges
113            .iter()
114            .filter(|e| {
115                matches!(e.kind, DependencyKind::PartitionOf)
116                    && (self.resolve_rename(&e.referenced) == target || &e.referenced == id)
117            })
118            .map(|e| self.resolve_rename(&e.dependent))
119            .collect()
120    }
121
122    pub fn resolve_rename<'a>(&'a self, id: &'a ObjectId) -> &'a ObjectId {
123        let mut current = id;
124        let mut visited = HashSet::new();
125        loop {
126            // A rename back to an earlier name is valid PostgreSQL. The graph
127            // retains historical aliases, so resolve only acyclic paths; a
128            // cycle has no unique alias target and must leave the supplied
129            // identity unchanged.
130            if !visited.insert(current.clone()) {
131                return id;
132            }
133            match self
134                .edges
135                .iter()
136                .find(|e| matches!(e.kind, DependencyKind::RenameTo) && &e.dependent == current)
137            {
138                Some(edge) => current = &edge.referenced,
139                None => return current,
140            }
141        }
142    }
143
144    // Partition ancestry must remain acyclic.
145    pub fn check_partition_cycle(&self, parent: &ObjectId, child: &ObjectId) -> bool {
146        let resolved_parent = self.resolve_rename(parent);
147        let resolved_child = self.resolve_rename(child);
148        if resolved_parent == resolved_child {
149            return true;
150        }
151
152        let mut current_parent = resolved_parent;
153        let mut visited = HashSet::new();
154        loop {
155            if !visited.insert(current_parent.clone()) {
156                // The existing ancestry is already malformed. Reject another
157                // attachment instead of looping or extending the cycle.
158                return true;
159            }
160            let maybe_edge = self.edges.iter().find(|e| {
161                matches!(e.kind, DependencyKind::PartitionOf)
162                    && self.resolve_rename(&e.dependent) == current_parent
163            });
164            if let Some(edge) = maybe_edge {
165                let p = self.resolve_rename(&edge.referenced);
166                if p == resolved_child {
167                    return true;
168                }
169                current_parent = p;
170            } else {
171                break;
172            }
173        }
174        false
175    }
176
177    pub fn propagate_rename(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
178        for edge in &mut self.edges {
179            if matches!(edge.kind, DependencyKind::RenameTo) {
180                continue;
181            }
182            if edge.dependent == *old_id {
183                edge.dependent = new_id.clone();
184            }
185            if edge.referenced == *old_id {
186                edge.referenced = new_id.clone();
187            }
188            if let DependencyKind::TriggerOnTable {
189                trigger_id,
190                function_id,
191            } = &mut edge.kind
192            {
193                if *trigger_id == *old_id {
194                    *trigger_id = new_id.clone();
195                }
196                if *function_id == *old_id {
197                    *function_id = new_id.clone();
198                }
199            }
200        }
201    }
202
203    pub fn triggers_on(&self, table_id: &ObjectId) -> Vec<&DependencyEdge> {
204        self.edges
205            .iter()
206            .filter(|e| {
207                matches!(e.kind, DependencyKind::TriggerOnTable { .. }) && &e.referenced == table_id
208            })
209            .collect()
210    }
211
212    pub fn triggers_for_function(&self, function_id: &ObjectId) -> Vec<&DependencyEdge> {
213        let normalize = |id: &ObjectId| -> ObjectId {
214            let name = if let Some(idx) = id.name.find('(') {
215                format!("{}()", &id.name[..idx])
216            } else {
217                id.name.clone()
218            };
219            ObjectId {
220                schema: id.schema.clone(),
221                name,
222                inferred_schema: id.inferred_schema,
223            }
224        };
225        let target_id = normalize(function_id);
226        self.edges
227            .iter()
228            .filter(|e| {
229                if let DependencyKind::TriggerOnTable {
230                    function_id: fid, ..
231                } = &e.kind
232                {
233                    normalize(fid) == target_id
234                } else {
235                    false
236                }
237            })
238            .collect()
239    }
240}