use std::collections::HashMap;
use std::sync::Arc;
use surrealdb_strand::Strand;
use surrealdb_types::{SqlFormat, ToSql};
use super::helpers::{args_access_mode, args_required_context, evaluate_args, validate_return};
use crate::exec::physical_expr::{BlockPhysicalExpr, EvalContext, PhysicalExpr};
use crate::exec::{AccessMode, BoxFut, Error as ExecError};
use crate::expr::{ControlFlow, Error as ExprError, FlowResult};
use crate::val::Value;
#[derive(Debug, Clone)]
pub struct ClosureExec {
pub(crate) closure: crate::expr::ClosureExpr,
}
impl PhysicalExpr for ClosureExec {
fn name(&self) -> &'static str {
"Closure"
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn required_context(&self) -> crate::exec::ContextLevel {
crate::exec::ContextLevel::Root
}
fn evaluate<'a>(&'a self, ctx: EvalContext<'a>) -> BoxFut<'a, FlowResult<Value>> {
Box::pin(async move {
use crate::dbs::ParameterCapturePass;
use crate::val::Closure;
let frozen_ctx = ctx.exec_ctx.ctx();
let captures = ParameterCapturePass::capture(frozen_ctx, &self.closure.body);
Ok(Value::Closure(Box::new(Closure::Expr {
args: self.closure.args.clone(),
returns: self.closure.returns.clone(),
body: self.closure.body.clone(),
captures,
})))
})
}
fn access_mode(&self) -> AccessMode {
if self.closure.body.read_only() {
AccessMode::ReadOnly
} else {
AccessMode::ReadWrite
}
}
}
impl ToSql for ClosureExec {
fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
self.closure.fmt_sql(f, fmt);
}
}
#[derive(Debug, Clone)]
pub struct ClosureCallExec {
pub(crate) target: Arc<dyn PhysicalExpr>,
pub(crate) arguments: Vec<Arc<dyn PhysicalExpr>>,
}
impl PhysicalExpr for ClosureCallExec {
fn name(&self) -> &'static str {
"ClosureCall"
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn required_context(&self) -> crate::exec::ContextLevel {
let target_ctx = self.target.required_context();
let args_ctx = args_required_context(&self.arguments);
target_ctx.max(args_ctx)
}
fn evaluate<'a>(&'a self, ctx: EvalContext<'a>) -> BoxFut<'a, FlowResult<Value>> {
Box::pin(async move {
use crate::val::Closure;
let target_value = self.target.evaluate(ctx.clone()).await?;
let closure = match target_value {
Value::Closure(c) => c,
other => {
return Err(ExecError::InvalidFunction {
name: "ANONYMOUS".to_string(),
message: format!("'{}' is not a function", other.kind_of()),
}
.into());
}
};
let evaluated_args = evaluate_args(&self.arguments, ctx.clone()).await?;
match closure.as_ref() {
Closure::Expr {
args: arg_spec,
returns,
body,
captures,
} => {
let mut isolated_ctx = ctx.exec_ctx.clone();
for (name, value) in captures.clone() {
isolated_ctx = isolated_ctx.with_param(name, value);
}
if arg_spec.len() > evaluated_args.len()
&& let Some((param, kind)) =
arg_spec[evaluated_args.len()..].iter().find(|(_, k)| !k.can_be_none())
{
return Err(ExprError::InvalidFunctionArguments {
name: "ANONYMOUS".to_string(),
message: format!(
"Expected a value of type '{}' for argument {}",
kind.to_sql(),
param.to_sql()
),
}
.into());
}
let mut local_params: HashMap<Strand, Value> = HashMap::new();
for ((param, kind), arg_value) in arg_spec.iter().zip(evaluated_args) {
let coerced = arg_value.coerce_to_kind(kind).map_err(|_| {
ExprError::InvalidFunctionArguments {
name: "ANONYMOUS".to_string(),
message: format!(
"Expected a value of type '{}' for argument {}",
kind.to_sql(),
param.to_sql()
),
}
})?;
local_params.insert(param.clone().into_strand(), coerced);
}
for (name, value) in &local_params {
isolated_ctx = isolated_ctx.with_param(name.clone(), value.clone());
}
let block_expr = BlockPhysicalExpr {
block: crate::expr::Block(vec![body.clone()]),
matches_scope: None,
};
let eval_ctx = EvalContext {
exec_ctx: &isolated_ctx,
current_value: ctx.current_value,
local_params: Some(&local_params),
recursion_ctx: None,
document_root: None,
skip_fetch_perms: ctx.skip_fetch_perms,
computing_record: ctx.computing_record.clone(),
plan_depth: ctx.plan_depth + 1,
};
let result = match block_expr.evaluate(eval_ctx).await {
Ok(v) => v,
Err(ControlFlow::Return(v)) => v,
Err(ControlFlow::Break) | Err(ControlFlow::Continue) => {
return Err(ExecError::InvalidControlFlow.into());
}
Err(e) => return Err(e),
};
Ok(validate_return("ANONYMOUS", returns.as_ref(), result)?)
}
Closure::Builtin(_) => {
Err(anyhow::anyhow!(
"Builtin closures are not yet supported in the streaming executor"
)
.into())
}
}
})
}
fn access_mode(&self) -> AccessMode {
AccessMode::ReadWrite
.combine(self.target.access_mode())
.combine(args_access_mode(&self.arguments))
}
}
impl ToSql for ClosureCallExec {
fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
self.target.fmt_sql(f, fmt);
f.push_str("(...)");
}
}
#[cfg(all(test, feature = "kv-mem"))]
#[allow(clippy::unwrap_used)]
mod tests {
use crate::exec::AccessMode;
use crate::exec::operators::test_util::TestDb;
use crate::exec::planner::Planner;
async fn access_mode_of(db: &TestDb, expression: &str) -> AccessMode {
let ctx = db.exec_ctx().await;
let crate::exec::ExecutionContext::Database(db_ctx) = &ctx else {
panic!("exec_ctx builds a Database context");
};
let txn = ctx.txn();
let planner = Planner::with_txn(
ctx.ctx(),
&db_ctx.ns_ctx.root.function_registry,
txn,
Some("test".to_owned()),
Some("test".to_owned()),
);
let expr: crate::expr::Expr = crate::syn::expr(expression).unwrap().into();
planner.physical_expr(expr).await.unwrap().access_mode()
}
#[tokio::test]
async fn a_pure_closure_literal_is_read_only() {
let db = TestDb::new("").await;
assert_eq!(access_mode_of(&db, "|$x: any| { RETURN $x; }").await, AccessMode::ReadOnly);
}
#[tokio::test]
async fn a_writing_closure_literal_is_read_write() {
let db = TestDb::new("DEFINE TABLE log SCHEMALESS;").await;
assert_eq!(
access_mode_of(&db, "|$x: any| { CREATE log SET v = $x; }").await,
AccessMode::ReadWrite
);
}
#[tokio::test]
async fn a_writing_closure_passed_to_a_builtin_is_read_write() {
let db = TestDb::new("DEFINE TABLE log SCHEMALESS;").await;
assert_eq!(
access_mode_of(&db, "array::map([1, 2], |$x: any| { CREATE log SET v = $x; })").await,
AccessMode::ReadWrite
);
assert_eq!(
access_mode_of(&db, "array::map([1, 2], |$x: any| { RETURN $x; })").await,
AccessMode::ReadOnly
);
}
}