Skip to main content

pgevolve_core/diff/
triggers.rs

1//! Differ for `Catalog::triggers`.
2//!
3//! Pair-by-qname. Any structural difference emits Replace (DROP + CREATE
4//! in the planner). Comment-only differences emit `CommentOn`.
5//! Triggers carry no data; Replace is non-destructive.
6
7use std::collections::BTreeMap;
8
9use crate::diff::change::{Change, TriggerChange};
10use crate::diff::changeset::ChangeSet;
11use crate::diff::destructiveness::Destructiveness;
12use crate::ir::trigger::Trigger;
13
14/// Compute trigger-level changes needed to converge `target` toward `source`.
15///
16/// Pair-by-qname semantics: any structural difference emits `Replace`; a
17/// comment-only difference emits `CommentOn`. Both are `Safe` — triggers
18/// carry no data.
19pub fn diff_triggers(target: &[Trigger], source: &[Trigger], out: &mut ChangeSet) {
20    let target_by: BTreeMap<_, _> = target.iter().map(|t| (t.qname.clone(), t)).collect();
21    let source_by: BTreeMap<_, _> = source.iter().map(|t| (t.qname.clone(), t)).collect();
22
23    for (qname, t) in &target_by {
24        if !source_by.contains_key(qname) {
25            out.push(
26                Change::Trigger(TriggerChange::Drop {
27                    qname: qname.clone(),
28                    table: t.table.clone(),
29                }),
30                Destructiveness::Safe,
31            );
32        }
33    }
34
35    for (qname, s) in &source_by {
36        match target_by.get(qname) {
37            None => out.push(
38                Change::Trigger(TriggerChange::Create((*s).clone())),
39                Destructiveness::Safe,
40            ),
41            Some(t) => {
42                let structurally_equal = same_structure(s, t);
43                let comment_changed = s.comment != t.comment;
44                if structurally_equal && comment_changed {
45                    out.push(
46                        Change::Trigger(TriggerChange::CommentOn {
47                            qname: qname.clone(),
48                            table: t.table.clone(),
49                            comment: s.comment.clone(),
50                        }),
51                        Destructiveness::Safe,
52                    );
53                } else if !structurally_equal {
54                    out.push(
55                        Change::Trigger(TriggerChange::Replace((*s).clone())),
56                        Destructiveness::Safe,
57                    );
58                }
59            }
60        }
61    }
62}
63
64/// True iff the two triggers are structurally identical (ignoring comment).
65fn same_structure(a: &Trigger, b: &Trigger) -> bool {
66    a.table == b.table
67        && a.timing == b.timing
68        && a.events == b.events
69        && a.level == b.level
70        && a.when_clause == b.when_clause
71        && a.transition_tables == b.transition_tables
72        && a.function_qname == b.function_qname
73        && a.function_args == b.function_args
74        && a.is_constraint == b.is_constraint
75        && a.deferrable == b.deferrable
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::identifier::{Identifier, QualifiedName};
82    use crate::ir::constraint::Deferrable;
83    use crate::ir::trigger::{TriggerEvent, TriggerLevel, TriggerTiming};
84
85    fn id(s: &str) -> Identifier {
86        Identifier::from_unquoted(s).unwrap()
87    }
88    fn qn(schema: &str, name: &str) -> QualifiedName {
89        QualifiedName::new(id(schema), id(name))
90    }
91    fn trg(name: &str) -> Trigger {
92        Trigger {
93            qname: qn("app", name),
94            table: qn("app", "users"),
95            timing: TriggerTiming::Before,
96            events: vec![TriggerEvent::Insert],
97            level: TriggerLevel::Row,
98            when_clause: None,
99            transition_tables: vec![],
100            function_qname: qn("app", "f"),
101            function_args: vec![],
102            is_constraint: false,
103            deferrable: Deferrable::NotDeferrable,
104            comment: None,
105        }
106    }
107
108    #[test]
109    fn create_when_only_in_source() {
110        let mut cs = ChangeSet::new();
111        diff_triggers(&[], &[trg("t1")], &mut cs);
112        assert!(matches!(
113            cs.iter().next().map(|e| &e.change),
114            Some(Change::Trigger(TriggerChange::Create(_)))
115        ));
116    }
117
118    #[test]
119    fn drop_when_only_in_target() {
120        let mut cs = ChangeSet::new();
121        diff_triggers(&[trg("t1")], &[], &mut cs);
122        let first = cs.iter().next().expect("one change");
123        assert!(matches!(
124            &first.change,
125            Change::Trigger(TriggerChange::Drop { .. })
126        ));
127        assert!(matches!(&first.destructiveness, Destructiveness::Safe));
128    }
129
130    #[test]
131    fn timing_change_emits_replace() {
132        let t = trg("t1");
133        let mut s = trg("t1");
134        s.timing = TriggerTiming::After;
135        let mut cs = ChangeSet::new();
136        diff_triggers(&[t], &[s], &mut cs);
137        let first = cs.iter().next().expect("one change");
138        assert!(matches!(
139            &first.change,
140            Change::Trigger(TriggerChange::Replace(_))
141        ));
142    }
143
144    #[test]
145    fn comment_only_change_emits_comment_on() {
146        let t = trg("t1");
147        let mut s = trg("t1");
148        s.comment = Some("docs".into());
149        let mut cs = ChangeSet::new();
150        diff_triggers(&[t], &[s], &mut cs);
151        let first = cs.iter().next().expect("one change");
152        assert!(matches!(
153            &first.change,
154            Change::Trigger(TriggerChange::CommentOn { .. })
155        ));
156    }
157
158    #[test]
159    fn identical_triggers_emit_no_change() {
160        let t = trg("t1");
161        let s = trg("t1");
162        let mut cs = ChangeSet::new();
163        diff_triggers(&[t], &[s], &mut cs);
164        assert!(cs.iter().next().is_none());
165    }
166}