Skip to main content

pgevolve_core/ir/
trigger.rs

1//! `Trigger` — a Postgres trigger declared via `CREATE [CONSTRAINT] TRIGGER`.
2
3use serde::{Deserialize, Serialize};
4
5use crate::identifier::{Identifier, QualifiedName};
6use crate::ir::constraint::Deferrable;
7use crate::ir::default_expr::NormalizedExpr;
8use crate::ir::eq::DiffMacro;
9
10/// A Postgres trigger.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, DiffMacro)]
12pub struct Trigger {
13    /// Schema-qualified trigger name. Schema mirrors the owning table's schema.
14    pub qname: QualifiedName,
15    /// Owning table (or view, for `INSTEAD OF` triggers).
16    pub table: QualifiedName,
17    /// When the trigger fires relative to the event.
18    #[diff(via_debug)]
19    pub timing: TriggerTiming,
20    /// Events that fire this trigger.
21    #[diff(via_debug)]
22    pub events: Vec<TriggerEvent>,
23    /// Row-level or statement-level.
24    #[diff(via_debug)]
25    pub level: TriggerLevel,
26    /// Optional `WHEN (condition)` predicate.
27    #[diff(via_debug)]
28    pub when_clause: Option<NormalizedExpr>,
29    /// Statement-level transition tables (`REFERENCING NEW TABLE AS n`).
30    #[diff(via_debug)]
31    pub transition_tables: Vec<TransitionTable>,
32    /// Qualified name of the trigger function.
33    pub function_qname: QualifiedName,
34    /// Literal string arguments passed to the trigger function.
35    #[diff(via_debug)]
36    pub function_args: Vec<String>,
37    /// `true` for `CREATE CONSTRAINT TRIGGER`.
38    pub is_constraint: bool,
39    /// Deferrability of constraint triggers.
40    #[diff(via_debug)]
41    pub deferrable: Deferrable,
42    /// Optional `COMMENT ON TRIGGER` text.
43    #[diff(via_debug)]
44    pub comment: Option<String>,
45}
46
47/// When a trigger fires relative to the triggering event.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub enum TriggerTiming {
51    /// Fires before the triggering event.
52    Before,
53    /// Fires after the triggering event.
54    After,
55    /// Replaces the triggering event (for views only).
56    InsteadOf,
57}
58
59/// An event that can trigger a trigger.
60#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(tag = "kind", rename_all = "snake_case")]
62pub enum TriggerEvent {
63    /// Trigger fires on `INSERT`.
64    Insert,
65    /// Trigger fires on `UPDATE` (optionally of specific columns).
66    Update {
67        /// Optional column list (`UPDATE OF col1, col2`). Empty = any column.
68        columns: Vec<Identifier>,
69    },
70    /// Trigger fires on `DELETE`.
71    Delete,
72    /// Trigger fires on `TRUNCATE`.
73    Truncate,
74}
75
76/// Whether a trigger fires for each row or each statement.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum TriggerLevel {
80    /// Fires once per affected row.
81    Row,
82    /// Fires once per statement.
83    Statement,
84}
85
86/// A statement-level transition table reference.
87#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
88pub struct TransitionTable {
89    /// The name of the transition table.
90    pub name: Identifier,
91    /// Whether this is a NEW or OLD table.
92    pub kind: TransitionKind,
93}
94
95/// Whether a transition table is for NEW or OLD rows.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum TransitionKind {
99    /// The `NEW` table containing inserted/updated rows.
100    NewTable,
101    /// The `OLD` table containing deleted/pre-updated rows.
102    OldTable,
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::ir::eq::Diff;
109
110    fn id(s: &str) -> Identifier {
111        Identifier::from_unquoted(s).unwrap()
112    }
113
114    fn qn(schema: &str, name: &str) -> QualifiedName {
115        QualifiedName::new(id(schema), id(name))
116    }
117
118    fn trg(name: &str) -> Trigger {
119        Trigger {
120            qname: qn("app", name),
121            table: qn("app", "users"),
122            timing: TriggerTiming::Before,
123            events: vec![TriggerEvent::Insert],
124            level: TriggerLevel::Row,
125            when_clause: None,
126            transition_tables: vec![],
127            function_qname: qn("app", "fn"),
128            function_args: vec![],
129            is_constraint: false,
130            deferrable: Deferrable::NotDeferrable,
131            comment: None,
132        }
133    }
134
135    #[test]
136    fn identical_triggers_diff_empty() {
137        assert!(trg("t1").canonical_eq(&trg("t1")));
138    }
139
140    #[test]
141    fn different_timing_diffs_reports_timing() {
142        let a = trg("t1");
143        let mut b = trg("t1");
144        b.timing = TriggerTiming::After;
145        let d = a.diff(&b);
146        assert!(d.iter().any(|x| x.path == "timing"));
147    }
148
149    #[test]
150    fn different_events_diffs() {
151        let a = trg("t1");
152        let mut b = trg("t1");
153        b.events = vec![TriggerEvent::Delete];
154        let d = a.diff(&b);
155        assert!(d.iter().any(|x| x.path == "events"));
156    }
157}