1use pg_query::NodeEnum;
30use pg_query::protobuf::{CreateTrigStmt, Node};
31
32use crate::identifier::{Identifier, QualifiedName};
33use crate::ir::constraint::Deferrable;
34use crate::ir::default_expr::NormalizedExpr;
35use crate::ir::trigger::{
36 TransitionKind, TransitionTable, Trigger, TriggerEvent, TriggerLevel, TriggerTiming,
37};
38use crate::parse::error::{ParseError, SourceLocation};
39use crate::parse::normalize_expr;
40
41pub(crate) fn build_trigger(
43 stmt: &CreateTrigStmt,
44 location: &SourceLocation,
45) -> Result<Trigger, ParseError> {
46 let name = Identifier::from_unquoted(&stmt.trigname).map_err(|e| ParseError::Structural {
47 location: location.clone(),
48 message: format!("CREATE TRIGGER: invalid name '{}': {e}", stmt.trigname),
49 })?;
50
51 let table = range_var_to_qname(stmt.relation.as_ref(), location, "trigger target table")?;
52 let qname = QualifiedName::new(table.schema.clone(), name);
54
55 if stmt.constrrel.is_some() {
56 return Err(ParseError::Structural {
57 location: location.clone(),
58 message: format!(
59 "{qname}: CREATE CONSTRAINT TRIGGER ... FROM ref_table is not supported."
60 ),
61 });
62 }
63
64 let timing = parse_timing(stmt.timing, location, &qname)?;
65 let events = parse_events(stmt.events, &stmt.columns, location, &qname)?;
66 let level = if stmt.row {
67 TriggerLevel::Row
68 } else {
69 TriggerLevel::Statement
70 };
71
72 let when_clause = match stmt.when_clause.as_deref() {
73 Some(node) => Some(node_to_normalized_expr(node, location, &qname)?),
74 None => None,
75 };
76
77 let transition_tables = parse_transition_rels(&stmt.transition_rels, location, &qname)?;
78
79 let function_qname = qualified_name_from_list(&stmt.funcname, location, "trigger function")?;
80 let function_args = string_list(&stmt.args);
81
82 let is_constraint = stmt.isconstraint;
83 let deferrable = if is_constraint {
84 if stmt.deferrable {
85 Deferrable::Deferrable {
86 initially_deferred: stmt.initdeferred,
87 }
88 } else {
89 Deferrable::NotDeferrable
90 }
91 } else {
92 Deferrable::NotDeferrable
94 };
95
96 if is_constraint {
98 if !matches!(timing, TriggerTiming::After) {
99 return Err(ParseError::Structural {
100 location: location.clone(),
101 message: format!("{qname}: CONSTRAINT TRIGGER must be AFTER."),
102 });
103 }
104 if !matches!(level, TriggerLevel::Row) {
105 return Err(ParseError::Structural {
106 location: location.clone(),
107 message: format!("{qname}: CONSTRAINT TRIGGER must be FOR EACH ROW."),
108 });
109 }
110 }
111
112 Ok(Trigger {
113 qname,
114 table,
115 timing,
116 events,
117 level,
118 when_clause,
119 transition_tables,
120 function_qname,
121 function_args,
122 is_constraint,
123 deferrable,
124 comment: None,
125 })
126}
127
128fn parse_timing(
129 timing: i32,
130 location: &SourceLocation,
131 qname: &QualifiedName,
132) -> Result<TriggerTiming, ParseError> {
133 const TRIGGER_TYPE_BEFORE: i32 = 1 << 1; const TRIGGER_TYPE_INSTEAD: i32 = 1 << 6; let bits = timing & (TRIGGER_TYPE_BEFORE | TRIGGER_TYPE_INSTEAD);
138 match bits {
139 b if b == TRIGGER_TYPE_BEFORE => Ok(TriggerTiming::Before),
140 b if b == TRIGGER_TYPE_INSTEAD => Ok(TriggerTiming::InsteadOf),
141 0 => Ok(TriggerTiming::After),
142 _ => Err(ParseError::Structural {
143 location: location.clone(),
144 message: format!("{qname}: invalid trigger timing bits 0x{timing:x}"),
145 }),
146 }
147}
148
149fn parse_events(
150 events: i32,
151 update_columns: &[pg_query::protobuf::Node],
152 location: &SourceLocation,
153 qname: &QualifiedName,
154) -> Result<Vec<TriggerEvent>, ParseError> {
155 const TRIGGER_TYPE_INSERT: i32 = 1 << 2; const TRIGGER_TYPE_DELETE: i32 = 1 << 3; const TRIGGER_TYPE_UPDATE: i32 = 1 << 4; const TRIGGER_TYPE_TRUNCATE: i32 = 1 << 5; let mut out = Vec::new();
161 if events & TRIGGER_TYPE_INSERT != 0 {
162 out.push(TriggerEvent::Insert);
163 }
164 if events & TRIGGER_TYPE_UPDATE != 0 {
165 let cols = update_columns
166 .iter()
167 .filter_map(|n| match n.node.as_ref() {
168 Some(NodeEnum::String(s)) => Identifier::from_unquoted(&s.sval).ok(),
169 _ => None,
170 })
171 .collect();
172 out.push(TriggerEvent::Update { columns: cols });
173 }
174 if events & TRIGGER_TYPE_DELETE != 0 {
175 out.push(TriggerEvent::Delete);
176 }
177 if events & TRIGGER_TYPE_TRUNCATE != 0 {
178 out.push(TriggerEvent::Truncate);
179 }
180 if out.is_empty() {
181 return Err(ParseError::Structural {
182 location: location.clone(),
183 message: format!("{qname}: trigger declares no events (bits=0x{events:x})"),
184 });
185 }
186 Ok(out)
187}
188
189fn parse_transition_rels(
190 rels: &[pg_query::protobuf::Node],
191 location: &SourceLocation,
192 qname: &QualifiedName,
193) -> Result<Vec<TransitionTable>, ParseError> {
194 let mut out = Vec::new();
195 for node in rels {
196 let Some(NodeEnum::TriggerTransition(trans)) = node.node.as_ref() else {
197 continue;
198 };
199 let name = Identifier::from_unquoted(&trans.name).map_err(|e| ParseError::Structural {
200 location: location.clone(),
201 message: format!(
202 "{qname}: invalid transition table name '{}': {e}",
203 trans.name
204 ),
205 })?;
206 let kind = if trans.is_new {
207 TransitionKind::NewTable
208 } else {
209 TransitionKind::OldTable
210 };
211 if !trans.is_table {
213 return Err(ParseError::Structural {
214 location: location.clone(),
215 message: format!("{qname}: REFERENCING must use NEW/OLD TABLE form."),
216 });
217 }
218 out.push(TransitionTable { name, kind });
219 }
220 Ok(out)
221}
222
223fn node_to_normalized_expr(
230 node: &Node,
231 location: &SourceLocation,
232 qname: &QualifiedName,
233) -> Result<NormalizedExpr, ParseError> {
234 let inner_enum = node.node.as_ref().ok_or_else(|| ParseError::Structural {
235 location: location.clone(),
236 message: format!("{qname}: WHEN clause node has no inner node"),
237 })?;
238 normalize_expr::from_pg_node(inner_enum, None, location).map_err(|e| ParseError::Structural {
239 location: location.clone(),
240 message: format!("{qname}: failed to canonicalize WHEN clause: {e}"),
241 })
242}
243
244fn range_var_to_qname(
245 rv: Option<&pg_query::protobuf::RangeVar>,
246 location: &SourceLocation,
247 context: &str,
248) -> Result<QualifiedName, ParseError> {
249 let rv = rv.ok_or_else(|| ParseError::Structural {
250 location: location.clone(),
251 message: format!("{context}: missing relation"),
252 })?;
253 if rv.schemaname.is_empty() {
254 return Err(ParseError::Structural {
255 location: location.clone(),
256 message: format!(
257 "{context}: relation '{}' has no schema qualifier",
258 rv.relname
259 ),
260 });
261 }
262 let schema = Identifier::from_unquoted(&rv.schemaname).map_err(|e| ParseError::Structural {
263 location: location.clone(),
264 message: format!("{context}: invalid schema '{}': {e}", rv.schemaname),
265 })?;
266 let name = Identifier::from_unquoted(&rv.relname).map_err(|e| ParseError::Structural {
267 location: location.clone(),
268 message: format!("{context}: invalid name '{}': {e}", rv.relname),
269 })?;
270 Ok(QualifiedName::new(schema, name))
271}
272
273fn qualified_name_from_list(
274 nodes: &[pg_query::protobuf::Node],
275 location: &SourceLocation,
276 context: &str,
277) -> Result<QualifiedName, ParseError> {
278 let parts: Vec<String> = nodes
279 .iter()
280 .filter_map(|n| match n.node.as_ref() {
281 Some(NodeEnum::String(s)) => Some(s.sval.clone()),
282 _ => None,
283 })
284 .collect();
285 let (schema_str, name_str) = match parts.as_slice() {
286 [s, n] => (s.clone(), n.clone()),
287 [n] => {
288 return Err(ParseError::Structural {
289 location: location.clone(),
290 message: format!("{context}: '{n}' must be schema-qualified"),
291 });
292 }
293 _ => {
294 return Err(ParseError::Structural {
295 location: location.clone(),
296 message: format!("{context}: unexpected name shape {parts:?}"),
297 });
298 }
299 };
300 let schema = Identifier::from_unquoted(&schema_str).map_err(|e| ParseError::Structural {
301 location: location.clone(),
302 message: format!("{context}: invalid schema '{schema_str}': {e}"),
303 })?;
304 let name = Identifier::from_unquoted(&name_str).map_err(|e| ParseError::Structural {
305 location: location.clone(),
306 message: format!("{context}: invalid name '{name_str}': {e}"),
307 })?;
308 Ok(QualifiedName::new(schema, name))
309}
310
311fn string_list(nodes: &[pg_query::protobuf::Node]) -> Vec<String> {
312 nodes
313 .iter()
314 .filter_map(|n| match n.node.as_ref() {
315 Some(NodeEnum::String(s)) => Some(s.sval.clone()),
316 _ => None,
317 })
318 .collect()
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 fn loc() -> SourceLocation {
326 SourceLocation::new(std::path::PathBuf::from("<test>"), 1, 1)
327 }
328
329 fn parse_trigger(sql: &str) -> Result<Trigger, ParseError> {
330 let parsed = pg_query::parse(sql).expect("pg_query");
331 let stmt = parsed
332 .protobuf
333 .stmts
334 .into_iter()
335 .next()
336 .expect("at least one statement")
337 .stmt
338 .expect("stmt")
339 .node
340 .expect("node");
341 match stmt {
342 NodeEnum::CreateTrigStmt(s) => build_trigger(&s, &loc()),
343 other => panic!("expected CreateTrigStmt, got {other:?}"),
344 }
345 }
346
347 #[test]
348 fn parses_simple_before_insert_row_trigger() {
349 let t = parse_trigger(
350 "CREATE TRIGGER t1 BEFORE INSERT ON app.users \
351 FOR EACH ROW EXECUTE FUNCTION app.f();",
352 )
353 .unwrap();
354 assert_eq!(t.qname.to_string(), "app.t1");
355 assert_eq!(t.table.to_string(), "app.users");
356 assert!(matches!(t.timing, TriggerTiming::Before));
357 assert_eq!(t.events, vec![TriggerEvent::Insert]);
358 assert!(matches!(t.level, TriggerLevel::Row));
359 assert!(t.when_clause.is_none());
360 assert_eq!(t.function_qname.to_string(), "app.f");
361 assert!(!t.is_constraint);
362 }
363
364 #[test]
365 fn parses_after_statement_trigger() {
366 let t = parse_trigger(
367 "CREATE TRIGGER t2 AFTER INSERT ON app.users \
368 FOR EACH STATEMENT EXECUTE FUNCTION app.f();",
369 )
370 .unwrap();
371 assert!(matches!(t.timing, TriggerTiming::After));
372 assert!(matches!(t.level, TriggerLevel::Statement));
373 }
374
375 #[test]
376 fn parses_multi_event_trigger() {
377 let t = parse_trigger(
378 "CREATE TRIGGER t BEFORE INSERT OR UPDATE OR DELETE ON app.users \
379 FOR EACH ROW EXECUTE FUNCTION app.f();",
380 )
381 .unwrap();
382 assert_eq!(t.events.len(), 3);
383 assert!(t.events.contains(&TriggerEvent::Insert));
384 assert!(t.events.contains(&TriggerEvent::Delete));
385 assert!(matches!(t.events[1], TriggerEvent::Update { .. }));
386 }
387
388 #[test]
389 fn parses_update_of_columns() {
390 let t = parse_trigger(
391 "CREATE TRIGGER t BEFORE UPDATE OF a, b ON app.users \
392 FOR EACH ROW EXECUTE FUNCTION app.f();",
393 )
394 .unwrap();
395 let cols = match &t.events[0] {
396 TriggerEvent::Update { columns } => columns,
397 other => panic!("expected Update, got {other:?}"),
398 };
399 assert_eq!(cols.len(), 2);
400 assert_eq!(cols[0].as_str(), "a");
401 assert_eq!(cols[1].as_str(), "b");
402 }
403
404 #[test]
405 fn parses_constraint_trigger_deferrable_initially_deferred() {
406 let t = parse_trigger(
407 "CREATE CONSTRAINT TRIGGER t AFTER INSERT ON app.users \
408 DEFERRABLE INITIALLY DEFERRED \
409 FOR EACH ROW EXECUTE FUNCTION app.f();",
410 )
411 .unwrap();
412 assert!(t.is_constraint);
413 assert!(matches!(
414 t.deferrable,
415 Deferrable::Deferrable {
416 initially_deferred: true
417 }
418 ));
419 }
420
421 #[test]
422 fn rejects_constraint_trigger_with_before_timing() {
423 use pg_query::protobuf::{CreateTrigStmt, RangeVar};
426 let stmt = CreateTrigStmt {
428 replace: false,
429 isconstraint: true,
430 trigname: "t".into(),
431 relation: Some(RangeVar {
432 catalogname: String::new(),
433 schemaname: "app".into(),
434 relname: "users".into(),
435 inh: true,
436 relpersistence: "p".into(),
437 alias: None,
438 location: -1,
439 }),
440 funcname: {
441 let s1 = pg_query::protobuf::Node {
443 node: Some(NodeEnum::String(pg_query::protobuf::String {
444 sval: "app".into(),
445 })),
446 };
447 let s2 = pg_query::protobuf::Node {
448 node: Some(NodeEnum::String(pg_query::protobuf::String {
449 sval: "f".into(),
450 })),
451 };
452 vec![s1, s2]
453 },
454 args: vec![],
455 row: true,
456 timing: 2, events: 4, columns: vec![],
459 when_clause: None,
460 transition_rels: vec![],
461 deferrable: false,
462 initdeferred: false,
463 constrrel: None,
464 };
465 let err = build_trigger(&stmt, &loc()).unwrap_err();
466 assert!(
467 err.to_string().contains("CONSTRAINT TRIGGER must be AFTER"),
468 "got {err}"
469 );
470 }
471
472 #[test]
473 fn parses_when_clause() {
474 let t = parse_trigger(
475 "CREATE TRIGGER t BEFORE INSERT ON app.users \
476 FOR EACH ROW WHEN (NEW.id > 0) EXECUTE FUNCTION app.f();",
477 )
478 .unwrap();
479 assert!(t.when_clause.is_some());
480 }
481
482 #[test]
483 fn parses_transition_tables() {
484 let t = parse_trigger(
485 "CREATE TRIGGER t AFTER INSERT ON app.users \
486 REFERENCING NEW TABLE AS new_rows \
487 FOR EACH STATEMENT EXECUTE FUNCTION app.f();",
488 )
489 .unwrap();
490 assert_eq!(t.transition_tables.len(), 1);
491 assert_eq!(t.transition_tables[0].name.as_str(), "new_rows");
492 assert!(matches!(
493 t.transition_tables[0].kind,
494 TransitionKind::NewTable
495 ));
496 }
497
498 #[test]
499 fn execute_procedure_is_synonym_for_execute_function() {
500 let t = parse_trigger(
501 "CREATE TRIGGER t BEFORE INSERT ON app.users \
502 FOR EACH ROW EXECUTE PROCEDURE app.f();",
503 )
504 .unwrap();
505 assert_eq!(t.function_qname.to_string(), "app.f");
506 }
507}