Skip to main content

safe_migrate/analysis/
graph.rs

1use crate::ast::identifiers::ObjectId;
2use std::cell::OnceCell;
3use std::collections::{HashMap, 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    /// A local primary-key/unique constraint whose key columns are known.
24    /// This edge is intentionally state-only (not a cache row): V6 caches
25    /// retain constraint identity but historically did not retain key
26    /// columns, so baseline callers must remain conservative.
27    ConstraintOnRelation {
28        constraint_name: String,
29        columns: Vec<String>,
30        is_primary: bool,
31    },
32    RenameTo,
33    PartitionOf,
34    SequenceOwnedBy {
35        column: String,
36    },
37    ColumnGeneratedFrom {
38        column: String,
39        depends_on_column: String,
40    },
41    TriggerOnTable {
42        trigger_id: ObjectId,
43        function_id: ObjectId,
44    },
45    PublicationIncludes {
46        publication_name: String,
47    },
48}
49
50#[derive(Debug, Clone, PartialEq)]
51pub struct DependencyEdge {
52    pub dependent: ObjectId,
53    pub referenced: ObjectId,
54    pub kind: DependencyKind,
55}
56
57impl DependencyEdge {
58    pub fn new(dependent: ObjectId, referenced: ObjectId, kind: DependencyKind) -> Self {
59        Self {
60            dependent,
61            referenced,
62            kind,
63        }
64    }
65}
66
67#[derive(Debug, Default)]
68pub struct DependencyGraph {
69    edges: Vec<DependencyEdge>,
70    indexes: OnceCell<GraphIndexes>,
71}
72
73#[derive(Debug, Clone, Default, PartialEq, Eq)]
74struct GraphIndexes {
75    rename_by_source: HashMap<ObjectId, usize>,
76    by_resolved_referenced: HashMap<ObjectId, Vec<usize>>,
77}
78
79impl Clone for DependencyGraph {
80    fn clone(&self) -> Self {
81        Self {
82            edges: self.edges.clone(),
83            // Indexes are derived state. Avoid duplicating them in statement
84            // checkpoints; the clone builds them only if a lookup needs them.
85            indexes: OnceCell::new(),
86        }
87    }
88}
89
90impl DependencyGraph {
91    const CASCADE_INDEX_MIN_EDGES: usize = 1_024;
92
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    pub fn edges(&self) -> &[DependencyEdge] {
98        &self.edges
99    }
100
101    pub fn add_edge(&mut self, edge: DependencyEdge) {
102        self.edges.push(edge);
103        self.invalidate_indexes();
104    }
105
106    pub(crate) fn retain_edges(&mut self, mut keep: impl FnMut(&DependencyEdge) -> bool) {
107        self.edges.retain(|edge| keep(edge));
108        self.invalidate_indexes();
109    }
110
111    pub(crate) fn edge_count(&self) -> usize {
112        self.edges.len()
113    }
114
115    pub(crate) fn truncate(&mut self, len: usize) {
116        self.edges.truncate(len);
117        self.invalidate_indexes();
118    }
119
120    pub(crate) fn replace_edges(&mut self, edges: Vec<DependencyEdge>) {
121        self.edges = edges;
122        self.invalidate_indexes();
123    }
124
125    pub(crate) fn mutate_edges(&mut self, mutate: impl FnOnce(&mut [DependencyEdge])) {
126        mutate(&mut self.edges);
127        self.invalidate_indexes();
128    }
129
130    /// Confirms that every derived lookup points at the canonical edge list.
131    /// This is intentionally cheap to call from invariant tests, not hot paths.
132    pub fn indexes_are_valid(&self) -> bool {
133        self.indexes() == &Self::build_indexes(&self.edges)
134    }
135
136    fn invalidate_indexes(&mut self) {
137        self.indexes.take();
138    }
139
140    fn indexes(&self) -> &GraphIndexes {
141        self.indexes
142            .get_or_init(|| Self::build_indexes(&self.edges))
143    }
144
145    fn build_indexes(edges: &[DependencyEdge]) -> GraphIndexes {
146        let mut indexes = GraphIndexes::default();
147        for (index, edge) in edges.iter().enumerate() {
148            if matches!(edge.kind, DependencyKind::RenameTo) {
149                indexes
150                    .rename_by_source
151                    .entry(edge.dependent.clone())
152                    .or_insert(index);
153            }
154        }
155
156        for (index, edge) in edges.iter().enumerate() {
157            let referenced = Self::resolve_rename_with(edges, &indexes, &edge.referenced).clone();
158            indexes
159                .by_resolved_referenced
160                .entry(referenced)
161                .or_default()
162                .push(index);
163        }
164        indexes
165    }
166
167    fn resolve_rename_with<'a>(
168        edges: &'a [DependencyEdge],
169        indexes: &GraphIndexes,
170        id: &'a ObjectId,
171    ) -> &'a ObjectId {
172        let mut current = id;
173        let mut visited = HashSet::new();
174        loop {
175            if !visited.insert(current.clone()) {
176                return id;
177            }
178            match indexes.rename_by_source.get(current) {
179                Some(index) => current = &edges[*index].referenced,
180                None => return current,
181            }
182        }
183    }
184
185    fn resolved_referenced_edges(&self, id: &ObjectId) -> impl Iterator<Item = &DependencyEdge> {
186        let target = self.resolve_rename(id);
187        self.indexes()
188            .by_resolved_referenced
189            .get(target)
190            .into_iter()
191            .flatten()
192            .map(|index| &self.edges[*index])
193    }
194
195    pub fn cascade_edges(&self, id: &ObjectId) -> Vec<&DependencyEdge> {
196        if self.edges.len() < Self::CASCADE_INDEX_MIN_EDGES {
197            let target = self.resolve_rename(id);
198            return self
199                .edges
200                .iter()
201                .filter(|edge| {
202                    matches!(
203                        edge.kind,
204                        DependencyKind::ViewDependency { .. }
205                            | DependencyKind::IndexOnRelation { .. }
206                            | DependencyKind::ForeignKey { .. }
207                            | DependencyKind::PartitionOf
208                    ) && self.resolve_rename(&edge.referenced) == target
209                })
210                .collect();
211        }
212        self.resolved_referenced_edges(id)
213            .filter(|edge| {
214                matches!(
215                    edge.kind,
216                    DependencyKind::ViewDependency { .. }
217                        | DependencyKind::IndexOnRelation { .. }
218                        | DependencyKind::ForeignKey { .. }
219                        | DependencyKind::PartitionOf
220                )
221            })
222            .collect()
223    }
224
225    pub(crate) fn cascade_index_is_worthwhile(&self) -> bool {
226        self.edges.len() >= Self::CASCADE_INDEX_MIN_EDGES
227    }
228
229    // Dependency lookups follow the current end of a rename chain.
230    pub fn is_referenced_by_view(&self, id: &ObjectId) -> Vec<&ObjectId> {
231        let target = self.resolve_rename(id);
232        self.edges
233            .iter()
234            .filter(|e| {
235                matches!(e.kind, DependencyKind::ViewDependency { .. })
236                    && (self.resolve_rename(&e.referenced) == target || &e.referenced == id)
237            })
238            .map(|e| self.resolve_rename(&e.dependent))
239            .collect()
240    }
241
242    pub fn is_referenced_by_fk(&self, id: &ObjectId) -> Vec<(&ObjectId, u64)> {
243        let target = self.resolve_rename(id);
244        self.edges
245            .iter()
246            .filter_map(|e| {
247                if let DependencyKind::ForeignKey {
248                    from_generation, ..
249                } = &e.kind
250                    && (self.resolve_rename(&e.referenced) == target || &e.referenced == id)
251                {
252                    Some((self.resolve_rename(&e.dependent), *from_generation))
253                } else {
254                    None
255                }
256            })
257            .collect()
258    }
259
260    pub fn is_referenced_by_index(&self, id: &ObjectId) -> Vec<&ObjectId> {
261        let target = self.resolve_rename(id);
262        self.edges
263            .iter()
264            .filter(|e| {
265                matches!(e.kind, DependencyKind::IndexOnRelation { .. })
266                    && (self.resolve_rename(&e.referenced) == target || &e.referenced == id)
267            })
268            .map(|e| self.resolve_rename(&e.dependent))
269            .collect()
270    }
271
272    pub fn partitions_of(&self, id: &ObjectId) -> Vec<&ObjectId> {
273        let target = self.resolve_rename(id);
274        self.edges
275            .iter()
276            .filter(|e| {
277                matches!(e.kind, DependencyKind::PartitionOf)
278                    && (self.resolve_rename(&e.referenced) == target || &e.referenced == id)
279            })
280            .map(|e| self.resolve_rename(&e.dependent))
281            .collect()
282    }
283
284    pub fn resolve_rename<'a>(&'a self, id: &'a ObjectId) -> &'a ObjectId {
285        let mut current = id;
286        let mut visited = HashSet::new();
287        loop {
288            // A rename back to an earlier name is valid PostgreSQL. The graph
289            // retains historical aliases, so resolve only acyclic paths; a
290            // cycle has no unique alias target and must leave the supplied
291            // identity unchanged.
292            if !visited.insert(current.clone()) {
293                return id;
294            }
295            match self.edges.iter().find(|edge| {
296                matches!(edge.kind, DependencyKind::RenameTo) && &edge.dependent == current
297            }) {
298                Some(edge) => current = &edge.referenced,
299                None => return current,
300            }
301        }
302    }
303
304    // Partition ancestry must remain acyclic.
305    pub fn check_partition_cycle(&self, parent: &ObjectId, child: &ObjectId) -> bool {
306        let resolved_parent = self.resolve_rename(parent);
307        let resolved_child = self.resolve_rename(child);
308        if resolved_parent == resolved_child {
309            return true;
310        }
311
312        let mut current_parent = resolved_parent;
313        let mut visited = HashSet::new();
314        loop {
315            if !visited.insert(current_parent.clone()) {
316                // The existing ancestry is already malformed. Reject another
317                // attachment instead of looping or extending the cycle.
318                return true;
319            }
320            let maybe_edge = self.edges.iter().find(|edge| {
321                matches!(edge.kind, DependencyKind::PartitionOf)
322                    && self.resolve_rename(&edge.dependent) == current_parent
323            });
324            if let Some(edge) = maybe_edge {
325                let p = self.resolve_rename(&edge.referenced);
326                if p == resolved_child {
327                    return true;
328                }
329                current_parent = p;
330            } else {
331                break;
332            }
333        }
334        false
335    }
336
337    /// Propagate a relation rename through relation-to-relation edges.
338    ///
339    /// Dependency endpoints are intentionally updated by edge kind rather than
340    /// by blindly comparing `ObjectId`s.  `ObjectId` carries no catalog kind,
341    /// and publications are represented by a synthetic `public/<name>` ID, so
342    /// a generic endpoint rewrite can otherwise corrupt an unrelated edge when
343    /// two namespaces happen to share a name.
344    pub fn propagate_relation_rename(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
345        for edge in &mut self.edges {
346            match &mut edge.kind {
347                DependencyKind::RenameTo => {}
348                DependencyKind::ForeignKey { .. }
349                | DependencyKind::ViewDependency { .. }
350                | DependencyKind::PartitionOf
351                | DependencyKind::ColumnGeneratedFrom { .. } => {
352                    if edge.dependent == *old_id {
353                        edge.dependent = new_id.clone();
354                    }
355                    if edge.referenced == *old_id {
356                        edge.referenced = new_id.clone();
357                    }
358                }
359                DependencyKind::IndexOnRelation { .. }
360                | DependencyKind::SequenceOwnedBy { .. }
361                | DependencyKind::TriggerOnTable { .. } => {
362                    if edge.referenced == *old_id {
363                        edge.referenced = new_id.clone();
364                    }
365                }
366                DependencyKind::ConstraintOnRelation { .. } => {
367                    if edge.dependent == *old_id {
368                        edge.dependent = new_id.clone();
369                    }
370                    if edge.referenced == *old_id {
371                        edge.referenced = new_id.clone();
372                    }
373                }
374                DependencyKind::PublicationIncludes { .. } => {
375                    if edge.dependent == *old_id {
376                        edge.dependent = new_id.clone();
377                    }
378                }
379            }
380        }
381        self.invalidate_indexes();
382    }
383
384    /// Propagate an index rename.  Indexes are dependent endpoints of their
385    /// `IndexOnRelation` edges; they are not relation references.
386    pub fn propagate_index_rename(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
387        for edge in &mut self.edges {
388            if matches!(edge.kind, DependencyKind::IndexOnRelation { .. })
389                && edge.dependent == *old_id
390            {
391                edge.dependent = new_id.clone();
392            }
393        }
394        self.invalidate_indexes();
395    }
396
397    /// Propagate a sequence rename only through sequence ownership edges.
398    pub fn propagate_sequence_rename(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
399        for edge in &mut self.edges {
400            if matches!(edge.kind, DependencyKind::SequenceOwnedBy { .. })
401                && edge.dependent == *old_id
402            {
403                edge.dependent = new_id.clone();
404            }
405        }
406        self.invalidate_indexes();
407    }
408
409    /// Propagate a trigger rename through its trigger edge and payload.
410    pub fn propagate_trigger_rename(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
411        for edge in &mut self.edges {
412            if let DependencyKind::TriggerOnTable { trigger_id, .. } = &mut edge.kind
413                && *trigger_id == *old_id
414            {
415                *trigger_id = new_id.clone();
416                if edge.dependent == *old_id {
417                    edge.dependent = new_id.clone();
418                }
419            }
420        }
421        self.invalidate_indexes();
422    }
423
424    /// Propagate a function rename through trigger dependency payloads.
425    pub fn propagate_function_rename(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
426        for edge in &mut self.edges {
427            if let DependencyKind::TriggerOnTable { function_id, .. } = &mut edge.kind
428                && *function_id == *old_id
429            {
430                *function_id = new_id.clone();
431            }
432        }
433        self.invalidate_indexes();
434    }
435
436    /// Backwards-compatible relation rename entry point.
437    ///
438    /// New callers should use the typed helpers above.  Keeping this method
439    /// relation-scoped prevents the old all-endpoints behavior from silently
440    /// rewriting sequence, trigger, function, or publication identity data.
441    pub fn propagate_rename(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
442        self.propagate_relation_rename(old_id, new_id);
443    }
444
445    pub fn triggers_on(&self, table_id: &ObjectId) -> Vec<&DependencyEdge> {
446        self.edges
447            .iter()
448            .filter(|e| {
449                matches!(e.kind, DependencyKind::TriggerOnTable { .. }) && &e.referenced == table_id
450            })
451            .collect()
452    }
453
454    pub fn triggers_for_function(&self, function_id: &ObjectId) -> Vec<&DependencyEdge> {
455        let normalize = |id: &ObjectId| -> ObjectId {
456            let name = if let Some(idx) = id.name.find('(') {
457                format!("{}()", &id.name[..idx])
458            } else {
459                id.name.clone()
460            };
461            ObjectId {
462                schema: id.schema.clone(),
463                name,
464                inferred_schema: id.inferred_schema,
465            }
466        };
467        let target_id = normalize(function_id);
468        self.edges
469            .iter()
470            .filter(|e| {
471                if let DependencyKind::TriggerOnTable {
472                    function_id: fid, ..
473                } = &e.kind
474                {
475                    normalize(fid) == target_id
476                } else {
477                    false
478                }
479            })
480            .collect()
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487
488    fn id(name: &str) -> ObjectId {
489        ObjectId::new("public", name)
490    }
491
492    fn view_edge(dependent: &str, referenced: &str) -> DependencyEdge {
493        DependencyEdge::new(
494            id(dependent),
495            id(referenced),
496            DependencyKind::ViewDependency { view_generation: 1 },
497        )
498    }
499
500    fn canonical_views<'a>(graph: &'a DependencyGraph, target: &ObjectId) -> Vec<&'a ObjectId> {
501        let resolved_target = graph.resolve_rename(target);
502        graph
503            .edges()
504            .iter()
505            .filter(|edge| {
506                matches!(edge.kind, DependencyKind::ViewDependency { .. })
507                    && (graph.resolve_rename(&edge.referenced) == resolved_target
508                        || &edge.referenced == target)
509            })
510            .map(|edge| graph.resolve_rename(&edge.dependent))
511            .collect()
512    }
513
514    fn assert_indexed_views_match_scan(graph: &DependencyGraph, targets: &[ObjectId]) {
515        assert!(graph.indexes_are_valid());
516        for target in targets {
517            let indexed = graph
518                .cascade_edges(target)
519                .into_iter()
520                .filter(|edge| matches!(edge.kind, DependencyKind::ViewDependency { .. }))
521                .map(|edge| graph.resolve_rename(&edge.dependent))
522                .collect::<Vec<_>>();
523            assert_eq!(indexed, canonical_views(graph, target));
524        }
525    }
526
527    #[test]
528    fn indexes_track_every_graph_mutation_and_alias_cycle() {
529        let a = id("a");
530        let b = id("b");
531        let c = id("c");
532        let d = id("d");
533        let targets = [a.clone(), b.clone(), c.clone(), d.clone()];
534        let mut graph = DependencyGraph::new();
535
536        graph.add_edge(view_edge("view_a", "a"));
537        graph.add_edge(view_edge("view_b", "b"));
538        for index in 0..DependencyGraph::CASCADE_INDEX_MIN_EDGES {
539            graph.add_edge(view_edge(
540                &format!("unrelated_view_{index}"),
541                &format!("unrelated_table_{index}"),
542            ));
543        }
544        assert_indexed_views_match_scan(&graph, &targets);
545
546        graph.add_edge(DependencyEdge::new(
547            a.clone(),
548            b.clone(),
549            DependencyKind::RenameTo,
550        ));
551        assert_indexed_views_match_scan(&graph, &targets);
552
553        graph.propagate_rename(&b, &c);
554        graph.add_edge(DependencyEdge::new(
555            b.clone(),
556            c.clone(),
557            DependencyKind::RenameTo,
558        ));
559        assert_indexed_views_match_scan(&graph, &targets);
560
561        graph.mutate_edges(|edges| {
562            for edge in edges {
563                if edge.dependent == id("view_b") {
564                    edge.dependent = id("view_c");
565                }
566            }
567        });
568        assert_indexed_views_match_scan(&graph, &targets);
569
570        let snapshot = graph.edges().to_vec();
571        graph.retain_edges(|edge| edge.dependent != id("view_a"));
572        assert_indexed_views_match_scan(&graph, &targets);
573        graph.replace_edges(snapshot);
574        assert_indexed_views_match_scan(&graph, &targets);
575
576        let checkpoint = graph.edge_count();
577        graph.add_edge(view_edge("temporary", "c"));
578        graph.truncate(checkpoint);
579        assert_indexed_views_match_scan(&graph, &targets);
580
581        graph.add_edge(DependencyEdge::new(
582            c.clone(),
583            a.clone(),
584            DependencyKind::RenameTo,
585        ));
586        assert_eq!(graph.resolve_rename(&a), &a);
587        assert_eq!(graph.resolve_rename(&b), &b);
588        assert_eq!(graph.resolve_rename(&c), &c);
589        assert_indexed_views_match_scan(&graph, &targets);
590    }
591}