uqa-execution 0.4.9

Volcano physical operators with row-batch pipelines
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

//! The row triggers a statement fires, resolved once for each table and event it writes.

use std::{cell::RefCell, collections::HashMap, rc::Rc};

use uqa_sql::{
    ast::{TriggerEvent, TriggerTiming},
    catalog::events::StoredTrigger,
    error::Result,
};

use super::context::TriggerContext;

/// The triggers of one table for a timing, an event and the columns an update names.
struct Resolved {
    timing: TriggerTiming,
    event: TriggerEvent,
    updated_columns: Vec<String>,
    triggers: Rc<[StoredTrigger]>,
}

thread_local! {
    /// What each statement in progress on this thread has resolved for the tables it writes, innermost statement last.
    static STATEMENTS: RefCell<Vec<HashMap<String, Vec<Resolved>>>> = const { RefCell::new(Vec::new()) };
}

pub(super) fn enter() {
    STATEMENTS.with(|statements| statements.borrow_mut().push(HashMap::new()));
}

pub(super) fn leave() {
    STATEMENTS.with(|statements| {
        let removed = statements.borrow_mut().pop();
        debug_assert!(removed.is_some(), "row trigger scope stack underflow");
    });
}

/// The row triggers of `table` for `timing` and `event`, whatever replication role fires them.
///
/// Resolving them names the table, walks its partition ancestors and copies the definitions, and a statement asks before and after every row it writes. `PostgreSQL` copies a relation's trigger descriptor when a statement begins to write the relation (`InitResultRelInfo`), so a statement fires the triggers that existed then: one created while it runs waits for the next statement, and one dropped while it runs still fires for the rest of it. A statement therefore resolves once for each table, timing, event and set of updated columns. Outside a statement every call resolves.
pub(super) fn resolve(
    context: &TriggerContext<'_>,
    table: &str,
    timing: TriggerTiming,
    event: TriggerEvent,
    updated_columns: &[String],
) -> Result<Rc<[StoredTrigger]>> {
    let known = STATEMENTS.with(|statements| {
        let statements = statements.borrow();
        statements
            .last()?
            .get(table)?
            .iter()
            .find(|resolved| {
                resolved.timing == timing
                    && resolved.event == event
                    && resolved.updated_columns == updated_columns
            })
            .map(|resolved| Rc::clone(&resolved.triggers))
    });
    if let Some(triggers) = known {
        return Ok(triggers);
    }
    let triggers: Rc<[StoredTrigger]> = context
        .catalog
        .row_trigger_definitions(table, timing, event, updated_columns)?
        .into();
    STATEMENTS.with(|statements| {
        if let Some(statement) = statements.borrow_mut().last_mut() {
            statement
                .entry(table.to_owned())
                .or_default()
                .push(Resolved {
                    timing,
                    event,
                    updated_columns: updated_columns.to_vec(),
                    triggers: Rc::clone(&triggers),
                });
        }
    });
    Ok(triggers)
}

/// Whether `trigger` fires in the session's replication role as it stands now. `PostgreSQL` reads the role at each firing (`TriggerEnabled`), so a trigger that changes it decides for every trigger after it, of its own row as well.
pub(super) fn fires(context: &TriggerContext<'_>, trigger: &StoredTrigger) -> bool {
    if context.referrers.session_replication_role_is_replica() {
        trigger.enabled.fires_in_replica()
    } else {
        trigger.enabled.fires_in_origin()
    }
}