use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use vibesql_ast::{PseudoTable, TriggerEvent, TriggerGranularity, TriggerTiming};
use vibesql_catalog::{TableSchema, TriggerDefinition};
use vibesql_storage::{Database, Row};
use vibesql_types::SqlValue;
use crate::errors::ExecutorError;
#[cfg(not(target_arch = "wasm32"))]
const MAX_TRIGGER_RECURSION_DEPTH: usize = 1000;
#[cfg(target_arch = "wasm32")]
const MAX_TRIGGER_RECURSION_DEPTH: usize = 700;
#[cfg(not(target_arch = "wasm32"))]
const TRIGGER_STACK_RED_ZONE: usize = 1024 * 1024;
#[cfg(not(target_arch = "wasm32"))]
const TRIGGER_STACK_GROW_SIZE: usize = 8 * 1024 * 1024;
#[cfg(not(target_arch = "wasm32"))]
#[inline]
fn grow_stack_for_trigger<R>(f: impl FnOnce() -> R) -> R {
stacker::maybe_grow(TRIGGER_STACK_RED_ZONE, TRIGGER_STACK_GROW_SIZE, f)
}
#[cfg(target_arch = "wasm32")]
#[inline]
fn grow_stack_for_trigger<R>(f: impl FnOnce() -> R) -> R {
f()
}
fn effective_trigger_depth_limit(db: &Database) -> usize {
match db.trigger_depth_limit() {
Some(n) if n >= 1 => (n as usize).min(MAX_TRIGGER_RECURSION_DEPTH),
_ => MAX_TRIGGER_RECURSION_DEPTH,
}
}
thread_local! {
static TRIGGER_RECURSION_DEPTH: Cell<usize> = const { Cell::new(0) };
}
struct RecursionGuard;
impl RecursionGuard {
fn new(db: &Database) -> Result<Self, ExecutorError> {
let limit = effective_trigger_depth_limit(db);
TRIGGER_RECURSION_DEPTH.with(|depth| {
let current = depth.get();
if current >= limit {
Err(ExecutorError::Other(
"too many levels of trigger recursion".to_string(),
))
} else {
depth.set(current + 1);
Ok(RecursionGuard)
}
})
}
}
impl Drop for RecursionGuard {
fn drop(&mut self) {
TRIGGER_RECURSION_DEPTH.with(|depth| {
depth.set(depth.get().saturating_sub(1));
});
}
}
thread_local! {
static ACTIVE_TRIGGERS: RefCell<HashMap<String, usize>> =
RefCell::new(HashMap::new());
}
struct ActiveTriggerGuard {
key: String,
}
impl ActiveTriggerGuard {
fn new(trigger_name: &str) -> Self {
let key = trigger_name.to_lowercase();
ACTIVE_TRIGGERS.with(|active| {
*active.borrow_mut().entry(key.clone()).or_insert(0) += 1;
});
ActiveTriggerGuard { key }
}
}
impl Drop for ActiveTriggerGuard {
fn drop(&mut self) {
ACTIVE_TRIGGERS.with(|active| {
let mut map = active.borrow_mut();
if let Some(count) = map.get_mut(&self.key) {
*count -= 1;
if *count == 0 {
map.remove(&self.key);
}
}
});
}
}
fn is_trigger_active(trigger_name: &str) -> bool {
let key = trigger_name.to_lowercase();
ACTIVE_TRIGGERS.with(|active| active.borrow().get(&key).is_some_and(|&c| c > 0))
}
pub struct TriggerContext<'a> {
pub old_row: Option<&'a Row>,
pub new_row: Option<&'a Row>,
pub table_schema: &'a TableSchema,
pub is_view: bool,
}
impl<'a> TriggerContext<'a> {
pub fn resolve_pseudo_var(
&self,
pseudo_table: PseudoTable,
column: &str,
) -> Result<SqlValue, ExecutorError> {
let row = match pseudo_table {
PseudoTable::Old => self.old_row.ok_or_else(|| {
ExecutorError::UnsupportedExpression(
"OLD pseudo-variable not available in this trigger context".to_string(),
)
})?,
PseudoTable::New => self.new_row.ok_or_else(|| {
ExecutorError::UnsupportedExpression(
"NEW pseudo-variable not available in this trigger context".to_string(),
)
})?,
};
if let Some(col_idx) = self.table_schema.get_column_index(column) {
return Ok(row.values[col_idx].clone());
}
let column_lower = column.to_lowercase();
if column_lower == "rowid" || column_lower == "_rowid_" || column_lower == "oid" {
if !self.is_view && !self.table_schema.without_rowid {
if let Some(ipk_col_idx) = self.table_schema.rowid_alias_column {
return row
.values
.get(ipk_col_idx)
.cloned()
.ok_or(ExecutorError::ColumnIndexOutOfBounds { index: ipk_col_idx });
}
return Ok(SqlValue::Bigint(row.row_id.map(|id| id as i64).unwrap_or(-1)));
}
}
Err(ExecutorError::ColumnNotFound {
column_name: column.to_string(),
table_name: self.table_schema.name.clone(),
searched_tables: vec![self.table_schema.name.clone()],
available_columns: self
.table_schema
.columns
.iter()
.map(|c| c.name.clone())
.collect(),
})
}
}
#[must_use = "a TriggerOutcome::SkipRow must be honored (skip the row); discarding it silently applies a RAISE(IGNORE)'d row"]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TriggerOutcome {
Proceed,
SkipRow,
}
pub struct TriggerFirer;
impl TriggerFirer {
pub fn find_triggers(
db: &Database,
table_name: &str,
timing: TriggerTiming,
event: TriggerEvent,
) -> Vec<TriggerDefinition> {
Self::find_triggers_in_schema(db, table_name, timing, event, None)
}
pub fn find_triggers_in_schema(
db: &Database,
table_name: &str,
timing: TriggerTiming,
event: TriggerEvent,
dml_schema: Option<&str>,
) -> Vec<TriggerDefinition> {
db.catalog
.get_triggers_for_table_in_schema(table_name, Some(event.clone()), dml_schema)
.filter(|trigger| trigger.timing == timing && trigger.enabled) .cloned()
.collect()
}
fn should_fire_update_of(
trigger: &TriggerDefinition,
old_row: &Row,
new_row: &Row,
table_schema: &TableSchema,
) -> bool {
match &trigger.event {
TriggerEvent::Update(Some(columns)) => {
for col_name in columns {
if let Some(col_idx) =
table_schema.columns.iter().position(|c| &c.name == col_name)
{
if col_idx < old_row.values.len()
&& col_idx < new_row.values.len()
&& old_row.values[col_idx] != new_row.values[col_idx]
{
return true; }
}
}
false }
_ => true, }
}
pub fn execute_trigger(
db: &mut Database,
trigger: &TriggerDefinition,
old_row: Option<&Row>,
new_row: Option<&Row>,
) -> Result<TriggerOutcome, ExecutorError> {
grow_stack_for_trigger(|| Self::execute_trigger_inner(db, trigger, old_row, new_row))
}
fn execute_trigger_inner(
db: &mut Database,
trigger: &TriggerDefinition,
old_row: Option<&Row>,
new_row: Option<&Row>,
) -> Result<TriggerOutcome, ExecutorError> {
if let Some(when_expr) = &trigger.when_condition {
let condition_result = Self::evaluate_when_condition(
db,
&trigger.table_name,
when_expr,
old_row,
new_row,
)?;
if !condition_result {
return Ok(TriggerOutcome::Proceed);
}
}
let _active = ActiveTriggerGuard::new(&trigger.name);
Self::execute_trigger_action(db, trigger, old_row, new_row)
}
fn evaluate_when_condition(
db: &Database,
table_name: &str,
when_expr: &vibesql_ast::Expression,
old_row: Option<&Row>,
new_row: Option<&Row>,
) -> Result<bool, ExecutorError> {
let schema = Self::resolve_trigger_schema(db, table_name)?;
let is_view = db.catalog.get_table(table_name).is_none();
let row = new_row.or(old_row).ok_or_else(|| {
ExecutorError::UnsupportedExpression(
"WHEN condition requires a row context".to_string(),
)
})?;
let trigger_context = TriggerContext { old_row, new_row, table_schema: &schema, is_view };
let evaluator =
crate::ExpressionEvaluator::with_trigger_context(&schema, db, &trigger_context);
let result = evaluator.eval(when_expr, row)?;
match result {
vibesql_types::SqlValue::Boolean(b) => Ok(b),
vibesql_types::SqlValue::Null => Ok(false),
_ => Err(ExecutorError::UnsupportedExpression(
"WHEN condition must evaluate to boolean".to_string(),
)),
}
}
fn execute_trigger_action(
db: &mut Database,
trigger: &TriggerDefinition,
old_row: Option<&Row>,
new_row: Option<&Row>,
) -> Result<TriggerOutcome, ExecutorError> {
let sql = match &trigger.triggered_action {
vibesql_ast::TriggerAction::RawSql(sql) => sql.clone(),
};
let statements = Self::parse_trigger_sql(&sql)?;
let schema = Self::resolve_trigger_schema(db, &trigger.table_name)?;
let is_view = db.catalog.get_table(&trigger.table_name).is_none();
let trigger_context = TriggerContext { old_row, new_row, table_schema: &schema, is_view };
for statement in statements {
match Self::execute_statement(db, &statement, &trigger_context) {
Ok(()) => {}
Err(ExecutorError::RaiseIgnore) => return Ok(TriggerOutcome::SkipRow),
Err(e) => return Err(e),
}
}
Ok(TriggerOutcome::Proceed)
}
fn resolve_trigger_schema(
db: &Database,
table_name: &str,
) -> Result<TableSchema, ExecutorError> {
if let Some(table_schema) = db.catalog.get_table(table_name) {
Ok(table_schema.clone())
} else if let Some(view_def) = db.catalog.get_view(table_name) {
Self::build_view_schema(db, view_def)
} else {
Err(ExecutorError::TableNotFound(table_name.to_string()))
}
}
fn build_view_schema(
db: &Database,
view_def: &vibesql_catalog::ViewDefinition,
) -> Result<TableSchema, ExecutorError> {
let select_executor = crate::SelectExecutor::new(db);
let result = select_executor.execute_with_columns(&view_def.query)?;
let column_names: Vec<String> = if let Some(ref cols) = view_def.columns {
cols.clone()
} else {
result.columns.clone()
};
let columns: Vec<vibesql_catalog::ColumnSchema> = column_names
.into_iter()
.map(|name| {
vibesql_catalog::ColumnSchema::new(
name,
vibesql_types::DataType::Varchar { max_length: None },
true,
)
})
.collect();
Ok(TableSchema::new(view_def.name.clone(), columns))
}
pub fn parse_trigger_sql(sql: &str) -> Result<Vec<vibesql_ast::Statement>, ExecutorError> {
let mut statements = Vec::new();
for stmt_sql in vibesql_parser::split_trigger_body_statements(sql) {
match vibesql_parser::Parser::parse_sql_in_trigger_body(&stmt_sql) {
Ok(stmt) => statements.push(stmt),
Err(e) => {
return Err(ExecutorError::UnsupportedExpression(format!(
"Failed to parse trigger SQL: {}",
e.message
)))
}
}
}
Ok(statements)
}
fn execute_statement(
db: &mut Database,
statement: &vibesql_ast::Statement,
trigger_context: &TriggerContext,
) -> Result<(), ExecutorError> {
use vibesql_ast::Statement;
match statement {
Statement::Insert(insert_stmt) => {
crate::insert::execute_insert_with_trigger_context(
db,
insert_stmt,
trigger_context,
)?;
Ok(())
}
Statement::Update(update_stmt) => {
crate::update::execute_update_with_trigger_context(
db,
update_stmt,
trigger_context,
)?;
Ok(())
}
Statement::Delete(delete_stmt) => {
crate::delete::execute_delete_with_trigger_context(
db,
delete_stmt,
trigger_context,
)?;
Ok(())
}
Statement::Select(select_stmt) => {
let executor =
crate::SelectExecutor::new_with_trigger_context(db, trigger_context);
executor.execute_with_columns(select_stmt)?;
Ok(())
}
_ => Err(ExecutorError::UnsupportedExpression(format!(
"Statement type not supported in triggers: {:?}",
statement
))),
}
}
fn suppressed_by_recursive_triggers_off(db: &Database, trigger: &TriggerDefinition) -> bool {
!db.recursive_triggers() && is_trigger_active(&trigger.name)
}
pub fn execute_before_triggers(
db: &mut Database,
table_name: &str,
event: TriggerEvent,
old_row: Option<&Row>,
new_row: Option<&Row>,
) -> Result<TriggerOutcome, ExecutorError> {
Self::execute_before_triggers_in_schema(db, table_name, event, old_row, new_row, None)
}
pub fn execute_before_triggers_in_schema(
db: &mut Database,
table_name: &str,
event: TriggerEvent,
old_row: Option<&Row>,
new_row: Option<&Row>,
dml_schema: Option<&str>,
) -> Result<TriggerOutcome, ExecutorError> {
let _guard = RecursionGuard::new(db)?;
let triggers =
Self::find_triggers_in_schema(db, table_name, TriggerTiming::Before, event, dml_schema);
let table_schema = db
.catalog
.get_table(table_name)
.ok_or_else(|| ExecutorError::TableNotFound(table_name.to_string()))?
.clone();
for trigger in triggers {
if trigger.granularity == TriggerGranularity::Row {
if Self::suppressed_by_recursive_triggers_off(db, &trigger) {
continue;
}
if let (Some(old), Some(new)) = (old_row, new_row) {
if !Self::should_fire_update_of(&trigger, old, new, &table_schema) {
continue; }
}
if Self::execute_trigger(db, &trigger, old_row, new_row)?
== TriggerOutcome::SkipRow
{
return Ok(TriggerOutcome::SkipRow);
}
}
}
Ok(TriggerOutcome::Proceed)
}
pub fn execute_before_statement_triggers(
db: &mut Database,
table_name: &str,
event: TriggerEvent,
) -> Result<TriggerOutcome, ExecutorError> {
Self::execute_before_statement_triggers_in_schema(db, table_name, event, None)
}
pub fn execute_before_statement_triggers_in_schema(
db: &mut Database,
table_name: &str,
event: TriggerEvent,
dml_schema: Option<&str>,
) -> Result<TriggerOutcome, ExecutorError> {
let _guard = RecursionGuard::new(db)?;
let triggers =
Self::find_triggers_in_schema(db, table_name, TriggerTiming::Before, event, dml_schema);
for trigger in triggers {
if trigger.granularity == TriggerGranularity::Statement {
if Self::suppressed_by_recursive_triggers_off(db, &trigger) {
continue;
}
if Self::execute_trigger(db, &trigger, None, None)? == TriggerOutcome::SkipRow {
return Ok(TriggerOutcome::SkipRow);
}
}
}
Ok(TriggerOutcome::Proceed)
}
pub fn execute_after_triggers(
db: &mut Database,
table_name: &str,
event: TriggerEvent,
old_row: Option<&Row>,
new_row: Option<&Row>,
) -> Result<TriggerOutcome, ExecutorError> {
Self::execute_after_triggers_in_schema(db, table_name, event, old_row, new_row, None)
}
pub fn execute_after_triggers_in_schema(
db: &mut Database,
table_name: &str,
event: TriggerEvent,
old_row: Option<&Row>,
new_row: Option<&Row>,
dml_schema: Option<&str>,
) -> Result<TriggerOutcome, ExecutorError> {
let _guard = RecursionGuard::new(db)?;
let triggers =
Self::find_triggers_in_schema(db, table_name, TriggerTiming::After, event, dml_schema);
let table_schema = db
.catalog
.get_table(table_name)
.ok_or_else(|| ExecutorError::TableNotFound(table_name.to_string()))?
.clone();
for trigger in triggers {
if trigger.granularity == TriggerGranularity::Row {
if Self::suppressed_by_recursive_triggers_off(db, &trigger) {
continue;
}
if let (Some(old), Some(new)) = (old_row, new_row) {
if !Self::should_fire_update_of(&trigger, old, new, &table_schema) {
continue; }
}
if Self::execute_trigger(db, &trigger, old_row, new_row)?
== TriggerOutcome::SkipRow
{
return Ok(TriggerOutcome::SkipRow);
}
}
}
Ok(TriggerOutcome::Proceed)
}
pub fn execute_after_statement_triggers(
db: &mut Database,
table_name: &str,
event: TriggerEvent,
) -> Result<TriggerOutcome, ExecutorError> {
Self::execute_after_statement_triggers_in_schema(db, table_name, event, None)
}
pub fn execute_after_statement_triggers_in_schema(
db: &mut Database,
table_name: &str,
event: TriggerEvent,
dml_schema: Option<&str>,
) -> Result<TriggerOutcome, ExecutorError> {
let _guard = RecursionGuard::new(db)?;
let triggers =
Self::find_triggers_in_schema(db, table_name, TriggerTiming::After, event, dml_schema);
for trigger in triggers {
if trigger.granularity == TriggerGranularity::Statement {
if Self::suppressed_by_recursive_triggers_off(db, &trigger) {
continue;
}
if Self::execute_trigger(db, &trigger, None, None)? == TriggerOutcome::SkipRow {
return Ok(TriggerOutcome::SkipRow);
}
}
}
Ok(TriggerOutcome::Proceed)
}
}