surrealdb-core 3.3.1

A scalable, distributed, collaborative, document-graph database, for the realtime web
//! Module function expressions - Surrealism WASM and Silo packages.

use std::sync::Arc;

use surrealdb_types::{SqlFormat, ToSql};

use super::helpers::{args_access_mode, args_required_context};
#[cfg(feature = "surrealism")]
use super::helpers::{check_permission, evaluate_args, validate_return};
#[cfg(feature = "surrealism")]
use crate::catalog::providers::DatabaseProvider;
use crate::exec::physical_expr::{EvalContext, PhysicalExpr};
use crate::exec::{AccessMode, BoxFut};
#[cfg(feature = "surrealism")]
use crate::expr::Error;
use crate::expr::FlowResult;
use crate::val::Value;

/// Look up a module by its storage name, then call one of its exports.
///
/// Both executable forms reach a module the same way — the storage name is the
/// only thing that differs — so `mod::` and `silo::` calls share this body and
/// cannot drift apart in permission checking, argument coercion or return
/// validation. `mod_name` is the storage name
/// ([`ModuleName::get_storage_name`](crate::expr::module::ModuleName)), and
/// `sub` names the export, or the default one when `None`.
#[cfg(feature = "surrealism")]
async fn call_module_export<'a>(
	mod_name: String,
	sub: Option<&'a str>,
	arguments: &'a [Arc<dyn PhysicalExpr>],
	ctx: EvalContext<'a>,
) -> FlowResult<Value> {
	use reblessive::TreeStack;

	use crate::doc::CursorDoc;
	use crate::expr::module::ModuleExecutable;

	let fnc_name = match sub {
		Some(sub) => format!("{mod_name}::{sub}"),
		None => mod_name.clone(),
	};

	// Check if this function is allowed
	ctx.check_allowed_function(&fnc_name)?;

	// Get the database context for module lookup
	let db_ctx = ctx
		.exec_ctx
		.database()
		.map_err(|_| anyhow::anyhow!("Module function '{}' requires database context", fnc_name))?;

	// Get namespace and database IDs
	let ns_id = db_ctx.ns_ctx.ns.namespace_id;
	let db_id = db_ctx.db.database_id;

	// Get the module definition
	let val =
		ctx.txn().get_db_module(ns_id, db_id, &mod_name, ctx.exec_ctx.version_stamp()).await?;

	// Check permissions
	if ctx.exec_ctx.should_check_perms(crate::iam::Action::View)? {
		check_permission(&val.permissions, &mod_name, &ctx).await?;
	}

	// Get the executable and signature
	let executable: ModuleExecutable = val.executable.clone().into();
	let frozen_ctx = ctx.exec_ctx.ctx();
	let signature =
		crate::legacy::module_executable_signature(&executable, frozen_ctx, &ns_id, &db_id, sub)
			.await?;

	// Evaluate all arguments
	let args = evaluate_args(arguments, ctx.clone()).await?;

	// Validate argument count against signature
	if args.len() != signature.args.len() {
		return Err(Error::InvalidFunctionArguments {
			name: fnc_name,
			message: format!(
				"The function expects {} arguments, but {} were provided.",
				signature.args.len(),
				args.len()
			),
		}
		.into());
	}

	// Validate and coerce arguments to their expected types
	let mut coerced_args = Vec::with_capacity(args.len());
	for (arg, kind) in args.into_iter().zip(signature.args.iter()) {
		let coerced = arg.coerce_to_kind(kind).map_err(|e| Error::InvalidFunctionArguments {
			name: fnc_name.clone(),
			message: format!("Failed to coerce argument: {e}"),
		})?;
		coerced_args.push(coerced);
	}

	// Get the Options for the module execution
	let opt = ctx
		.exec_ctx
		.options()
		.ok_or_else(|| anyhow::anyhow!("Module functions require Options context"))?;

	// Build CursorDoc from current value
	let doc = ctx.current_value.map(|v| CursorDoc::new(None, None, v.clone()));

	// Run the module using the legacy stack-based execution
	let mut stack = TreeStack::new();
	let result = stack
		.enter(|stk| {
			crate::legacy::module_executable_run(
				&executable,
				stk,
				frozen_ctx,
				opt,
				doc.as_ref(),
				coerced_args,
				sub,
			)
		})
		.finish()
		.await?;

	// Validate return value if signature specifies a return type
	validate_return(&fnc_name, signature.returns.as_ref(), result).map_err(Into::into)
}

// =============================================================================
// SurrealismModuleExec - for Function::Module
// =============================================================================

/// Surrealism WASM module function expression.
#[derive(Debug, Clone)]
pub struct SurrealismModuleExec {
	pub(crate) module: String,
	pub(crate) sub: Option<String>,
	pub(crate) arguments: Vec<Arc<dyn PhysicalExpr>>,
	/// Whether this function is declared writeable by the module author.
	/// When `false`, the function is read-only and the host can use a read transaction.
	pub(crate) writeable: bool,
}
impl PhysicalExpr for SurrealismModuleExec {
	fn name(&self) -> &'static str {
		"SurrealismModule"
	}

	fn as_any(&self) -> &dyn std::any::Any {
		self
	}

	fn required_context(&self) -> crate::exec::ContextLevel {
		// Module functions require database context, and arguments
		// may have their own context requirements
		args_required_context(&self.arguments).max(crate::exec::ContextLevel::Database)
	}

	#[cfg(feature = "surrealism")]
	fn evaluate<'a>(&'a self, ctx: EvalContext<'a>) -> BoxFut<'a, FlowResult<Value>> {
		Box::pin(call_module_export(
			format!("mod::{}", self.module),
			self.sub.as_deref(),
			&self.arguments,
			ctx,
		))
	}

	#[cfg(not(feature = "surrealism"))]
	fn evaluate<'a>(&'a self, _ctx: EvalContext<'a>) -> BoxFut<'a, FlowResult<Value>> {
		Box::pin(async move {
			let name = match &self.sub {
				Some(s) => format!("mod::{}::{}", self.module, s),
				None => format!("mod::{}", self.module),
			};
			Err(anyhow::anyhow!(
				"Module function '{}' requires the 'surrealism' feature to be enabled",
				name
			)
			.into())
		})
	}

	fn access_mode(&self) -> AccessMode {
		let func_mode = if self.writeable {
			AccessMode::ReadWrite
		} else {
			AccessMode::ReadOnly
		};
		func_mode.combine(args_access_mode(&self.arguments))
	}
}

impl ToSql for SurrealismModuleExec {
	fn fmt_sql(&self, f: &mut String, _fmt: SqlFormat) {
		f.push_str("mod::");
		f.push_str(&self.module);
		if let Some(sub) = &self.sub {
			f.push_str("::");
			f.push_str(sub);
		}
		f.push_str("(...)");
	}
}

// =============================================================================
// SiloModuleExec - for Function::Silo
// =============================================================================

/// Silo versioned package function expression.
#[derive(Debug, Clone)]
pub struct SiloModuleExec {
	pub(crate) org: String,
	pub(crate) pkg: String,
	pub(crate) major: u32,
	pub(crate) minor: u32,
	pub(crate) patch: u32,
	pub(crate) sub: Option<String>,
	pub(crate) arguments: Vec<Arc<dyn PhysicalExpr>>,
	/// Whether this function is declared writeable by the module author.
	pub(crate) writeable: bool,
}
impl PhysicalExpr for SiloModuleExec {
	fn name(&self) -> &'static str {
		"SiloModule"
	}

	fn as_any(&self) -> &dyn std::any::Any {
		self
	}

	fn required_context(&self) -> crate::exec::ContextLevel {
		// Silo package functions require database context, and arguments
		// may have their own context requirements
		args_required_context(&self.arguments).max(crate::exec::ContextLevel::Database)
	}

	#[cfg(feature = "surrealism")]
	fn evaluate<'a>(&'a self, ctx: EvalContext<'a>) -> BoxFut<'a, FlowResult<Value>> {
		Box::pin(call_module_export(
			format!(
				"silo::{}::{}::<{}.{}.{}>",
				self.org, self.pkg, self.major, self.minor, self.patch
			),
			self.sub.as_deref(),
			&self.arguments,
			ctx,
		))
	}

	#[cfg(not(feature = "surrealism"))]
	fn evaluate<'a>(&'a self, _ctx: EvalContext<'a>) -> BoxFut<'a, FlowResult<Value>> {
		Box::pin(async move {
			let name = format!(
				"silo::{}::{}::<{}.{}.{}>",
				self.org, self.pkg, self.major, self.minor, self.patch
			);
			Err(anyhow::anyhow!(
				"Silo function '{}' requires the 'surrealism' feature to be enabled",
				name
			)
			.into())
		})
	}

	fn access_mode(&self) -> AccessMode {
		let func_mode = if self.writeable {
			AccessMode::ReadWrite
		} else {
			AccessMode::ReadOnly
		};
		func_mode.combine(args_access_mode(&self.arguments))
	}
}

impl ToSql for SiloModuleExec {
	fn fmt_sql(&self, f: &mut String, _fmt: SqlFormat) {
		f.push_str("silo::");
		f.push_str(&self.org);
		f.push_str("::");
		f.push_str(&self.pkg);
		f.push_str("::");
		f.push('<');
		f.push_str(&self.major.to_string());
		f.push('.');
		f.push_str(&self.minor.to_string());
		f.push('.');
		f.push_str(&self.patch.to_string());
		f.push('>');
		if let Some(sub) = &self.sub {
			f.push_str("::");
			f.push_str(sub);
		}
		f.push_str("(...)");
	}
}