use std::sync::Arc;
use futures::stream;
use surrealdb_types::{SqlFormat, ToSql};
use crate::ctx::FrozenContext;
use crate::err::Error;
use crate::exec::context::{ContextLevel, ExecutionContext};
use crate::exec::plan_or_compute::{block_required_context, collect_stream, legacy_compute};
use crate::exec::planner::try_plan_expr;
use crate::exec::{
AccessMode, BoxFut, CardinalityHint, ExecOperator, FlowResult, OperatorMetrics, ValueBatch,
ValueBatchStream,
};
use crate::expr::{Block, ControlFlow, ControlFlowExt, Expr};
use crate::val::{Array, Value};
#[derive(Debug)]
pub struct SequencePlan {
pub block: Block,
pub(crate) metrics: Arc<OperatorMetrics>,
plan_depth: u32,
}
impl SequencePlan {
pub(crate) fn new(block: Block, plan_depth: u32) -> Self {
Self {
block,
metrics: Arc::new(OperatorMetrics::new()),
plan_depth,
}
}
}
impl ExecOperator for SequencePlan {
fn name(&self) -> &'static str {
"Sequence"
}
fn attrs(&self) -> Vec<(String, String)> {
vec![("statements".to_string(), self.block.0.len().to_string())]
}
fn required_context(&self) -> ContextLevel {
block_required_context(&self.block)
}
fn access_mode(&self) -> AccessMode {
if self.block.read_only() {
AccessMode::ReadOnly
} else {
AccessMode::ReadWrite
}
}
fn cardinality_hint(&self) -> CardinalityHint {
CardinalityHint::AtMostOne
}
fn execute(&self, ctx: &ExecutionContext) -> FlowResult<ValueBatchStream> {
let block = self.block.clone();
let depth = self.plan_depth + 1;
let initial_ctx = ctx.clone();
let stream = stream::once(async move {
let (result, _) = execute_block_with_context(&block, &initial_ctx, depth).await?;
Ok(ValueBatch {
values: vec![result],
})
});
Ok(Box::pin(stream))
}
fn mutates_context(&self) -> bool {
self.block.0.iter().any(|expr| matches!(expr, Expr::Let(_)))
}
fn output_context<'a>(
&'a self,
input: &'a ExecutionContext,
) -> BoxFut<'a, Result<ExecutionContext, Error>> {
Box::pin(async move {
let (_result, final_ctx) =
execute_block_with_context(&self.block, input, self.plan_depth + 1).await.map_err(
|ctrl| match ctrl {
ControlFlow::Break | ControlFlow::Continue | ControlFlow::Return(_) => {
Error::InvalidControlFlow
}
ControlFlow::Err(e) => Error::Thrown(e.to_string()),
},
)?;
Ok(final_ctx)
})
}
fn children(&self) -> Vec<&Arc<dyn ExecOperator>> {
vec![]
}
fn metrics(&self) -> Option<&OperatorMetrics> {
Some(&self.metrics)
}
fn is_scalar(&self) -> bool {
true
}
}
async fn execute_block_with_context(
block: &Block,
initial_ctx: &ExecutionContext,
depth: u32,
) -> crate::expr::FlowResult<(Value, ExecutionContext)> {
if block.0.is_empty() {
return Ok((Value::None, initial_ctx.clone()));
}
let mut current_ctx = initial_ctx.clone();
let mut result = Value::None;
for expr in block.0.iter() {
if current_ctx.cancellation().is_cancelled() {
return Err(ControlFlow::Err(anyhow::anyhow!(Error::QueryCancelled)));
}
let frozen_ctx = Arc::clone(current_ctx.ctx());
let auth = current_ctx.options().map(|o| Arc::clone(&o.auth));
match try_plan_expr!(expr, &frozen_ctx, current_ctx.txn(), auth, depth) {
Ok(plan) => {
if plan.mutates_context() {
current_ctx = plan.output_context(¤t_ctx).await?;
result = Value::None;
} else {
let stream = plan.execute(¤t_ctx)?;
let values = collect_stream(stream).await?;
result = if plan.is_scalar() {
values.into_iter().next().unwrap_or(Value::None)
} else {
Value::Array(Array(values))
};
}
}
Err(e @ (Error::PlannerUnsupported(_) | Error::PlannerUnimplemented(_))) => {
match &e {
Error::PlannerUnimplemented(msg) => {
tracing::warn!("PlannerUnimplemented fallback in sequence: {msg}");
}
Error::PlannerUnsupported(msg) => {
tracing::debug!("PlannerUnsupported fallback in sequence: {msg}",);
}
_ => {}
}
let (opt, frozen) = legacy_context_for_fallback(¤t_ctx)
.context("Legacy compute fallback context unavailable")?;
let opt = opt.with_dive_consumed(depth);
if let Expr::Let(set_stmt) = expr {
let value = legacy_compute(&set_stmt.what, &frozen, &opt, None).await?;
current_ctx = current_ctx.with_param(set_stmt.name.clone(), value.clone());
result = Value::None;
} else {
result = legacy_compute(expr, &frozen, &opt, None).await?;
}
}
Err(e) => return Err(ControlFlow::Err(e.into())),
}
}
Ok((result, current_ctx))
}
fn legacy_context_for_fallback(
exec_ctx: &ExecutionContext,
) -> Result<(crate::dbs::Options, FrozenContext), Error> {
let options = exec_ctx.options().ok_or_else(|| {
Error::Internal("Options not available for legacy compute fallback".into())
})?;
let options = if exec_ctx.root().skip_fetch_perms {
options.new_for_permission_predicate()
} else {
options.clone()
};
Ok((options, Arc::clone(exec_ctx.ctx())))
}
impl ToSql for SequencePlan {
fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
self.block.fmt_sql(f, fmt);
}
}