Skip to main content

radixdb_executor/procedural/
host.rs

1use std::collections::BTreeMap;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::Arc;
4
5use radixdb_catalog::{ObjectId, ObjectKind, SecurityMode, Volatility};
6use radixdb_core::{Error, Row, Value};
7use radixdb_procedural::{
8    admit_embedded_sql, AuditEvent, AuditHost, BudgetOwner, CancellationProbe, CursorHost,
9    CursorToken, Diagnostic, DiagnosticKind, Interpreter, OutboxHost, OutboxMessage,
10    PrincipalContext, PrincipalHost, ProceduralResult, RoutineCallHost, RuntimeValue,
11    SavepointToken, SqlHost, SqlOutcome, SqlRowSink, TransactionHost,
12};
13use radixdb_sql::{
14    parse_sql, walk_statement_physical_table_sources, walk_statement_tree, Expression,
15    InfixExpression, InfixOperator, Position, Statement, Token, TokenType,
16};
17use radixdb_storage::traits::QueryResult;
18
19use crate::context::{CancellationHandle, ExecutionContext, TimeoutGuard};
20use crate::expression::ExpressionEval;
21use crate::Executor;
22
23use super::error::map_executor_error;
24use super::function::ExecutorStoredFunctionInvoker;
25use super::load_published_routine;
26use super::value::{runtime_row, scalar_parameters, scalar_value};
27
28static NEXT_HOST_RESOURCE_ID: AtomicU64 = AtomicU64::new(1);
29
30struct CursorEntry {
31    result: Box<dyn QueryResult>,
32    _timeout: Option<TimeoutGuard>,
33}
34
35pub(super) struct ExecutorProceduralHost<'a> {
36    executor: &'a Executor,
37    context: ExecutionContext,
38    principals: PrincipalContext,
39    definers: Vec<ObjectId>,
40    cursors: BTreeMap<u64, CursorEntry>,
41    savepoints: BTreeMap<u64, String>,
42    function_volatility: Option<Volatility>,
43}
44
45impl<'a> ExecutorProceduralHost<'a> {
46    pub(super) fn new(
47        executor: &'a Executor,
48        context: &ExecutionContext,
49        principals: PrincipalContext,
50        function_volatility: Option<Volatility>,
51    ) -> Self {
52        Self {
53            executor,
54            context: context.clone(),
55            principals,
56            definers: Vec::new(),
57            cursors: BTreeMap::new(),
58            savepoints: BTreeMap::new(),
59            function_volatility,
60        }
61    }
62
63    fn sql_context(
64        &self,
65        parameters: &[RuntimeValue],
66        budget: &BudgetOwner,
67    ) -> ProceduralResult<ExecutionContext> {
68        budget.check_boundary()?;
69        self.context.check_cancelled().map_err(map_executor_error)?;
70        let mut context = self.context.clone();
71        context.set_params(scalar_parameters(parameters)?);
72        let remaining = budget.remaining_deadline()?;
73        let remaining_ms = u64::try_from(remaining.as_millis())
74            .unwrap_or(u64::MAX)
75            .max(1);
76        context.set_timeout_ms(remaining_ms);
77        context = context.with_procedural_budget(budget.clone());
78        context = context.with_effective_principal_id(self.principal_context().effective_principal);
79        let invoker = Arc::new(ExecutorStoredFunctionInvoker::new_with_principals(
80            self.executor,
81            &context,
82            self.principal_context(),
83            self.function_volatility,
84        ));
85        context = context.with_stored_function_invoker(invoker);
86        Ok(context)
87    }
88
89    fn execute_result(
90        &self,
91        statement: &Statement,
92        parameters: &[RuntimeValue],
93        budget: &BudgetOwner,
94    ) -> ProceduralResult<(Box<dyn QueryResult>, Option<TimeoutGuard>)> {
95        admit_embedded_sql(statement)?;
96        let context = self.sql_context(parameters, budget)?;
97        let timeout = TimeoutGuard::new(&context);
98        let result = self
99            .executor
100            .execute_statement(statement, &context)
101            .map_err(|error| map_controlled_error(error, budget))?;
102        Ok((result, timeout))
103    }
104
105    fn check_statement_capability(&self, statement: &Statement) -> ProceduralResult<()> {
106        let Some(caller) = self.function_volatility else {
107            return Ok(());
108        };
109        if caller != Volatility::Volatile
110            && matches!(
111                statement,
112                Statement::Insert(_) | Statement::Update(_) | Statement::Delete(_)
113            )
114        {
115            return Err(Diagnostic::new(
116                DiagnosticKind::VerifyCapabilityDenied,
117                "IMMUTABLE and STABLE functions cannot execute DML",
118            ));
119        }
120        if caller != Volatility::Volatile && matches!(statement, Statement::Call(_)) {
121            return Err(Diagnostic::new(
122                DiagnosticKind::VerifyCapabilityDenied,
123                "IMMUTABLE and STABLE functions cannot call procedures",
124            ));
125        }
126        if caller == Volatility::Immutable {
127            let mut reads_relation = false;
128            walk_statement_physical_table_sources(statement, &mut |_| reads_relation = true);
129            if reads_relation {
130                return Err(Diagnostic::new(
131                    DiagnosticKind::VerifyCapabilityDenied,
132                    "IMMUTABLE functions cannot read tables or views",
133                ));
134            }
135        }
136        let mut denied = None;
137        walk_statement_tree(statement, &mut |expression| {
138            let Expression::FunctionCall(function) = expression else {
139                return;
140            };
141            let Some(info) = self.executor.function_registry.get_info(&function.function) else {
142                return;
143            };
144            let target = match info.volatility {
145                radixdb_functions::FunctionVolatility::Immutable => Volatility::Immutable,
146                radixdb_functions::FunctionVolatility::Stable => Volatility::Stable,
147                radixdb_functions::FunctionVolatility::Volatile => Volatility::Volatile,
148            };
149            if !volatility_allows(caller, target) {
150                denied = Some(Diagnostic::new(
151                    DiagnosticKind::VerifyCapabilityDenied,
152                    format!(
153                        "{caller:?} function cannot invoke {target:?} built-in {}",
154                        function.function
155                    ),
156                ));
157            }
158        });
159        if let Some(error) = denied {
160            return Err(error);
161        }
162        Ok(())
163    }
164
165    fn evaluate(
166        &self,
167        expression: &Expression,
168        context: &ExecutionContext,
169    ) -> ProceduralResult<Value> {
170        ExpressionEval::compile(expression, &[])
171            .map_err(map_executor_error)?
172            .with_context(context)
173            .eval_slice(&Row::new())
174            .map_err(map_executor_error)
175    }
176}
177
178fn volatility_allows(caller: Volatility, target: Volatility) -> bool {
179    match caller {
180        Volatility::Volatile => true,
181        Volatility::Stable => target != Volatility::Volatile,
182        Volatility::Immutable => target == Volatility::Immutable,
183    }
184}
185
186impl CancellationProbe for CancellationHandle {
187    fn is_cancelled(&self) -> bool {
188        CancellationHandle::is_cancelled(self)
189    }
190}
191
192impl SqlHost for ExecutorProceduralHost<'_> {
193    fn evaluate_expression(
194        &mut self,
195        expression: &Expression,
196        parameters: &[RuntimeValue],
197        budget: &BudgetOwner,
198    ) -> ProceduralResult<RuntimeValue> {
199        let context = self.sql_context(parameters, budget)?;
200        let value = self.evaluate(expression, &context)?;
201        budget.check_boundary()?;
202        Ok(RuntimeValue::scalar(value))
203    }
204
205    fn evaluate_binary(
206        &mut self,
207        operator: InfixOperator,
208        left: &RuntimeValue,
209        right: &RuntimeValue,
210        budget: &BudgetOwner,
211    ) -> ProceduralResult<RuntimeValue> {
212        let spelling = operator_spelling(operator).ok_or_else(|| {
213            Diagnostic::new(
214                DiagnosticKind::VerifyCapabilityDenied,
215                "unbound SQL binary operator reached the executor bridge",
216            )
217        })?;
218        let token = Token::new(TokenType::Operator, spelling, Position::default());
219        let expression = Expression::Infix(InfixExpression::new(
220            token,
221            Box::new(Expression::BoundValue(Box::new(scalar_value(left)?))),
222            spelling,
223            Box::new(Expression::BoundValue(Box::new(scalar_value(right)?))),
224        ));
225        let context = self.sql_context(&[], budget)?;
226        let value = self.evaluate(&expression, &context)?;
227        budget.check_boundary()?;
228        Ok(RuntimeValue::scalar(value))
229    }
230
231    fn execute_sql(
232        &mut self,
233        statement: &Statement,
234        parameters: &[RuntimeValue],
235        rows: &mut dyn SqlRowSink,
236        budget: &BudgetOwner,
237    ) -> ProceduralResult<SqlOutcome> {
238        self.check_statement_capability(statement)?;
239        let (mut result, _timeout) = self.execute_result(statement, parameters, budget)?;
240        let affected_rows = u64::try_from(result.rows_affected()).unwrap_or(0);
241        while result.next() {
242            if let Err(error) = budget.check_boundary() {
243                let _ = result.close();
244                return Err(error);
245            }
246            if let Err(error) = rows.push_row(runtime_row(result.take_row())) {
247                let _ = result.close();
248                return Err(error);
249            }
250        }
251        if let Some(error) = result.last_error() {
252            let _ = result.close();
253            return Err(map_controlled_error(error, budget));
254        }
255        result.close().map_err(map_executor_error)?;
256        budget.check_boundary()?;
257        Ok(SqlOutcome { affected_rows })
258    }
259
260    fn execute_dynamic_sql(
261        &mut self,
262        source: &str,
263        parameters: &[RuntimeValue],
264        rows: &mut dyn SqlRowSink,
265        budget: &BudgetOwner,
266    ) -> ProceduralResult<SqlOutcome> {
267        budget.check_boundary()?;
268        let mut statements = parse_sql(source).map_err(|error| {
269            Diagnostic::new(DiagnosticKind::ParseExpectedToken, error.to_string())
270        })?;
271        if statements.len() != 1 {
272            return Err(Diagnostic::new(
273                DiagnosticKind::ParseUnsupportedSyntax,
274                "dynamic SQL must contain exactly one statement",
275            ));
276        }
277        let statement = statements.remove(0);
278        admit_embedded_sql(&statement)?;
279        self.check_statement_capability(&statement)?;
280        self.execute_sql(&statement, parameters, rows, budget)
281    }
282}
283
284impl CursorHost for ExecutorProceduralHost<'_> {
285    fn open_cursor(
286        &mut self,
287        statement: &Statement,
288        parameters: &[RuntimeValue],
289        budget: &BudgetOwner,
290    ) -> ProceduralResult<CursorToken> {
291        let (result, timeout) = self.execute_result(statement, parameters, budget)?;
292        let token = NEXT_HOST_RESOURCE_ID.fetch_add(1, Ordering::Relaxed);
293        self.cursors.insert(
294            token,
295            CursorEntry {
296                result,
297                _timeout: timeout,
298            },
299        );
300        Ok(CursorToken(token))
301    }
302
303    fn fetch_cursor(
304        &mut self,
305        cursor: CursorToken,
306        budget: &BudgetOwner,
307    ) -> ProceduralResult<Option<Vec<RuntimeValue>>> {
308        budget.check_boundary()?;
309        let entry = self.cursors.get_mut(&cursor.0).ok_or_else(|| {
310            Diagnostic::new(DiagnosticKind::RuntimeInvalidState, "cursor is not open")
311        })?;
312        if entry.result.next() {
313            budget.charge_rows(1)?;
314            return Ok(Some(runtime_row(entry.result.take_row())));
315        }
316        if let Some(error) = entry.result.last_error() {
317            return Err(map_controlled_error(error, budget));
318        }
319        Ok(None)
320    }
321
322    fn close_cursor(&mut self, cursor: CursorToken) -> ProceduralResult<()> {
323        let mut entry = self.cursors.remove(&cursor.0).ok_or_else(|| {
324            Diagnostic::new(DiagnosticKind::RuntimeInvalidState, "cursor is not open")
325        })?;
326        entry.result.close().map_err(map_executor_error)
327    }
328}
329
330impl TransactionHost for ExecutorProceduralHost<'_> {
331    fn create_savepoint(&mut self) -> ProceduralResult<SavepointToken> {
332        let token = NEXT_HOST_RESOURCE_ID.fetch_add(1, Ordering::Relaxed);
333        let name = format!("\0radixdb-procedural-{token}");
334        self.executor
335            .create_active_savepoint(&name)
336            .map_err(map_executor_error)?;
337        self.savepoints.insert(token, name);
338        Ok(SavepointToken(token))
339    }
340
341    fn rollback_savepoint(&mut self, savepoint: SavepointToken) -> ProceduralResult<()> {
342        let name = self.savepoints.get(&savepoint.0).ok_or_else(|| {
343            Diagnostic::new(
344                DiagnosticKind::RuntimeInvalidState,
345                "procedural savepoint is not active",
346            )
347        })?;
348        self.executor
349            .rollback_active_to_savepoint(name)
350            .map_err(map_executor_error)
351    }
352
353    fn release_savepoint(&mut self, savepoint: SavepointToken) -> ProceduralResult<()> {
354        let name = self.savepoints.remove(&savepoint.0).ok_or_else(|| {
355            Diagnostic::new(
356                DiagnosticKind::RuntimeInvalidState,
357                "procedural savepoint is not active",
358            )
359        })?;
360        self.executor
361            .release_active_savepoint(&name)
362            .map_err(map_executor_error)
363    }
364}
365
366impl PrincipalHost for ExecutorProceduralHost<'_> {
367    fn principal_context(&self) -> PrincipalContext {
368        let mut context = self.principals;
369        context.effective_principal = self
370            .definers
371            .last()
372            .copied()
373            .unwrap_or(context.effective_principal);
374        context
375    }
376
377    fn push_definer(&mut self, owner: ObjectId) -> ProceduralResult<()> {
378        self.definers.push(owner);
379        Ok(())
380    }
381
382    fn pop_definer(&mut self) -> ProceduralResult<()> {
383        self.definers.pop().map(|_| ()).ok_or_else(|| {
384            Diagnostic::new(
385                DiagnosticKind::RuntimeInvalidState,
386                "definer stack is empty",
387            )
388        })
389    }
390}
391
392impl RoutineCallHost for ExecutorProceduralHost<'_> {
393    fn call_routine(
394        &mut self,
395        routine: ObjectId,
396        arguments: &[RuntimeValue],
397        budget: &BudgetOwner,
398    ) -> ProceduralResult<Vec<RuntimeValue>> {
399        budget.check_boundary()?;
400        let principals = self.principal_context();
401        crate::authorization::authorize_routine_invocation(
402            self.executor,
403            principals.session_principal,
404            principals.effective_principal,
405            routine,
406        )
407        .map_err(map_executor_error)?;
408        let published = load_published_routine(self.executor, routine, ObjectKind::Procedure)
409            .map_err(map_executor_error)?;
410        let use_definer = published.security == SecurityMode::Definer;
411        if use_definer {
412            self.push_definer(published.owner)?;
413        }
414        let execution = Interpreter.execute(&published.program, arguments.to_vec(), self, budget);
415        let cleanup = if use_definer {
416            self.pop_definer()
417        } else {
418            Ok(())
419        };
420        match (execution, cleanup) {
421            (Ok(outcome), Ok(())) => Ok(outcome.output_values),
422            (Err(primary), Ok(())) => Err(primary),
423            (Ok(_), Err(cleanup)) => Err(cleanup),
424            (Err(primary), Err(cleanup)) => {
425                Err(primary.with_detail("definer_cleanup_error", cleanup.to_string()))
426            }
427        }
428    }
429}
430
431impl AuditHost for ExecutorProceduralHost<'_> {
432    fn append_audit(&mut self, event: AuditEvent) -> ProceduralResult<()> {
433        if self
434            .function_volatility
435            .is_some_and(|volatility| volatility != Volatility::Volatile)
436        {
437            return Err(Diagnostic::new(
438                DiagnosticKind::VerifyCapabilityDenied,
439                "IMMUTABLE and STABLE functions cannot append audit records",
440            ));
441        }
442        let context = self
443            .context
444            .clone()
445            .with_effective_principal_id(self.principal_context().effective_principal);
446        self.executor
447            .append_audit_event(&context, event)
448            .map(|_| ())
449            .map_err(map_executor_error)
450    }
451}
452
453impl OutboxHost for ExecutorProceduralHost<'_> {
454    fn append_outbox(&mut self, message: OutboxMessage) -> ProceduralResult<()> {
455        if self
456            .function_volatility
457            .is_some_and(|volatility| volatility != Volatility::Volatile)
458        {
459            return Err(Diagnostic::new(
460                DiagnosticKind::VerifyCapabilityDenied,
461                "IMMUTABLE and STABLE functions cannot append outbox records",
462            ));
463        }
464        self.executor
465            .append_outbox_message(message)
466            .map(|_| ())
467            .map_err(map_executor_error)
468    }
469}
470
471impl Drop for ExecutorProceduralHost<'_> {
472    fn drop(&mut self) {
473        for (_, mut entry) in std::mem::take(&mut self.cursors) {
474            let _ = entry.result.close();
475        }
476    }
477}
478
479pub(super) fn operator_spelling(operator: InfixOperator) -> Option<&'static str> {
480    Some(match operator {
481        InfixOperator::Equal => "=",
482        InfixOperator::NotEqual => "<>",
483        InfixOperator::LessThan => "<",
484        InfixOperator::LessEqual => "<=",
485        InfixOperator::GreaterThan => ">",
486        InfixOperator::GreaterEqual => ">=",
487        InfixOperator::And => "AND",
488        InfixOperator::Or => "OR",
489        InfixOperator::Xor => "XOR",
490        InfixOperator::Add => "+",
491        InfixOperator::Subtract => "-",
492        InfixOperator::Multiply => "*",
493        InfixOperator::Divide => "/",
494        InfixOperator::Modulo => "%",
495        InfixOperator::Concat => "||",
496        InfixOperator::Like => "LIKE",
497        InfixOperator::ILike => "ILIKE",
498        InfixOperator::NotLike => "NOT LIKE",
499        InfixOperator::NotILike => "NOT ILIKE",
500        InfixOperator::Glob => "GLOB",
501        InfixOperator::NotGlob => "NOT GLOB",
502        InfixOperator::Regexp => "REGEXP",
503        InfixOperator::NotRegexp => "NOT REGEXP",
504        InfixOperator::Is => "IS",
505        InfixOperator::IsNot => "IS NOT",
506        InfixOperator::IsDistinctFrom => "IS DISTINCT FROM",
507        InfixOperator::IsNotDistinctFrom => "IS NOT DISTINCT FROM",
508        InfixOperator::Index => "[]",
509        InfixOperator::JsonAccess => "->",
510        InfixOperator::JsonAccessText => "->>",
511        InfixOperator::VectorDistance => "<=>",
512        InfixOperator::BitwiseAnd => "&",
513        InfixOperator::BitwiseOr => "|",
514        InfixOperator::BitwiseXor => "^",
515        InfixOperator::LeftShift => "<<",
516        InfixOperator::RightShift => ">>",
517        InfixOperator::Other => return None,
518    })
519}
520
521fn map_controlled_error(error: Error, budget: &BudgetOwner) -> Diagnostic {
522    if matches!(error, Error::QueryCancelled) {
523        if let Err(control) = budget.check_boundary() {
524            return control;
525        }
526    }
527    map_executor_error(error)
528}