uqa-execution 0.5.0

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

//! Block execution and subtransaction-backed exception handling.

use super::{arm_matches, routine_message, Flow, Interpreter, PLpgSQLBlock, PLpgSQLStmt, SQLError};

impl Interpreter<'_> {
    /// Run one block, routing failures through its EXCEPTION arms.
    pub(super) fn exec_block(&mut self, block: &PLpgSQLBlock) -> Result<Flow, SQLError> {
        let mut names = Vec::with_capacity(block.initvarnos.len());
        let result = self
            .initialize_block(block, &mut names)
            .and_then(|()| self.exec_initialized_block(block));
        for name in names.iter().rev() {
            self.pop_binding(name);
        }
        result
    }

    fn initialize_block(
        &mut self,
        block: &PLpgSQLBlock,
        names: &mut Vec<String>,
    ) -> Result<(), SQLError> {
        for index in &block.initvarnos {
            // Re-entry resets values but does not discard prepared expressions.
            self.values[*index] = super::Value::Null;
            self.record_types.remove(index);
            if let super::PLpgSQLDatum::Var(variable) = &self.datums[*index] {
                let (value, source) = match &variable.default {
                    Some(expression) => self.eval_expr_with_type(expression)?,
                    None => (super::Value::Null, None),
                };
                let value = super::coerce_routine_value_from(
                    self.services.expressions,
                    &value,
                    &variable.type_name,
                    source.as_ref(),
                )?;
                if variable.not_null && matches!(value, super::Value::Null) {
                    return Err(SQLError::Routine {
                        sqlstate: "22004".into(),
                        message: format!(
                            "null value cannot be assigned to variable \"{}\" declared NOT NULL",
                            variable.name
                        ),
                    });
                }
                self.values[*index] = value;
            }
            // A declaration's DEFAULT sees preceding declarations and the
            // enclosing scope, before this declaration starts shadowing it.
            // Cursor arguments occur in initvarnos but their namespace is
            // visible only while preparing that cursor's query.
            if !self.cursor_arguments.contains(index) {
                let name = self.datum_name(*index)?;
                self.push_binding(&name, *index);
                names.push(name);
            }
        }
        Ok(())
    }

    fn exec_initialized_block(&mut self, block: &PLpgSQLBlock) -> Result<Flow, SQLError> {
        let result = if block.exceptions.is_empty() {
            self.exec_stmts(&block.body)
        } else {
            self.exec_exception_block(block)
        };
        match result {
            Ok(Flow::Exit(Some(label))) if block.label.as_deref() == Some(label.as_str()) => {
                Ok(Flow::Normal)
            }
            other => other,
        }
    }

    /// `PostgreSQL` executes the guarded body of a block with `EXCEPTION`
    /// inside a subtransaction. Database changes made before an error are
    /// rolled back before its handler runs, while PL/pgSQL datum values stay
    /// unchanged. The engine's nested transaction frame provides those same
    /// memory-snapshot and persistent-backend savepoint semantics.
    pub(super) fn exec_exception_block(&mut self, block: &PLpgSQLBlock) -> Result<Flow, SQLError> {
        if self.services.transactions.depth() == 0 {
            return Err(SQLError::Internal(
                "PL/pgSQL exception block executed outside a statement transaction".into(),
            ));
        }
        self.services.transactions.begin()?;
        match self.exec_stmts(&block.body) {
            Ok(flow) => {
                self.services.transactions.commit()?;
                Ok(flow)
            }
            Err(error) => {
                if let Err(rollback_error) = self.services.transactions.rollback() {
                    return Err(SQLError::Internal(format!(
                        "PL/pgSQL exception-block rollback failed: {rollback_error}; original error: {error}"
                    )));
                }
                // A termination of the session is reported at FATAL, which no handler catches.
                if matches!(&error, SQLError::Cancelled(cancelled) if cancelled.reason.terminates_session())
                {
                    return Err(error);
                }
                let state = error
                    .sqlstate()
                    .ok_or_else(|| {
                        SQLError::Internal(format!(
                            "caught PL/pgSQL error has no SQLSTATE: {error}"
                        ))
                    })?
                    .to_string();
                let message = routine_message(&error);
                let mut arm = None;
                for candidate in &block.exceptions {
                    if arm_matches(&candidate.conditions, &state)? {
                        arm = Some(candidate);
                        break;
                    }
                }
                match arm {
                    Some(arm) => {
                        // A handler that catches a cancellation consumes it, as PostgreSQL reports a cancel or timeout interrupt once.
                        if let SQLError::Cancelled(cancelled) = &error {
                            self.services.runtime.cancellation.clear(cancelled.reason);
                        }
                        self.err_stack.push(super::CaughtError {
                            diagnostics: (state, message),
                            cause: error,
                        });
                        let handled = self.exec_stmts(&arm.body);
                        self.err_stack.pop();
                        handled
                    }
                    None => Err(error),
                }
            }
        }
    }

    pub(super) fn exec_stmts(&mut self, stmts: &[PLpgSQLStmt]) -> Result<Flow, SQLError> {
        for stmt in stmts {
            match self.exec_stmt(stmt)? {
                Flow::Normal => {}
                flow => return Ok(flow),
            }
        }
        Ok(Flow::Normal)
    }
}