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