Skip to main content

mongreldb_core/
trigger.rs

1use crate::error::{MongrelError, Result};
2use crate::memtable::Value;
3use crate::schema::ColumnDef;
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::collections::HashSet;
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct TriggerEntry {
10    pub trigger: StoredTrigger,
11}
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct StoredTrigger {
15    pub name: String,
16    pub version: u64,
17    pub target: TriggerTarget,
18    pub timing: TriggerTiming,
19    pub event: TriggerEvent,
20    #[serde(default)]
21    pub update_of: Vec<String>,
22    #[serde(default)]
23    pub target_columns: Vec<ColumnDef>,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub when: Option<TriggerExpr>,
26    pub program: TriggerProgram,
27    pub enabled: bool,
28    pub checksum: String,
29    pub created_epoch: u64,
30    pub updated_epoch: u64,
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct TriggerDefinition {
35    pub target: TriggerTarget,
36    pub timing: TriggerTiming,
37    pub event: TriggerEvent,
38    #[serde(default)]
39    pub update_of: Vec<String>,
40    #[serde(default)]
41    pub target_columns: Vec<ColumnDef>,
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub when: Option<TriggerExpr>,
44    pub program: TriggerProgram,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48pub struct TriggerConfig {
49    pub recursive_triggers: bool,
50    pub max_depth: u32,
51}
52
53impl Default for TriggerConfig {
54    fn default() -> Self {
55        Self {
56            recursive_triggers: false,
57            max_depth: 32,
58        }
59    }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case", tag = "kind", content = "name")]
64pub enum TriggerTarget {
65    Table(String),
66    View(String),
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "snake_case")]
71pub enum TriggerTiming {
72    Before,
73    After,
74    InsteadOf,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum TriggerEvent {
80    Insert,
81    Update,
82    Delete,
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct TriggerProgram {
87    #[serde(default)]
88    pub steps: Vec<TriggerStep>,
89}
90
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case", tag = "kind")]
93pub enum TriggerStep {
94    SetNew {
95        cells: Vec<TriggerCell>,
96    },
97    Insert {
98        table: String,
99        cells: Vec<TriggerCell>,
100    },
101    UpdateByPk {
102        table: String,
103        pk: TriggerValue,
104        cells: Vec<TriggerCell>,
105    },
106    DeleteByPk {
107        table: String,
108        pk: TriggerValue,
109    },
110    Select {
111        id: String,
112        table: String,
113        #[serde(default)]
114        conditions: Vec<TriggerCondition>,
115    },
116    Raise {
117        action: TriggerRaiseAction,
118        message: TriggerValue,
119    },
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123pub struct TriggerCell {
124    pub column_id: u16,
125    pub value: TriggerValue,
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129#[serde(rename_all = "snake_case", tag = "kind")]
130pub enum TriggerCondition {
131    Pk { value: TriggerValue },
132    Eq { column_id: u16, value: TriggerValue },
133    IsNull { column_id: u16 },
134    IsNotNull { column_id: u16 },
135}
136
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
139pub enum TriggerValue {
140    Literal(Value),
141    NewColumn(u16),
142    OldColumn(u16),
143}
144
145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
146#[serde(rename_all = "snake_case", tag = "kind")]
147pub enum TriggerExpr {
148    Value(TriggerValue),
149    Eq {
150        left: TriggerValue,
151        right: TriggerValue,
152    },
153    NotEq {
154        left: TriggerValue,
155        right: TriggerValue,
156    },
157    IsNull(TriggerValue),
158    IsNotNull(TriggerValue),
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(rename_all = "snake_case")]
163pub enum TriggerRaiseAction {
164    Abort,
165    Fail,
166    Rollback,
167    Ignore,
168}
169
170impl StoredTrigger {
171    pub fn new(name: impl Into<String>, definition: TriggerDefinition, epoch: u64) -> Result<Self> {
172        let name = name.into();
173        let checksum = checksum(ChecksumInput {
174            name: &name,
175            version: 1,
176            target: &definition.target,
177            timing: definition.timing,
178            event: definition.event,
179            update_of: &definition.update_of,
180            target_columns: &definition.target_columns,
181            when: &definition.when,
182            program: &definition.program,
183        })?;
184        let trigger = Self {
185            name,
186            version: 1,
187            target: definition.target,
188            timing: definition.timing,
189            event: definition.event,
190            update_of: definition.update_of,
191            target_columns: definition.target_columns,
192            when: definition.when,
193            program: definition.program,
194            enabled: true,
195            checksum,
196            created_epoch: epoch,
197            updated_epoch: epoch,
198        };
199        trigger.validate()?;
200        Ok(trigger)
201    }
202
203    pub fn replaced(&self, mut next: StoredTrigger, epoch: u64) -> Result<StoredTrigger> {
204        next.name = self.name.clone();
205        next.version = self.version + 1;
206        next.created_epoch = self.created_epoch;
207        next.updated_epoch = epoch;
208        next.checksum = checksum(ChecksumInput {
209            name: &next.name,
210            version: next.version,
211            target: &next.target,
212            timing: next.timing,
213            event: next.event,
214            update_of: &next.update_of,
215            target_columns: &next.target_columns,
216            when: &next.when,
217            program: &next.program,
218        })?;
219        next.validate()?;
220        Ok(next)
221    }
222
223    pub fn retarget_table(&self, new_name: impl Into<String>, epoch: u64) -> Result<Self> {
224        let mut next = self.clone();
225        next.version = next.version.saturating_add(1);
226        next.target = TriggerTarget::Table(new_name.into());
227        next.updated_epoch = epoch;
228        next.refresh_checksum()?;
229        Ok(next)
230    }
231
232    pub fn renamed_update_column(
233        &self,
234        old_name: &str,
235        new_name: impl Into<String>,
236        epoch: u64,
237    ) -> Result<Self> {
238        let new_name = new_name.into();
239        let mut next = self.clone();
240        let mut changed = false;
241        for column in &mut next.update_of {
242            if column == old_name {
243                *column = new_name.clone();
244                changed = true;
245            }
246        }
247        if changed {
248            next.version = next.version.saturating_add(1);
249            next.updated_epoch = epoch;
250            next.refresh_checksum()?;
251        }
252        Ok(next)
253    }
254
255    pub fn validate(&self) -> Result<()> {
256        validate_name("trigger", &self.name)?;
257        match &self.target {
258            TriggerTarget::Table(name) => validate_name("trigger table target", name)?,
259            TriggerTarget::View(name) => validate_name("trigger view target", name)?,
260        }
261        match (&self.target, self.timing) {
262            (TriggerTarget::Table(_), TriggerTiming::Before | TriggerTiming::After) => {
263                if !self.target_columns.is_empty() {
264                    return Err(MongrelError::InvalidArgument(
265                        "table triggers must not carry target_columns".into(),
266                    ));
267                }
268            }
269            (TriggerTarget::View(_), TriggerTiming::InsteadOf) => {
270                validate_target_columns(&self.target_columns)?;
271            }
272            (TriggerTarget::Table(_), TriggerTiming::InsteadOf) => {
273                return Err(MongrelError::InvalidArgument(
274                    "INSTEAD OF triggers target views, not tables".into(),
275                ));
276            }
277            (TriggerTarget::View(_), TriggerTiming::Before | TriggerTiming::After) => {
278                return Err(MongrelError::InvalidArgument(
279                    "views support INSTEAD OF triggers, not BEFORE/AFTER triggers".into(),
280                ));
281            }
282        }
283        let mut update_cols = HashSet::new();
284        for col in &self.update_of {
285            validate_name("trigger UPDATE OF column", col)?;
286            if !update_cols.insert(col) {
287                return Err(MongrelError::InvalidArgument(format!(
288                    "duplicate trigger UPDATE OF column {col:?}"
289                )));
290            }
291        }
292        for step in &self.program.steps {
293            step.validate()?;
294        }
295        Ok(())
296    }
297
298    fn refresh_checksum(&mut self) -> Result<()> {
299        self.checksum = checksum(ChecksumInput {
300            name: &self.name,
301            version: self.version,
302            target: &self.target,
303            timing: self.timing,
304            event: self.event,
305            update_of: &self.update_of,
306            target_columns: &self.target_columns,
307            when: &self.when,
308            program: &self.program,
309        })?;
310        self.validate()
311    }
312}
313
314impl TriggerStep {
315    fn validate(&self) -> Result<()> {
316        match self {
317            TriggerStep::SetNew { cells } => {
318                let mut seen = HashSet::new();
319                for cell in cells {
320                    if !seen.insert(cell.column_id) {
321                        return Err(MongrelError::InvalidArgument(format!(
322                            "duplicate trigger cell column id {}",
323                            cell.column_id
324                        )));
325                    }
326                }
327                Ok(())
328            }
329            TriggerStep::Insert { table, cells } | TriggerStep::UpdateByPk { table, cells, .. } => {
330                validate_name("trigger step table", table)?;
331                let mut seen = HashSet::new();
332                for cell in cells {
333                    if !seen.insert(cell.column_id) {
334                        return Err(MongrelError::InvalidArgument(format!(
335                            "duplicate trigger cell column id {}",
336                            cell.column_id
337                        )));
338                    }
339                }
340                Ok(())
341            }
342            TriggerStep::DeleteByPk { table, .. } | TriggerStep::Select { table, .. } => {
343                validate_name("trigger step table", table)
344            }
345            TriggerStep::Raise { .. } => Ok(()),
346        }
347    }
348}
349
350impl From<StoredTrigger> for TriggerEntry {
351    fn from(trigger: StoredTrigger) -> Self {
352        Self { trigger }
353    }
354}
355
356#[derive(Serialize)]
357struct ChecksumInput<'a> {
358    name: &'a str,
359    version: u64,
360    target: &'a TriggerTarget,
361    timing: TriggerTiming,
362    event: TriggerEvent,
363    update_of: &'a [String],
364    target_columns: &'a [ColumnDef],
365    when: &'a Option<TriggerExpr>,
366    program: &'a TriggerProgram,
367}
368
369fn checksum(input: ChecksumInput<'_>) -> Result<String> {
370    let bytes = serde_json::to_vec(&input)
371        .map_err(|e| MongrelError::Other(format!("trigger checksum serialize: {e}")))?;
372    Ok(hex(&Sha256::digest(bytes)))
373}
374
375fn hex(bytes: &[u8]) -> String {
376    const HEX: &[u8; 16] = b"0123456789abcdef";
377    let mut out = String::with_capacity(bytes.len() * 2);
378    for b in bytes {
379        out.push(HEX[(b >> 4) as usize] as char);
380        out.push(HEX[(b & 0x0f) as usize] as char);
381    }
382    out
383}
384
385fn validate_target_columns(columns: &[ColumnDef]) -> Result<()> {
386    if columns.is_empty() {
387        return Err(MongrelError::InvalidArgument(
388            "view triggers require target_columns".into(),
389        ));
390    }
391    let mut names = HashSet::new();
392    let mut ids = HashSet::new();
393    for column in columns {
394        validate_name("trigger target column", &column.name)?;
395        if !names.insert(&column.name) {
396            return Err(MongrelError::InvalidArgument(format!(
397                "duplicate trigger target column {:?}",
398                column.name
399            )));
400        }
401        if !ids.insert(column.id) {
402            return Err(MongrelError::InvalidArgument(format!(
403                "duplicate trigger target column id {}",
404                column.id
405            )));
406        }
407    }
408    Ok(())
409}
410
411fn validate_name(kind: &str, name: &str) -> Result<()> {
412    if name.is_empty()
413        || !name
414            .chars()
415            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
416    {
417        return Err(MongrelError::InvalidArgument(format!(
418            "{kind} name {name:?} must be non-empty ASCII alphanumeric, '_' or '-'"
419        )));
420    }
421    Ok(())
422}