1use alloc::collections::BTreeMap;
31use alloc::format;
32use alloc::string::String;
33use alloc::vec::Vec;
34use core::fmt;
35
36use spg_sql::ast::{AssignTarget, Expr, PlPgSqlDeclare, PlPgSqlStmt, RaiseLevel, ReturnTarget};
37use spg_storage::{ColumnSchema, FunctionDef, Row, TriggerDef, Value};
38
39use crate::eval::{self, EvalContext, EvalError};
40
41#[derive(Debug, Clone, PartialEq)]
47pub struct DeferredEmbeddedStmt {
48 pub function: String,
51 pub stmt: spg_sql::ast::Statement,
53}
54
55#[derive(Debug, Clone, PartialEq)]
58pub enum TriggerOutcome {
59 Row(Row),
65 Skip,
69}
70
71#[derive(Debug, Clone, PartialEq)]
76pub enum TriggerError {
77 UnparseableBody { function: String, detail: String },
81 UnsupportedConstruct { function: String, detail: String },
86 OldIsReadOnly { function: String, column: String },
90 NewReadOnlyInAfterTrigger { function: String, column: String },
94 UnknownColumn {
97 function: String,
98 column: String,
99 table: String,
100 },
101 EvalFailed { function: String, cause: EvalError },
105 RaiseException { function: String, message: String },
110}
111
112impl fmt::Display for TriggerError {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 match self {
115 Self::UnparseableBody { function, detail } => {
116 write!(
117 f,
118 "trigger function {function:?} body did not parse: {detail}"
119 )
120 }
121 Self::UnsupportedConstruct { function, detail } => {
122 write!(
123 f,
124 "trigger function {function:?} uses an unsupported PL/pgSQL construct: {detail}"
125 )
126 }
127 Self::OldIsReadOnly { function, column } => {
128 write!(
129 f,
130 "trigger function {function:?}: cannot assign to OLD.{column} (OLD is read-only — PG rule)"
131 )
132 }
133 Self::NewReadOnlyInAfterTrigger { function, column } => {
134 write!(
135 f,
136 "trigger function {function:?}: cannot assign to NEW.{column} inside an AFTER trigger \
137 (NEW is read-only post-write — use BEFORE triggers for mutation, or an embedded UPDATE statement \
138 in v7.12.5+)"
139 )
140 }
141 Self::UnknownColumn {
142 function,
143 column,
144 table,
145 } => {
146 write!(
147 f,
148 "trigger function {function:?}: target column {column:?} not in table {table:?} schema"
149 )
150 }
151 Self::EvalFailed { function, cause } => {
152 write!(
153 f,
154 "trigger function {function:?}: expression eval failed: {cause}"
155 )
156 }
157 Self::RaiseException { function, message } => {
158 write!(
159 f,
160 "trigger function {function:?}: RAISE EXCEPTION {message:?}"
161 )
162 }
163 }
164 }
165}
166
167#[allow(clippy::too_many_arguments)] pub fn fire_row_trigger(
180 function: &FunctionDef,
181 new_row: Option<Row>,
182 old_row: Option<&Row>,
183 table_name: &str,
184 columns: &[ColumnSchema],
185 params: &[Value],
186 default_text_search_config: Option<&str>,
187 is_after: bool,
188) -> Result<(TriggerOutcome, Vec<DeferredEmbeddedStmt>), TriggerError> {
189 if !function.language.eq_ignore_ascii_case("plpgsql") {
190 return Err(TriggerError::UnsupportedConstruct {
191 function: function.name.clone(),
192 detail: format!(
193 "v7.12.4 only invokes LANGUAGE plpgsql trigger functions; \
194 {:?} declares LANGUAGE {}",
195 function.name, function.language
196 ),
197 });
198 }
199 let block = spg_sql::parse_function_body(&function.body).map_err(|e| {
200 TriggerError::UnparseableBody {
201 function: function.name.clone(),
202 detail: format!("{e}"),
203 }
204 })?;
205 let mut locals: BTreeMap<String, Value> = BTreeMap::new();
210 init_locals_from_declarations(
211 &block.declarations,
212 &mut locals,
213 new_row.as_ref(),
214 old_row,
215 columns,
216 table_name,
217 params,
218 default_text_search_config,
219 &function.name,
220 )?;
221 let mut current_new = new_row;
222 let ctx = BodyCtx {
223 function: &function.name,
224 table_name,
225 columns,
226 params,
227 default_text_search_config,
228 is_after,
229 select_into_resolver: None,
230 };
231 let mut deferred: Vec<DeferredEmbeddedStmt> = Vec::new();
232 let outcome = match execute_stmts(
233 &block.statements,
234 &mut current_new,
235 old_row,
236 &mut locals,
237 &ctx,
238 &mut deferred,
239 )? {
240 BodyOutcome::Return(target) => resolve_return(target, current_new, old_row),
241 BodyOutcome::FellThrough => TriggerOutcome::Skip,
245 };
246 Ok((outcome, deferred))
247}
248
249enum BodyOutcome {
254 Return(ReturnTarget),
255 FellThrough,
256}
257
258struct BodyCtx<'a> {
262 function: &'a str,
263 table_name: &'a str,
264 columns: &'a [ColumnSchema],
265 params: &'a [Value],
266 default_text_search_config: Option<&'a str>,
267 is_after: bool,
268 select_into_resolver: Option<&'a SelectIntoResolver<'a>>,
275}
276
277pub type SelectIntoResolver<'a> =
281 dyn Fn(&spg_sql::ast::Statement) -> Result<Value, TriggerError> + 'a;
282
283fn execute_stmts(
284 stmts: &[PlPgSqlStmt],
285 current_new: &mut Option<Row>,
286 old_row: Option<&Row>,
287 locals: &mut BTreeMap<String, Value>,
288 ctx: &BodyCtx<'_>,
289 deferred: &mut Vec<DeferredEmbeddedStmt>,
290) -> Result<BodyOutcome, TriggerError> {
291 for stmt in stmts {
292 match stmt {
293 PlPgSqlStmt::Assign { target, value } => {
294 let evaluated = eval_with_new_old_and_locals(
295 value,
296 current_new.as_ref(),
297 old_row,
298 locals,
299 ctx.columns,
300 ctx.table_name,
301 ctx.params,
302 ctx.default_text_search_config,
303 )
304 .map_err(|cause| TriggerError::EvalFailed {
305 function: ctx.function.into(),
306 cause,
307 })?;
308 match target {
309 AssignTarget::NewColumn(col) => {
310 if ctx.is_after {
311 return Err(TriggerError::NewReadOnlyInAfterTrigger {
312 function: ctx.function.into(),
313 column: col.clone(),
314 });
315 }
316 let pos = ctx
317 .columns
318 .iter()
319 .position(|c| c.name.eq_ignore_ascii_case(col))
320 .ok_or_else(|| TriggerError::UnknownColumn {
321 function: ctx.function.into(),
322 column: col.clone(),
323 table: alloc::string::ToString::to_string(&ctx.table_name),
324 })?;
325 let row = current_new.as_mut().ok_or_else(|| {
326 TriggerError::UnsupportedConstruct {
327 function: ctx.function.into(),
328 detail: format!(
329 "NEW.{col} := … requires a NEW row context \
330 (BEFORE INSERT / UPDATE only — not available on DELETE)"
331 ),
332 }
333 })?;
334 row.values[pos] = evaluated;
335 }
336 AssignTarget::OldColumn(col) => {
337 return Err(TriggerError::OldIsReadOnly {
338 function: ctx.function.into(),
339 column: col.clone(),
340 });
341 }
342 AssignTarget::Local(name) => {
343 locals.insert(name.clone(), evaluated);
350 }
351 }
352 }
353 PlPgSqlStmt::Return(target) => {
354 return Ok(BodyOutcome::Return(target.clone()));
355 }
356 PlPgSqlStmt::If {
357 branches,
358 else_branch,
359 } => {
360 let mut matched = false;
361 for (cond_expr, body) in branches {
362 let cond_val = eval_with_new_old_and_locals(
363 cond_expr,
364 current_new.as_ref(),
365 old_row,
366 locals,
367 ctx.columns,
368 ctx.table_name,
369 ctx.params,
370 ctx.default_text_search_config,
371 )
372 .map_err(|cause| TriggerError::EvalFailed {
373 function: ctx.function.into(),
374 cause,
375 })?;
376 if matches!(cond_val, Value::Bool(true)) {
377 matched = true;
378 match execute_stmts(body, current_new, old_row, locals, ctx, deferred)? {
379 BodyOutcome::Return(t) => return Ok(BodyOutcome::Return(t)),
380 BodyOutcome::FellThrough => {}
381 }
382 break;
383 }
384 }
385 if !matched && !else_branch.is_empty() {
386 match execute_stmts(else_branch, current_new, old_row, locals, ctx, deferred)? {
387 BodyOutcome::Return(t) => return Ok(BodyOutcome::Return(t)),
388 BodyOutcome::FellThrough => {}
389 }
390 }
391 }
392 PlPgSqlStmt::Raise {
393 level,
394 message,
395 args,
396 } => {
397 let mut rendered_args: Vec<String> = Vec::with_capacity(args.len());
400 for a in args {
401 let v = eval_with_new_old_and_locals(
402 a,
403 current_new.as_ref(),
404 old_row,
405 locals,
406 ctx.columns,
407 ctx.table_name,
408 ctx.params,
409 ctx.default_text_search_config,
410 )
411 .map_err(|cause| TriggerError::EvalFailed {
412 function: ctx.function.into(),
413 cause,
414 })?;
415 rendered_args.push(value_to_display_string(&v));
416 }
417 let resolved = format_raise_message(message, &rendered_args);
418 if matches!(level, RaiseLevel::Exception) {
419 return Err(TriggerError::RaiseException {
420 function: ctx.function.into(),
421 message: resolved,
422 });
423 }
424 let _ = resolved;
429 let _ = level;
430 }
431 PlPgSqlStmt::SelectInto { var, body } => {
432 let mut substituted = spg_sql::ast::Statement::Select((**body).clone());
438 substitute_trigger_context_in_statement(
439 &mut substituted,
440 current_new.as_ref(),
441 old_row,
442 locals,
443 ctx.columns,
444 )
445 .map_err(|cause| TriggerError::EvalFailed {
446 function: ctx.function.into(),
447 cause,
448 })?;
449 let resolver =
450 ctx.select_into_resolver.ok_or_else(|| TriggerError::UnsupportedConstruct {
451 function: ctx.function.into(),
452 detail: alloc::format!(
453 "SELECT … INTO {var}: only supported inside DO blocks (not trigger bodies) in v7.16.2"
454 ),
455 })?;
456 let value = resolver(&substituted)?;
457 locals.insert(var.clone(), value);
458 }
459 PlPgSqlStmt::EmbeddedSql(boxed_stmt) => {
460 let mut substituted = (**boxed_stmt).clone();
468 substitute_trigger_context_in_statement(
469 &mut substituted,
470 current_new.as_ref(),
471 old_row,
472 locals,
473 ctx.columns,
474 )
475 .map_err(|cause| TriggerError::EvalFailed {
476 function: ctx.function.into(),
477 cause,
478 })?;
479 deferred.push(DeferredEmbeddedStmt {
480 function: ctx.function.into(),
481 stmt: substituted,
482 });
483 }
484 }
485 }
486 Ok(BodyOutcome::FellThrough)
487}
488
489pub fn execute_do_block_top_level<'a>(
510 block: &spg_sql::ast::PlPgSqlBlock,
511 default_text_search_config: Option<&'a str>,
512 select_into_resolver: Option<&'a SelectIntoResolver<'a>>,
513) -> Result<Vec<spg_sql::ast::Statement>, TriggerError> {
514 let mut locals: BTreeMap<String, Value> = BTreeMap::new();
515 let empty_cols: &[ColumnSchema] = &[];
516 init_locals_from_declarations(
517 &block.declarations,
518 &mut locals,
519 None,
520 None,
521 empty_cols,
522 "",
523 &[],
524 default_text_search_config,
525 "DO",
526 )?;
527 let ctx = BodyCtx {
528 function: "DO",
529 table_name: "",
530 columns: empty_cols,
531 params: &[],
532 default_text_search_config,
533 is_after: false,
534 select_into_resolver,
535 };
536 let mut current_new: Option<Row> = None;
537 let mut deferred: Vec<DeferredEmbeddedStmt> = Vec::new();
538 let _ = execute_stmts(
543 &block.statements,
544 &mut current_new,
545 None,
546 &mut locals,
547 &ctx,
548 &mut deferred,
549 )?;
550 Ok(deferred.into_iter().map(|d| d.stmt).collect())
551}
552
553fn resolve_return(
554 target: ReturnTarget,
555 current_new: Option<Row>,
556 old_row: Option<&Row>,
557) -> TriggerOutcome {
558 match target {
559 ReturnTarget::New => current_new.map_or(TriggerOutcome::Skip, TriggerOutcome::Row),
560 ReturnTarget::Old => old_row
561 .cloned()
562 .map_or(TriggerOutcome::Skip, TriggerOutcome::Row),
563 ReturnTarget::Null => TriggerOutcome::Skip,
564 ReturnTarget::Expr(_) => TriggerOutcome::Skip,
567 }
568}
569
570#[allow(clippy::too_many_arguments)]
571fn init_locals_from_declarations(
572 decls: &[PlPgSqlDeclare],
573 locals: &mut BTreeMap<String, Value>,
574 new_row: Option<&Row>,
575 old_row: Option<&Row>,
576 columns: &[ColumnSchema],
577 table_name: &str,
578 params: &[Value],
579 default_text_search_config: Option<&str>,
580 function_name: &str,
581) -> Result<(), TriggerError> {
582 for d in decls {
583 let v = if let Some(init) = &d.default {
584 eval_with_new_old_and_locals(
585 init,
586 new_row,
587 old_row,
588 locals,
589 columns,
590 table_name,
591 params,
592 default_text_search_config,
593 )
594 .map_err(|cause| TriggerError::EvalFailed {
595 function: function_name.into(),
596 cause,
597 })?
598 } else {
599 Value::Null
600 };
601 locals.insert(d.name.clone(), v);
602 }
603 Ok(())
604}
605
606fn format_raise_message(fmt: &str, args: &[String]) -> String {
609 let mut out = String::with_capacity(fmt.len());
610 let mut iter = args.iter();
611 let mut chars = fmt.chars().peekable();
612 while let Some(c) = chars.next() {
613 if c == '%' {
614 match chars.peek() {
615 Some('%') => {
616 out.push('%');
617 chars.next();
618 }
619 _ => {
620 if let Some(a) = iter.next() {
621 out.push_str(a);
622 } else {
623 out.push('%');
627 }
628 }
629 }
630 } else {
631 out.push(c);
632 }
633 }
634 out
635}
636
637fn value_to_display_string(v: &Value) -> String {
641 use alloc::string::ToString;
642 match v {
643 Value::Null => String::new(),
644 Value::Bool(b) => b.to_string(),
645 Value::SmallInt(n) => n.to_string(),
646 Value::Int(n) => n.to_string(),
647 Value::BigInt(n) => n.to_string(),
648 Value::Float(x) => x.to_string(),
649 Value::Text(s) | Value::Json(s) => s.clone(),
650 other => format!("{other:?}"),
651 }
652}
653
654#[allow(clippy::too_many_arguments)]
666fn eval_with_new_old_and_locals(
667 expr: &Expr,
668 new_row: Option<&Row>,
669 old_row: Option<&Row>,
670 locals: &BTreeMap<String, Value>,
671 columns: &[ColumnSchema],
672 table_alias: &str,
673 params: &[Value],
674 default_text_search_config: Option<&str>,
675) -> Result<Value, EvalError> {
676 let mut rewritten = expr.clone();
677 substitute_locals(&mut rewritten, locals);
678 substitute_new_old(&mut rewritten, new_row, old_row, columns)?;
679 let ctx = EvalContext::new(columns, Some(table_alias))
680 .with_params(params)
681 .with_default_text_search_config(default_text_search_config);
682 let empty = Row::new(Vec::new());
683 eval::eval_expr(&rewritten, &empty, &ctx)
684}
685
686fn substitute_locals(expr: &mut Expr, locals: &BTreeMap<String, Value>) {
692 if let Expr::Column(c) = expr {
693 if c.qualifier.is_none()
694 && let Some(v) = locals.get(&c.name)
695 {
696 *expr = value_to_literal_expr(&[], 0, v.clone());
697 return;
698 }
699 }
700 match expr {
701 Expr::AggregateOrdered { call, order_by, .. } => {
702 substitute_locals(call, locals);
703 for o in order_by.iter_mut() {
704 substitute_locals(&mut o.expr, locals);
705 }
706 }
707 Expr::Binary { lhs, rhs, .. } => {
708 substitute_locals(lhs, locals);
709 substitute_locals(rhs, locals);
710 }
711 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
712 substitute_locals(expr, locals);
713 }
714 Expr::Like { expr, pattern, .. } => {
715 substitute_locals(expr, locals);
716 substitute_locals(pattern, locals);
717 }
718 Expr::FunctionCall { args, .. } => {
719 for a in args {
720 substitute_locals(a, locals);
721 }
722 }
723 Expr::Extract { source, .. } => substitute_locals(source, locals),
724 Expr::Array(items) => {
725 for elem in items {
726 substitute_locals(elem, locals);
727 }
728 }
729 Expr::ArraySubscript { target, index } => {
730 substitute_locals(target, locals);
731 substitute_locals(index, locals);
732 }
733 Expr::AnyAll { expr, array, .. } => {
734 substitute_locals(expr, locals);
735 substitute_locals(array, locals);
736 }
737 Expr::InList { expr, list, .. } => {
738 substitute_locals(expr, locals);
739 for item in list {
740 substitute_locals(item, locals);
741 }
742 }
743 Expr::Case {
744 operand,
745 branches,
746 else_branch,
747 } => {
748 if let Some(o) = operand {
749 substitute_locals(o, locals);
750 }
751 for (w, t) in branches {
752 substitute_locals(w, locals);
753 substitute_locals(t, locals);
754 }
755 if let Some(e) = else_branch {
756 substitute_locals(e, locals);
757 }
758 }
759 Expr::Literal(_)
760 | Expr::Placeholder(_)
761 | Expr::Column(_)
762 | Expr::WindowFunction { .. }
763 | Expr::ScalarSubquery(_)
764 | Expr::Exists { .. }
765 | Expr::InSubquery { .. } => {}
766 }
767}
768
769fn eval_with_new_old(
770 expr: &Expr,
771 new_row: Option<&Row>,
772 old_row: Option<&Row>,
773 columns: &[ColumnSchema],
774 table_alias: &str,
775 params: &[Value],
776 default_text_search_config: Option<&str>,
777) -> Result<Value, EvalError> {
778 let mut rewritten = expr.clone();
779 substitute_new_old(&mut rewritten, new_row, old_row, columns)?;
780 let ctx = EvalContext::new(columns, Some(table_alias))
781 .with_params(params)
782 .with_default_text_search_config(default_text_search_config);
783 let empty = Row::new(Vec::new());
787 eval::eval_expr(&rewritten, &empty, &ctx)
788}
789
790fn substitute_new_old(
797 expr: &mut Expr,
798 new_row: Option<&Row>,
799 old_row: Option<&Row>,
800 columns: &[ColumnSchema],
801) -> Result<(), EvalError> {
802 if let Expr::Column(c) = expr {
803 if let Some(q) = &c.qualifier {
804 let lower = q.to_ascii_lowercase();
805 if lower == "new" || lower == "old" {
806 let (row, side) = if lower == "new" {
807 (new_row, "NEW")
808 } else {
809 (old_row, "OLD")
810 };
811 let pos = columns
812 .iter()
813 .position(|sc| sc.name.eq_ignore_ascii_case(&c.name))
814 .ok_or_else(|| EvalError::ColumnNotFound {
815 name: format!("{side}.{}", c.name),
816 })?;
817 let v = match row {
818 Some(r) => r.values.get(pos).cloned().unwrap_or(Value::Null),
819 None => Value::Null,
820 };
821 *expr = value_to_literal_expr(columns, pos, v);
822 return Ok(());
823 }
824 }
825 }
826 match expr {
827 Expr::AggregateOrdered { call, order_by, .. } => {
828 substitute_new_old(call, new_row, old_row, columns)?;
829 for o in order_by.iter_mut() {
830 substitute_new_old(&mut o.expr, new_row, old_row, columns)?;
831 }
832 }
833 Expr::Binary { lhs, rhs, .. } => {
834 substitute_new_old(lhs, new_row, old_row, columns)?;
835 substitute_new_old(rhs, new_row, old_row, columns)?;
836 }
837 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
838 substitute_new_old(expr, new_row, old_row, columns)?;
839 }
840 Expr::Like { expr, pattern, .. } => {
841 substitute_new_old(expr, new_row, old_row, columns)?;
842 substitute_new_old(pattern, new_row, old_row, columns)?;
843 }
844 Expr::FunctionCall { args, .. } => {
845 for a in args {
846 substitute_new_old(a, new_row, old_row, columns)?;
847 }
848 }
849 Expr::Extract { source, .. } => substitute_new_old(source, new_row, old_row, columns)?,
850 Expr::Array(items) => {
851 for elem in items {
852 substitute_new_old(elem, new_row, old_row, columns)?;
853 }
854 }
855 Expr::ArraySubscript { target, index } => {
856 substitute_new_old(target, new_row, old_row, columns)?;
857 substitute_new_old(index, new_row, old_row, columns)?;
858 }
859 Expr::AnyAll { expr, array, .. } => {
860 substitute_new_old(expr, new_row, old_row, columns)?;
861 substitute_new_old(array, new_row, old_row, columns)?;
862 }
863 Expr::InList { expr, list, .. } => {
864 substitute_new_old(expr, new_row, old_row, columns)?;
865 for item in list {
866 substitute_new_old(item, new_row, old_row, columns)?;
867 }
868 }
869 Expr::Case {
870 operand,
871 branches,
872 else_branch,
873 } => {
874 if let Some(o) = operand {
875 substitute_new_old(o, new_row, old_row, columns)?;
876 }
877 for (w, t) in branches {
878 substitute_new_old(w, new_row, old_row, columns)?;
879 substitute_new_old(t, new_row, old_row, columns)?;
880 }
881 if let Some(e) = else_branch {
882 substitute_new_old(e, new_row, old_row, columns)?;
883 }
884 }
885 Expr::Literal(_)
889 | Expr::Placeholder(_)
890 | Expr::Column(_)
891 | Expr::WindowFunction { .. }
892 | Expr::ScalarSubquery(_)
893 | Expr::Exists { .. }
894 | Expr::InSubquery { .. } => {}
895 }
896 Ok(())
897}
898
899fn value_to_literal_expr(_columns: &[ColumnSchema], _pos: usize, v: Value) -> Expr {
903 use spg_sql::ast::Literal;
904 let lit = match v {
905 Value::Null => Literal::Null,
906 Value::Bool(b) => Literal::Bool(b),
907 Value::SmallInt(n) => Literal::Integer(i64::from(n)),
908 Value::Int(n) => Literal::Integer(i64::from(n)),
909 Value::BigInt(n) => Literal::Integer(n),
910 Value::Float(x) => Literal::Float(x),
911 Value::Text(s) | Value::Json(s) => Literal::String(s),
912 other => Literal::String(format!("{other:?}")),
917 };
918 Expr::Literal(lit)
919}
920
921fn substitute_trigger_context_in_statement(
927 stmt: &mut spg_sql::ast::Statement,
928 new_row: Option<&Row>,
929 old_row: Option<&Row>,
930 locals: &BTreeMap<String, Value>,
931 columns: &[ColumnSchema],
932) -> Result<(), EvalError> {
933 use spg_sql::ast::Statement;
934 let mut walk = |e: &mut Expr| -> Result<(), EvalError> {
935 substitute_locals(e, locals);
936 substitute_new_old(e, new_row, old_row, columns)?;
937 Ok(())
938 };
939 match stmt {
940 Statement::Insert(s) => {
941 for tuple in &mut s.rows {
942 for e in tuple {
943 walk(e)?;
944 }
945 }
946 }
947 Statement::Update(s) => {
948 for (_col, e) in &mut s.assignments {
949 walk(e)?;
950 }
951 if let Some(w) = &mut s.where_ {
952 walk(w)?;
953 }
954 }
955 Statement::Delete(s) => {
956 if let Some(w) = &mut s.where_ {
957 walk(w)?;
958 }
959 }
960 Statement::Select(s) => {
961 substitute_trigger_context_in_select(s, new_row, old_row, locals, columns)?
962 }
963 _ => {}
969 }
970 Ok(())
971}
972
973fn substitute_trigger_context_in_select(
974 s: &mut spg_sql::ast::SelectStatement,
975 new_row: Option<&Row>,
976 old_row: Option<&Row>,
977 locals: &BTreeMap<String, Value>,
978 columns: &[ColumnSchema],
979) -> Result<(), EvalError> {
980 use spg_sql::ast::SelectItem;
981 let mut walk = |e: &mut Expr| -> Result<(), EvalError> {
982 substitute_locals(e, locals);
983 substitute_new_old(e, new_row, old_row, columns)?;
984 Ok(())
985 };
986 for item in &mut s.items {
987 if let SelectItem::Expr { expr, .. } = item {
988 walk(expr)?;
989 }
990 }
991 if let Some(w) = &mut s.where_ {
992 walk(w)?;
993 }
994 if let Some(group_by) = &mut s.group_by {
995 for g in group_by {
996 walk(g)?;
997 }
998 }
999 if let Some(h) = &mut s.having {
1000 walk(h)?;
1001 }
1002 for ob in &mut s.order_by {
1003 walk(&mut ob.expr)?;
1004 }
1005 let _ = &s.limit;
1009 let _ = &s.offset;
1010 Ok(())
1011}
1012
1013pub fn matching_trigger_names<'a>(
1018 triggers: &'a [TriggerDef],
1019 table: &str,
1020 event: &str,
1021 timing: &str,
1022) -> Vec<&'a TriggerDef> {
1023 triggers
1024 .iter()
1025 .filter(|t| {
1026 t.table == table
1027 && t.timing.eq_ignore_ascii_case(timing)
1028 && t.for_each.eq_ignore_ascii_case("row")
1029 && t.events.iter().any(|e| e.eq_ignore_ascii_case(event))
1030 })
1031 .collect()
1032}