safe_migrate/analysis/
graph.rs1use 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 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 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 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 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}