use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use spg_sql::ast::{AssignTarget, Expr, PlPgSqlDeclare, PlPgSqlStmt, RaiseLevel, ReturnTarget};
use spg_storage::{ColumnSchema, FunctionDef, Row, StorageError, TriggerDef, Value};
use crate::eval::{self, EvalContext, EvalError};
use crate::{CancelToken, Engine, EngineError, MAX_TRIGGER_RECURSION};
#[derive(Debug, Clone, PartialEq)]
pub struct DeferredEmbeddedStmt {
pub function: String,
pub stmt: spg_sql::ast::Statement,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TriggerOutcome {
Row(Row<'static>),
Skip,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TriggerError {
UnparseableBody { function: String, detail: String },
UnsupportedConstruct { function: String, detail: String },
OldIsReadOnly { function: String, column: String },
NewReadOnlyInAfterTrigger { function: String, column: String },
UnknownColumn {
function: String,
column: String,
table: String,
},
EvalFailed { function: String, cause: EvalError },
RaiseException { function: String, message: String },
}
impl fmt::Display for TriggerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnparseableBody { function, detail } => {
write!(
f,
"trigger function {function:?} body did not parse: {detail}"
)
}
Self::UnsupportedConstruct { function, detail } => {
write!(
f,
"trigger function {function:?} uses an unsupported PL/pgSQL construct: {detail}"
)
}
Self::OldIsReadOnly { function, column } => {
write!(
f,
"trigger function {function:?}: cannot assign to OLD.{column} (OLD is read-only — PG rule)"
)
}
Self::NewReadOnlyInAfterTrigger { function, column } => {
write!(
f,
"trigger function {function:?}: cannot assign to NEW.{column} inside an AFTER trigger \
(NEW is read-only post-write — use BEFORE triggers for mutation, or an embedded UPDATE statement \
in v7.12.5+)"
)
}
Self::UnknownColumn {
function,
column,
table,
} => {
write!(
f,
"trigger function {function:?}: target column {column:?} not in table {table:?} schema"
)
}
Self::EvalFailed { function, cause } => {
write!(
f,
"trigger function {function:?}: expression eval failed: {cause}"
)
}
Self::RaiseException { function, message } => {
write!(
f,
"trigger function {function:?}: RAISE EXCEPTION {message:?}"
)
}
}
}
}
#[derive(Debug)]
pub struct TgMeta<'a> {
pub op: &'a str,
pub name: &'a str,
pub level: &'a str,
}
#[allow(clippy::too_many_arguments)] pub fn fire_row_trigger(
function: &FunctionDef,
new_row: Option<Row<'static>>,
old_row: Option<&Row<'static>>,
table_name: &str,
columns: &[ColumnSchema],
params: &[Value<'static>],
default_text_search_config: Option<&str>,
is_after: bool,
tg: &TgMeta<'_>,
notice_sink: Option<&NoticeSink>,
) -> Result<(TriggerOutcome, Vec<DeferredEmbeddedStmt>), TriggerError> {
if !function.language.eq_ignore_ascii_case("plpgsql") {
return Err(TriggerError::UnsupportedConstruct {
function: function.name.clone(),
detail: format!(
"v7.12.4 only invokes LANGUAGE plpgsql trigger functions; \
{:?} declares LANGUAGE {}",
function.name, function.language
),
});
}
let block = spg_sql::parse_function_body(&function.body).map_err(|e| {
TriggerError::UnparseableBody {
function: function.name.clone(),
detail: format!("{e}"),
}
})?;
let mut locals: BTreeMap<String, Value<'static>> = BTreeMap::new();
locals.insert(
"tg_op".into(),
Value::text::<alloc::string::String>(tg.op.into()),
);
locals.insert(
"tg_when".into(),
Value::text::<alloc::string::String>(if is_after { "AFTER" } else { "BEFORE" }.into()),
);
locals.insert(
"tg_level".into(),
Value::text::<alloc::string::String>(tg.level.into()),
);
locals.insert(
"tg_name".into(),
Value::text::<alloc::string::String>(tg.name.into()),
);
locals.insert(
"tg_table_name".into(),
Value::text::<alloc::string::String>(table_name.into()),
);
locals.insert(
"tg_table_schema".into(),
Value::text::<alloc::string::String>("public".into()),
);
locals.insert(
"tg_relname".into(),
Value::text::<alloc::string::String>(table_name.into()),
);
locals.insert("tg_nargs".into(), Value::Int(0));
init_locals_from_declarations(
&block.declarations,
&mut locals,
new_row.as_ref(),
old_row,
columns,
table_name,
params,
default_text_search_config,
&function.name,
None,
)?;
let mut current_new = new_row;
let ctx = BodyCtx {
function: &function.name,
table_name,
columns,
params,
default_text_search_config,
is_after,
select_into_resolver: None,
notice_sink,
for_query_resolver: None,
set_sink: None,
};
let mut deferred: Vec<DeferredEmbeddedStmt> = Vec::new();
let outcome = match execute_stmts(
&block.statements,
&mut current_new,
old_row,
&mut locals,
&ctx,
&mut deferred,
)? {
BodyOutcome::Return(target) => resolve_return(target, current_new, old_row),
BodyOutcome::FellThrough | BodyOutcome::Break | BodyOutcome::Continue => {
TriggerOutcome::Skip
}
};
Ok((outcome, deferred))
}
enum BodyOutcome {
Return(ReturnTarget),
FellThrough,
Break,
Continue,
}
pub type NoticeSink = core::cell::RefCell<Vec<(crate::NoticeSeverity, String)>>;
struct BodyCtx<'a> {
function: &'a str,
table_name: &'a str,
columns: &'a [ColumnSchema],
params: &'a [Value<'static>],
default_text_search_config: Option<&'a str>,
is_after: bool,
select_into_resolver: Option<&'a SelectIntoResolver<'a>>,
notice_sink: Option<&'a NoticeSink>,
for_query_resolver: Option<&'a ForQueryResolver<'a>>,
set_sink: Option<&'a core::cell::RefCell<Vec<Vec<Value<'static>>>>>,
}
pub type SelectIntoResolver<'a> =
dyn Fn(&spg_sql::ast::Statement) -> Result<Value<'static>, TriggerError> + 'a;
pub type ForQueryResolver<'a> = dyn Fn(
&spg_sql::ast::Statement,
) -> Result<
(
alloc::vec::Vec<String>,
alloc::vec::Vec<alloc::vec::Vec<Value<'static>>>,
),
TriggerError,
> + 'a;
fn execute_stmts(
stmts: &[PlPgSqlStmt],
current_new: &mut Option<Row<'static>>,
old_row: Option<&Row<'static>>,
locals: &mut BTreeMap<String, Value<'static>>,
ctx: &BodyCtx<'_>,
deferred: &mut Vec<DeferredEmbeddedStmt>,
) -> Result<BodyOutcome, TriggerError> {
for stmt in stmts {
match stmt {
PlPgSqlStmt::Assign { target, value } => {
let evaluated = eval_with_new_old_and_locals(
value,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
match target {
AssignTarget::NewColumn(col) => {
let pos = ctx
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(col))
.ok_or_else(|| TriggerError::UnknownColumn {
function: ctx.function.into(),
column: col.clone(),
table: alloc::string::ToString::to_string(&ctx.table_name),
})?;
let row = current_new.as_mut().ok_or_else(|| {
TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: format!(
"NEW.{col} := … requires a NEW row context \
(BEFORE INSERT / UPDATE only — not available on DELETE)"
),
}
})?;
row.values[pos] = evaluated;
}
AssignTarget::OldColumn(col) => {
let _ = col;
let _ = evaluated;
}
AssignTarget::Local(name) => {
locals.insert(name.clone(), evaluated);
}
}
}
PlPgSqlStmt::Return(target) => {
return Ok(BodyOutcome::Return(target.clone()));
}
PlPgSqlStmt::ReturnNext(e) => {
let sink = ctx
.set_sink
.ok_or_else(|| TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: alloc::string::String::from(
"cannot use RETURN NEXT in a non-SETOF function",
),
})?;
let v = eval_with_new_old_and_locals(
e,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
sink.borrow_mut().push(alloc::vec![v]);
}
PlPgSqlStmt::ReturnQuery(query) => {
let sink = ctx
.set_sink
.ok_or_else(|| TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: alloc::string::String::from(
"cannot use RETURN QUERY in a non-SETOF function",
),
})?;
let resolver =
ctx.for_query_resolver
.ok_or_else(|| TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: alloc::string::String::from(
"RETURN QUERY needs a query runner (this context has none)",
),
})?;
let mut stmt = spg_sql::ast::Statement::Select((**query).clone());
substitute_trigger_context_in_statement(
&mut stmt,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
let (_cols, rows) = resolver(&stmt)?;
sink.borrow_mut().extend(rows);
}
PlPgSqlStmt::If {
branches,
else_branch,
} => {
let mut matched = false;
for (cond_expr, body) in branches {
let cond_val = eval_with_new_old_and_locals(
cond_expr,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
if matches!(cond_val, Value::Bool(true)) {
matched = true;
match execute_stmts(body, current_new, old_row, locals, ctx, deferred)? {
BodyOutcome::Return(t) => return Ok(BodyOutcome::Return(t)),
BodyOutcome::Break => return Ok(BodyOutcome::Break),
BodyOutcome::Continue => return Ok(BodyOutcome::Continue),
BodyOutcome::FellThrough => {}
}
break;
}
}
if !matched && !else_branch.is_empty() {
match execute_stmts(else_branch, current_new, old_row, locals, ctx, deferred)? {
BodyOutcome::Return(t) => return Ok(BodyOutcome::Return(t)),
BodyOutcome::Break => return Ok(BodyOutcome::Break),
BodyOutcome::Continue => return Ok(BodyOutcome::Continue),
BodyOutcome::FellThrough => {}
}
}
}
PlPgSqlStmt::Raise {
level,
message,
args,
} => {
let mut rendered_args: Vec<String> = Vec::with_capacity(args.len());
for a in args {
let v = eval_with_new_old_and_locals(
a,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
rendered_args.push(value_to_display_string(&v));
}
let resolved = format_raise_message(message, &rendered_args);
if matches!(level, RaiseLevel::Exception) {
return Err(TriggerError::RaiseException {
function: ctx.function.into(),
message: resolved,
});
}
let severity = match level {
RaiseLevel::Notice => Some(crate::NoticeSeverity::Notice),
RaiseLevel::Warning => Some(crate::NoticeSeverity::Warning),
RaiseLevel::Info => Some(crate::NoticeSeverity::Info),
_ => None,
};
if let (Some(sev), Some(sink)) = (severity, ctx.notice_sink) {
sink.borrow_mut().push((sev, resolved));
}
}
PlPgSqlStmt::SelectInto { var, body } => {
let mut substituted = spg_sql::ast::Statement::Select((**body).clone());
substitute_trigger_context_in_statement(
&mut substituted,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
let resolver =
ctx.select_into_resolver.ok_or_else(|| TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: alloc::format!(
"SELECT … INTO {var}: only supported inside DO blocks (not trigger bodies) in v7.16.2"
),
})?;
let value = resolver(&substituted)?;
let found_after_select_into = !matches!(value, spg_storage::Value::Null);
locals.insert(
"found".into(),
spg_storage::Value::Bool(found_after_select_into),
);
locals.insert(var.clone(), value);
}
PlPgSqlStmt::ForRange {
var,
start,
end,
reverse,
body,
} => {
const FOR_RANGE_BUDGET: i64 = 1_000_000;
let s_v = eval_with_new_old_and_locals(
start,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
let e_v = eval_with_new_old_and_locals(
end,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
let to_i64 = |v: &spg_storage::Value<'static>| -> Result<i64, TriggerError> {
match v {
spg_storage::Value::Int(n) => Ok(i64::from(*n)),
spg_storage::Value::BigInt(n) => Ok(*n),
spg_storage::Value::SmallInt(n) => Ok(i64::from(*n)),
other => Err(TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: alloc::format!(
"FOR <var> IN start..end: bounds must be integer, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
};
let s = to_i64(&s_v)?;
let e = to_i64(&e_v)?;
let (lo, hi, step): (i64, i64, i64) = if *reverse { (s, e, -1) } else { (s, e, 1) };
let mut i = lo;
let mut iter: i64 = 0;
loop {
if iter >= FOR_RANGE_BUDGET {
return Err(TriggerError::RaiseException {
function: ctx.function.into(),
message: alloc::format!(
"FOR loop iteration budget {FOR_RANGE_BUDGET} reached"
),
});
}
let cont = if *reverse { i >= hi } else { i <= hi };
if !cont {
break;
}
locals.insert(var.clone(), spg_storage::Value::BigInt(i));
match execute_stmts(body, current_new, old_row, locals, ctx, deferred)? {
BodyOutcome::FellThrough | BodyOutcome::Continue => {}
BodyOutcome::Break => break,
early @ BodyOutcome::Return(_) => return Ok(early),
}
i = i.saturating_add(step);
iter += 1;
}
}
PlPgSqlStmt::Loop { body } => {
const LOOP_BUDGET: u64 = 1_000_000;
let mut iter: u64 = 0;
loop {
if iter >= LOOP_BUDGET {
return Err(TriggerError::RaiseException {
function: ctx.function.into(),
message: alloc::format!("LOOP iteration budget {LOOP_BUDGET} reached"),
});
}
match execute_stmts(body, current_new, old_row, locals, ctx, deferred)? {
BodyOutcome::FellThrough | BodyOutcome::Continue => {}
BodyOutcome::Break => break,
early @ BodyOutcome::Return(_) => return Ok(early),
}
iter += 1;
}
}
PlPgSqlStmt::Exit { when } => {
let should_break = match when {
None => true,
Some(cond) => {
let v = eval_with_new_old_and_locals(
cond,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
matches!(v, spg_storage::Value::Bool(true))
}
};
if should_break {
return Ok(BodyOutcome::Break);
}
}
PlPgSqlStmt::ForExecute {
var,
sql_expr,
body,
} => {
let resolver =
ctx.for_query_resolver
.ok_or_else(|| TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: alloc::format!(
"FOR <var> IN EXECUTE <expr> LOOP: only supported inside DO blocks"
),
})?;
let v = eval_with_new_old_and_locals(
sql_expr,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
let sql_text = match v {
spg_storage::Value::Text(s) => s.into_owned(),
other => {
return Err(TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: alloc::format!(
"FOR IN EXECUTE: expression must evaluate to TEXT, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let stmt = spg_sql::parser::parse_statement(&sql_text).map_err(|e| {
TriggerError::UnparseableBody {
function: ctx.function.into(),
detail: alloc::format!(
"FOR IN EXECUTE {sql_text:?}: parse failed: {}",
e.message
),
}
})?;
let (col_names, rows) = resolver(&stmt)?;
for row_values in rows {
for (i, cname) in col_names.iter().enumerate() {
locals.insert(
alloc::format!(
"{}.{}",
var.to_ascii_lowercase(),
cname.to_ascii_lowercase()
),
row_values
.get(i)
.cloned()
.unwrap_or(spg_storage::Value::Null),
);
}
let first_cell = row_values
.into_iter()
.next()
.unwrap_or(spg_storage::Value::Null);
locals.insert(var.clone(), first_cell);
match execute_stmts(body, current_new, old_row, locals, ctx, deferred)? {
BodyOutcome::FellThrough | BodyOutcome::Continue => {}
BodyOutcome::Break => break,
early @ BodyOutcome::Return(_) => return Ok(early),
}
}
}
PlPgSqlStmt::ForQuery { var, query, body } => {
let resolver = ctx.for_query_resolver.ok_or_else(|| {
TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: alloc::format!(
"FOR <var> IN <SELECT> LOOP: only supported inside DO blocks in v7.37.20 (trigger paths queue with v7.40)"
),
}
})?;
let mut stmt = spg_sql::ast::Statement::Select((**query).clone());
substitute_trigger_context_in_statement(
&mut stmt,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
let (col_names, rows) = resolver(&stmt)?;
for row_values in rows {
for (i, cname) in col_names.iter().enumerate() {
locals.insert(
alloc::format!(
"{}.{}",
var.to_ascii_lowercase(),
cname.to_ascii_lowercase()
),
row_values
.get(i)
.cloned()
.unwrap_or(spg_storage::Value::Null),
);
}
let first_cell = row_values
.into_iter()
.next()
.unwrap_or(spg_storage::Value::Null);
locals.insert(var.clone(), first_cell);
match execute_stmts(body, current_new, old_row, locals, ctx, deferred)? {
BodyOutcome::FellThrough | BodyOutcome::Continue => {}
BodyOutcome::Break => break,
early @ BodyOutcome::Return(_) => return Ok(early),
}
}
}
PlPgSqlStmt::ReturnQueryExecute { sql } => {
let sink = ctx
.set_sink
.ok_or_else(|| TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: alloc::string::String::from(
"cannot use RETURN QUERY in a non-SETOF function",
),
})?;
let resolver =
ctx.for_query_resolver
.ok_or_else(|| TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: alloc::string::String::from(
"RETURN QUERY EXECUTE needs a query runner (this context has none)",
),
})?;
let sql_val = eval_with_new_old_and_locals(
sql,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
let Value::Text(text) = &sql_val else {
return Err(TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: alloc::format!(
"RETURN QUERY EXECUTE needs a text SQL string, got {}",
crate::conversions::pg_type_name_for_error_opt(sql_val.data_type())
),
});
};
let stmt = spg_sql::parser::parse_statement(text.as_ref()).map_err(|e| {
TriggerError::UnparseableBody {
function: ctx.function.into(),
detail: alloc::format!("RETURN QUERY EXECUTE: {e}"),
}
})?;
let (_cols, rows) = resolver(&stmt)?;
sink.borrow_mut().extend(rows);
}
PlPgSqlStmt::ExecuteDynamic { sql } => {
let v = eval_with_new_old_and_locals(
sql,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
let sql_text = match v {
spg_storage::Value::Text(s) => s.into_owned(),
other => {
return Err(TriggerError::UnsupportedConstruct {
function: ctx.function.into(),
detail: alloc::format!(
"EXECUTE <expr>: expression must evaluate to TEXT, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let parsed = spg_sql::parser::parse_statement(&sql_text).map_err(|e| {
TriggerError::UnparseableBody {
function: ctx.function.into(),
detail: alloc::format!("EXECUTE {sql_text:?}: parse failed: {}", e.message),
}
})?;
deferred.push(DeferredEmbeddedStmt {
function: ctx.function.into(),
stmt: parsed,
});
}
PlPgSqlStmt::Continue { when } => {
let should_continue = match when {
None => true,
Some(cond) => {
let v = eval_with_new_old_and_locals(
cond,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
matches!(v, spg_storage::Value::Bool(true))
}
};
if should_continue {
return Ok(BodyOutcome::Continue);
}
}
PlPgSqlStmt::While { condition, body } => {
const WHILE_LOOP_BUDGET: u64 = 1_000_000;
let mut iter: u64 = 0;
loop {
if iter >= WHILE_LOOP_BUDGET {
return Err(TriggerError::RaiseException {
function: ctx.function.into(),
message: alloc::format!(
"WHILE loop iteration budget {WHILE_LOOP_BUDGET} reached — likely runaway condition"
),
});
}
let v = eval_with_new_old_and_locals(
condition,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
if !matches!(v, spg_storage::Value::Bool(true)) {
break;
}
match execute_stmts(body, current_new, old_row, locals, ctx, deferred)? {
BodyOutcome::FellThrough | BodyOutcome::Continue => {}
BodyOutcome::Break => break,
early @ BodyOutcome::Return(_) => return Ok(early),
}
iter += 1;
}
}
PlPgSqlStmt::Assert { condition, message } => {
let v = eval_with_new_old_and_locals(
condition,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
let cond_holds = matches!(v, spg_storage::Value::Bool(true));
if !cond_holds {
let msg_text = if let Some(m) = message {
let mv = eval_with_new_old_and_locals(
m,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
ctx.table_name,
ctx.params,
ctx.default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
value_to_display_string(&mv)
} else {
alloc::string::String::from("assertion failed")
};
return Err(TriggerError::RaiseException {
function: ctx.function.into(),
message: msg_text,
});
}
}
PlPgSqlStmt::EmbeddedSql(boxed_stmt) => {
let mut substituted = (**boxed_stmt).clone();
substitute_trigger_context_in_statement(
&mut substituted,
current_new.as_ref(),
old_row,
locals,
ctx.columns,
)
.map_err(|cause| TriggerError::EvalFailed {
function: ctx.function.into(),
cause,
})?;
deferred.push(DeferredEmbeddedStmt {
function: ctx.function.into(),
stmt: substituted,
});
}
}
}
Ok(BodyOutcome::FellThrough)
}
pub fn execute_do_block_top_level<'a>(
block: &spg_sql::ast::PlPgSqlBlock,
default_text_search_config: Option<&'a str>,
select_into_resolver: Option<&'a SelectIntoResolver<'a>>,
for_query_resolver: Option<&'a ForQueryResolver<'a>>,
notice_sink: Option<&'a NoticeSink>,
) -> Result<Vec<spg_sql::ast::Statement>, TriggerError> {
let set_sink: Option<&core::cell::RefCell<Vec<Vec<Value<'static>>>>> = None;
let mut locals: BTreeMap<String, Value<'static>> = BTreeMap::new();
let empty_cols: &[ColumnSchema] = &[];
init_locals_from_declarations(
&block.declarations,
&mut locals,
None,
None,
empty_cols,
"",
&[],
default_text_search_config,
"DO",
select_into_resolver,
)?;
let ctx = BodyCtx {
function: "DO",
table_name: "",
columns: empty_cols,
params: &[],
default_text_search_config,
is_after: false,
select_into_resolver,
notice_sink,
for_query_resolver,
set_sink,
};
let mut current_new: Option<Row> = None;
let mut deferred: Vec<DeferredEmbeddedStmt> = Vec::new();
let body_result = execute_stmts(
&block.statements,
&mut current_new,
None,
&mut locals,
&ctx,
&mut deferred,
);
if let Err(err) = body_result {
if !block.exception_handlers.is_empty() {
if let TriggerError::RaiseException { message, .. } = &err {
for handler in &block.exception_handlers {
let matches = handler.conditions.iter().any(|c| {
c.eq_ignore_ascii_case("others")
|| message
.to_ascii_lowercase()
.contains(&c.to_ascii_lowercase())
});
if matches {
locals.insert("sqlerrm".into(), Value::text(message.clone()));
locals.insert(
"sqlstate".into(),
Value::text(alloc::string::String::from("P0001")),
);
let _ = execute_stmts(
&handler.body,
&mut current_new,
None,
&mut locals,
&ctx,
&mut deferred,
)?;
return Ok(deferred.into_iter().map(|d| d.stmt).collect());
}
}
}
}
return Err(err);
}
Ok(deferred.into_iter().map(|d| d.stmt).collect())
}
pub fn call_plpgsql_scalar<'a>(
function: &str,
block: &spg_sql::ast::PlPgSqlBlock,
args: BTreeMap<String, Value<'static>>,
default_text_search_config: Option<&'a str>,
select_into_resolver: Option<&'a SelectIntoResolver<'a>>,
for_query_resolver: Option<&'a ForQueryResolver<'a>>,
set_sink: Option<&'a core::cell::RefCell<Vec<Vec<Value<'static>>>>>,
notice_sink: Option<&'a NoticeSink>,
) -> Result<Option<Value<'static>>, TriggerError> {
let mut locals: BTreeMap<String, Value<'static>> = args;
let empty_cols: &[ColumnSchema] = &[];
init_locals_from_declarations(
&block.declarations,
&mut locals,
None,
None,
empty_cols,
"",
&[],
default_text_search_config,
function,
select_into_resolver,
)?;
let ctx = BodyCtx {
function,
table_name: "",
columns: empty_cols,
params: &[],
default_text_search_config,
is_after: false,
select_into_resolver,
notice_sink,
for_query_resolver,
set_sink,
};
let mut current_new: Option<Row> = None;
let mut deferred: Vec<DeferredEmbeddedStmt> = Vec::new();
let mut outcome = execute_stmts(
&block.statements,
&mut current_new,
None,
&mut locals,
&ctx,
&mut deferred,
);
if let Err(err) = outcome {
let mut handled = None;
if !block.exception_handlers.is_empty()
&& let TriggerError::RaiseException { message, .. } = &err
{
for handler in &block.exception_handlers {
let matches = handler.conditions.iter().any(|c| {
c.eq_ignore_ascii_case("others")
|| message
.to_ascii_lowercase()
.contains(&c.to_ascii_lowercase())
});
if matches {
locals.insert("sqlerrm".into(), Value::text(message.clone()));
locals.insert(
"sqlstate".into(),
Value::text(alloc::string::String::from("P0001")),
);
handled = Some(execute_stmts(
&handler.body,
&mut current_new,
None,
&mut locals,
&ctx,
&mut deferred,
)?);
break;
}
}
}
match handled {
Some(o) => outcome = Ok(o),
None => return Err(err),
}
}
if !deferred.is_empty() {
return Err(TriggerError::UnsupportedConstruct {
function: function.into(),
detail: alloc::string::String::from(
"a plpgsql function body that writes (INSERT / UPDATE / DELETE) \
cannot be called from an expression",
),
});
}
match outcome.expect("error paths returned above") {
BodyOutcome::Return(ReturnTarget::Expr(e)) => {
let v = eval_with_new_old_and_locals(
&e,
None,
None,
&locals,
empty_cols,
"",
&[],
default_text_search_config,
ctx.select_into_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: function.into(),
cause,
})?;
Ok(Some(v))
}
BodyOutcome::Return(ReturnTarget::Null) => Ok(Some(Value::Null)),
BodyOutcome::Return(_) => Err(TriggerError::UnsupportedConstruct {
function: function.into(),
detail: alloc::string::String::from("RETURN NEW / OLD is only meaningful in a trigger"),
}),
_ => Ok(None),
}
}
fn resolve_return(
target: ReturnTarget,
current_new: Option<Row<'static>>,
old_row: Option<&Row<'static>>,
) -> TriggerOutcome {
match target {
ReturnTarget::New => current_new.map_or(TriggerOutcome::Skip, TriggerOutcome::Row),
ReturnTarget::Old => old_row
.cloned()
.map_or(TriggerOutcome::Skip, TriggerOutcome::Row),
ReturnTarget::Null => TriggerOutcome::Skip,
ReturnTarget::Expr(_) => TriggerOutcome::Skip,
}
}
#[allow(clippy::too_many_arguments)]
fn init_locals_from_declarations(
decls: &[PlPgSqlDeclare],
locals: &mut BTreeMap<String, Value>,
new_row: Option<&Row<'static>>,
old_row: Option<&Row<'static>>,
columns: &[ColumnSchema],
table_name: &str,
params: &[Value<'static>],
default_text_search_config: Option<&str>,
function_name: &str,
subquery_resolver: Option<&SelectIntoResolver<'_>>,
) -> Result<(), TriggerError> {
for d in decls {
let v = if let Some(init) = &d.default {
eval_with_new_old_and_locals(
init,
new_row,
old_row,
locals,
columns,
table_name,
params,
default_text_search_config,
subquery_resolver,
)
.map_err(|cause| TriggerError::EvalFailed {
function: function_name.into(),
cause,
})?
} else {
Value::Null
};
locals.insert(d.name.clone(), v);
}
Ok(())
}
fn format_raise_message(fmt: &str, args: &[String]) -> String {
let mut out = String::with_capacity(fmt.len());
let mut iter = args.iter();
let mut chars = fmt.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
match chars.peek() {
Some('%') => {
out.push('%');
chars.next();
}
_ => {
if let Some(a) = iter.next() {
out.push_str(a);
} else {
out.push('%');
}
}
}
} else {
out.push(c);
}
}
out
}
fn value_to_display_string(v: &Value) -> String {
use alloc::string::ToString;
match v {
Value::Null => String::new(),
Value::Bool(b) => b.to_string(),
Value::SmallInt(n) => n.to_string(),
Value::Int(n) => n.to_string(),
Value::BigInt(n) => n.to_string(),
Value::Float(x) => x.to_string(),
Value::Text(s) | Value::Json(s) => s.to_string(),
other => format!("{other:?}"),
}
}
#[allow(clippy::too_many_arguments)]
fn eval_with_new_old_and_locals(
expr: &Expr,
new_row: Option<&Row<'static>>,
old_row: Option<&Row<'static>>,
locals: &BTreeMap<String, Value>,
columns: &[ColumnSchema],
table_alias: &str,
params: &[Value<'static>],
default_text_search_config: Option<&str>,
subquery_resolver: Option<&SelectIntoResolver<'_>>,
) -> Result<Value<'static>, EvalError> {
let mut rewritten = expr.clone();
substitute_locals(&mut rewritten, locals);
substitute_new_old(&mut rewritten, new_row, old_row, columns)?;
if let Some(resolver) = subquery_resolver {
let mut failure: Option<EvalError> = None;
substitute_locals_visiting(&mut rewritten, locals, &mut |node| {
if failure.is_some() {
return;
}
let Expr::ScalarSubquery(sel) = node else {
return;
};
let mut stmt = spg_sql::ast::Statement::Select((**sel).clone());
if let Err(e) = substitute_trigger_context_in_statement(
&mut stmt, new_row, old_row, locals, columns,
) {
failure = Some(e);
return;
}
match resolver(&stmt) {
Ok(v) => *node = value_to_literal_expr(&[], 0, v),
Err(e) => {
failure = Some(EvalError::TypeMismatch {
detail: alloc::format!("{e}"),
});
}
}
});
if let Some(e) = failure {
return Err(e);
}
}
let ctx = EvalContext::new(columns, Some(table_alias))
.with_params(params)
.with_default_text_search_config(default_text_search_config);
let empty = Row::new(Vec::new());
eval::eval_expr(&rewritten, &empty, &ctx)
}
fn substitute_locals(expr: &mut Expr, locals: &BTreeMap<String, Value>) {
substitute_locals_visiting(expr, locals, &mut |_| {});
}
fn substitute_locals_visiting(
expr: &mut Expr,
locals: &BTreeMap<String, Value>,
visit: &mut dyn FnMut(&mut Expr),
) {
visit(expr);
if let Expr::Column(c) = expr {
if c.qualifier.is_none()
&& let Some(v) = locals.get(&c.name)
{
*expr = value_to_literal_expr(&[], 0, v.clone());
return;
}
if let Some(q) = &c.qualifier {
let key = alloc::format!("{}.{}", q.to_ascii_lowercase(), c.name.to_ascii_lowercase());
if let Some(v) = locals.get(&key) {
*expr = value_to_literal_expr(&[], 0, v.clone());
return;
}
}
}
match expr {
Expr::NamedArg { expr, .. } => substitute_locals_visiting(expr, locals, visit),
Expr::Variadic(expr) => substitute_locals_visiting(expr, locals, visit),
Expr::AggregateOrdered { call, order_by, .. } => {
substitute_locals_visiting(call, locals, visit);
for o in order_by.iter_mut() {
substitute_locals_visiting(&mut o.expr, locals, visit);
}
}
Expr::Binary { lhs, rhs, .. } => {
substitute_locals_visiting(lhs, locals, visit);
substitute_locals_visiting(rhs, locals, visit);
}
Expr::Unary { expr, .. }
| Expr::Cast { expr, .. }
| Expr::IsNull { expr, .. }
| Expr::BoolTest { expr, .. }
| Expr::FieldAccess { base: expr, .. } => {
substitute_locals_visiting(expr, locals, visit);
}
Expr::Like { expr, pattern, .. } => {
substitute_locals_visiting(expr, locals, visit);
substitute_locals_visiting(pattern, locals, visit);
}
Expr::FunctionCall { args, .. } => {
for a in args {
substitute_locals_visiting(a, locals, visit);
}
}
Expr::Extract { source, .. } => substitute_locals_visiting(source, locals, visit),
Expr::Array(items) => {
for elem in items {
substitute_locals_visiting(elem, locals, visit);
}
}
Expr::ArraySubscript { target, index } => {
substitute_locals_visiting(target, locals, visit);
substitute_locals_visiting(index, locals, visit);
}
Expr::ArraySlice { target, lo, hi } => {
substitute_locals_visiting(target, locals, visit);
if let Some(l) = lo {
substitute_locals_visiting(l, locals, visit);
}
if let Some(h) = hi {
substitute_locals_visiting(h, locals, visit);
}
}
Expr::AnyAll { expr, array, .. } => {
substitute_locals_visiting(expr, locals, visit);
substitute_locals_visiting(array, locals, visit);
}
Expr::InList { expr, list, .. } => {
substitute_locals_visiting(expr, locals, visit);
for item in list {
substitute_locals_visiting(item, locals, visit);
}
}
Expr::Case {
operand,
branches,
else_branch,
} => {
if let Some(o) = operand {
substitute_locals_visiting(o, locals, visit);
}
for (w, t) in branches {
substitute_locals_visiting(w, locals, visit);
substitute_locals_visiting(t, locals, visit);
}
if let Some(e) = else_branch {
substitute_locals_visiting(e, locals, visit);
}
}
Expr::Literal(_)
| Expr::Placeholder(_)
| Expr::Column(_)
| Expr::WindowFunction { .. }
| Expr::ScalarSubquery(_)
| Expr::Exists { .. }
| Expr::InSubquery { .. }
| Expr::RowInSubquery { .. }
| Expr::RowCmpSubquery { .. } => {}
}
}
fn eval_with_new_old(
expr: &Expr,
new_row: Option<&Row<'static>>,
old_row: Option<&Row<'static>>,
columns: &[ColumnSchema],
table_alias: &str,
params: &[Value<'static>],
default_text_search_config: Option<&str>,
) -> Result<Value<'static>, EvalError> {
let mut rewritten = expr.clone();
substitute_new_old(&mut rewritten, new_row, old_row, columns)?;
let ctx = EvalContext::new(columns, Some(table_alias))
.with_params(params)
.with_default_text_search_config(default_text_search_config);
let empty = Row::new(Vec::new());
eval::eval_expr(&rewritten, &empty, &ctx)
}
pub(crate) fn trigger_when_holds(
when_text: &str,
new_row: Option<&Row<'static>>,
old_row: Option<&Row<'static>>,
columns: &[ColumnSchema],
) -> Result<bool, EngineError> {
if when_text.is_empty() {
return Ok(true);
}
let mut expr = spg_sql::parser::parse_expression(when_text)
.map_err(|e| EngineError::Unsupported(alloc::format!("trigger WHEN: {e}")))?;
substitute_new_old(&mut expr, new_row, old_row, columns).map_err(EngineError::Eval)?;
let ctx = crate::eval::EvalContext::new(&[], None);
let empty = Row::new(alloc::vec::Vec::new());
let v = crate::eval::eval_expr(&expr, &empty, &ctx).map_err(EngineError::Eval)?;
Ok(matches!(v, Value::Bool(true)))
}
pub(crate) fn substitute_new_old(
expr: &mut Expr,
new_row: Option<&Row<'static>>,
old_row: Option<&Row<'static>>,
columns: &[ColumnSchema],
) -> Result<(), EvalError> {
if let Expr::Column(c) = expr {
if let Some(q) = &c.qualifier {
let lower = q.to_ascii_lowercase();
if lower == "new" || lower == "old" {
let (row, side) = if lower == "new" {
(new_row, "NEW")
} else {
(old_row, "OLD")
};
let pos = columns
.iter()
.position(|sc| sc.name.eq_ignore_ascii_case(&c.name))
.ok_or_else(|| EvalError::ColumnNotFound {
name: format!("{side}.{}", c.name),
})?;
let v = match row {
Some(r) => r.values.get(pos).cloned().unwrap_or(Value::Null),
None => Value::Null,
};
*expr = value_to_literal_expr(columns, pos, v);
return Ok(());
}
}
}
match expr {
Expr::NamedArg { expr, .. } => substitute_new_old(expr, new_row, old_row, columns)?,
Expr::Variadic(expr) => substitute_new_old(expr, new_row, old_row, columns)?,
Expr::AggregateOrdered { call, order_by, .. } => {
substitute_new_old(call, new_row, old_row, columns)?;
for o in order_by.iter_mut() {
substitute_new_old(&mut o.expr, new_row, old_row, columns)?;
}
}
Expr::Binary { lhs, rhs, .. } => {
substitute_new_old(lhs, new_row, old_row, columns)?;
substitute_new_old(rhs, new_row, old_row, columns)?;
}
Expr::Unary { expr, .. }
| Expr::Cast { expr, .. }
| Expr::IsNull { expr, .. }
| Expr::BoolTest { expr, .. }
| Expr::FieldAccess { base: expr, .. } => {
substitute_new_old(expr, new_row, old_row, columns)?;
}
Expr::Like { expr, pattern, .. } => {
substitute_new_old(expr, new_row, old_row, columns)?;
substitute_new_old(pattern, new_row, old_row, columns)?;
}
Expr::FunctionCall { args, .. } => {
for a in args {
substitute_new_old(a, new_row, old_row, columns)?;
}
}
Expr::Extract { source, .. } => substitute_new_old(source, new_row, old_row, columns)?,
Expr::Array(items) => {
for elem in items {
substitute_new_old(elem, new_row, old_row, columns)?;
}
}
Expr::ArraySubscript { target, index } => {
substitute_new_old(target, new_row, old_row, columns)?;
substitute_new_old(index, new_row, old_row, columns)?;
}
Expr::ArraySlice { target, lo, hi } => {
substitute_new_old(target, new_row, old_row, columns)?;
if let Some(l) = lo {
substitute_new_old(l, new_row, old_row, columns)?;
}
if let Some(h) = hi {
substitute_new_old(h, new_row, old_row, columns)?;
}
}
Expr::AnyAll { expr, array, .. } => {
substitute_new_old(expr, new_row, old_row, columns)?;
substitute_new_old(array, new_row, old_row, columns)?;
}
Expr::InList { expr, list, .. } => {
substitute_new_old(expr, new_row, old_row, columns)?;
for item in list {
substitute_new_old(item, new_row, old_row, columns)?;
}
}
Expr::Case {
operand,
branches,
else_branch,
} => {
if let Some(o) = operand {
substitute_new_old(o, new_row, old_row, columns)?;
}
for (w, t) in branches {
substitute_new_old(w, new_row, old_row, columns)?;
substitute_new_old(t, new_row, old_row, columns)?;
}
if let Some(e) = else_branch {
substitute_new_old(e, new_row, old_row, columns)?;
}
}
Expr::Literal(_)
| Expr::Placeholder(_)
| Expr::Column(_)
| Expr::WindowFunction { .. }
| Expr::ScalarSubquery(_)
| Expr::Exists { .. }
| Expr::InSubquery { .. }
| Expr::RowInSubquery { .. }
| Expr::RowCmpSubquery { .. } => {}
}
Ok(())
}
fn value_to_literal_expr(_columns: &[ColumnSchema], _pos: usize, v: Value) -> Expr {
use spg_sql::ast::Literal;
let lit = match v {
Value::Null => Literal::Null,
Value::Bool(b) => Literal::Bool(b),
Value::SmallInt(n) => Literal::Integer(i64::from(n)),
Value::Int(n) => Literal::Integer(i64::from(n)),
Value::BigInt(n) => Literal::Integer(n),
Value::Float(x) => Literal::Float(x),
Value::Text(s) | Value::Json(s) => Literal::String(s.into_owned()),
other => Literal::String(format!("{other:?}")),
};
Expr::Literal(lit)
}
pub(crate) fn substitute_trigger_context_in_statement(
stmt: &mut spg_sql::ast::Statement,
new_row: Option<&Row<'static>>,
old_row: Option<&Row<'static>>,
locals: &BTreeMap<String, Value>,
columns: &[ColumnSchema],
) -> Result<(), EvalError> {
use spg_sql::ast::Statement;
let mut walk = |e: &mut Expr| -> Result<(), EvalError> {
substitute_locals(e, locals);
substitute_new_old(e, new_row, old_row, columns)?;
Ok(())
};
match stmt {
Statement::Insert(s) => {
for tuple in &mut s.rows {
for e in tuple {
walk(e)?;
}
}
}
Statement::Update(s) => {
for (_col, e) in &mut s.assignments {
walk(e)?;
}
if let Some(w) = &mut s.where_ {
walk(w)?;
}
}
Statement::Delete(s) => {
if let Some(w) = &mut s.where_ {
walk(w)?;
}
}
Statement::Select(s) => {
substitute_trigger_context_in_select(s, new_row, old_row, locals, columns)?
}
_ => {}
}
Ok(())
}
fn substitute_trigger_context_in_select(
s: &mut spg_sql::ast::SelectStatement,
new_row: Option<&Row<'static>>,
old_row: Option<&Row<'static>>,
locals: &BTreeMap<String, Value>,
columns: &[ColumnSchema],
) -> Result<(), EvalError> {
use spg_sql::ast::SelectItem;
let mut walk = |e: &mut Expr| -> Result<(), EvalError> {
substitute_locals(e, locals);
substitute_new_old(e, new_row, old_row, columns)?;
Ok(())
};
for item in &mut s.items {
if let SelectItem::Expr { expr, .. } = item {
walk(expr)?;
}
}
if let Some(w) = &mut s.where_ {
walk(w)?;
}
if let Some(group_by) = &mut s.group_by {
for g in group_by {
walk(g)?;
}
}
if let Some(h) = &mut s.having {
walk(h)?;
}
for ob in &mut s.order_by {
walk(&mut ob.expr)?;
}
let _ = &s.limit;
let _ = &s.offset;
Ok(())
}
pub fn matching_trigger_names<'a>(
triggers: &'a [TriggerDef],
table: &str,
event: &str,
timing: &str,
) -> Vec<&'a TriggerDef> {
triggers
.iter()
.filter(|t| {
t.table == table
&& t.timing.eq_ignore_ascii_case(timing)
&& t.for_each.eq_ignore_ascii_case("row")
&& t.events.iter().any(|e| e.eq_ignore_ascii_case(event))
})
.collect()
}
impl Engine {
pub(crate) fn snapshot_row_triggers(
&self,
table: &str,
event: &str,
timing: &str,
) -> Vec<(
spg_storage::FunctionDef,
alloc::string::String,
alloc::string::String,
)> {
let cat = self.active_catalog();
let mut matching: Vec<&spg_storage::TriggerDef> = cat
.triggers()
.iter()
.filter(|t| {
t.enabled
&& t.table == table
&& t.timing.eq_ignore_ascii_case(timing)
&& t.for_each.eq_ignore_ascii_case("row")
&& t.events.iter().any(|e| e.eq_ignore_ascii_case(event))
})
.collect();
matching.sort_by(|a, b| a.name.cmp(&b.name));
matching
.into_iter()
.filter_map(|t| {
cat.functions_named(&t.function)
.first()
.map(|f| ((*f).clone(), t.when_condition.clone(), t.name.clone()))
})
.collect()
}
pub(crate) fn snapshot_update_row_triggers(
&self,
table: &str,
timing: &str,
) -> Vec<(
spg_storage::FunctionDef,
Vec<String>,
alloc::string::String,
alloc::string::String,
)> {
let cat = self.active_catalog();
let mut matching: Vec<&spg_storage::TriggerDef> = cat
.triggers()
.iter()
.filter(|t| {
t.enabled
&& t.table == table
&& t.timing.eq_ignore_ascii_case(timing)
&& t.for_each.eq_ignore_ascii_case("row")
&& t.events.iter().any(|e| e.eq_ignore_ascii_case("UPDATE"))
})
.collect();
matching.sort_by(|a, b| a.name.cmp(&b.name));
matching
.into_iter()
.filter_map(|t| {
cat.functions_named(&t.function).first().map(|fd| {
(
(*fd).clone(),
t.update_columns.clone(),
t.when_condition.clone(),
t.name.clone(),
)
})
})
.collect()
}
pub(crate) fn execute_deferred_trigger_stmts(
&mut self,
deferred: Vec<DeferredEmbeddedStmt>,
cancel: CancelToken<'_>,
) -> Result<(), EngineError> {
for d in deferred {
if self.trigger_recursion_depth >= MAX_TRIGGER_RECURSION {
return Err(EngineError::Storage(StorageError::Corrupt(alloc::format!(
"trigger embedded SQL recursion depth {} exceeded (trigger function \
{:?} would push past the {} cap — check for trigger cycles)",
self.trigger_recursion_depth,
d.function,
MAX_TRIGGER_RECURSION,
))));
}
self.trigger_recursion_depth += 1;
let res = self.execute_stmt_with_cancel(d.stmt, cancel);
self.trigger_recursion_depth -= 1;
res?;
}
Ok(())
}
}