use std::sync::Arc;
use surrealdb_types::ToSql;
use super::common::{RecursionBounds, is_recursion_target};
use crate::exec::FlowResult;
use crate::exec::parts::{clean_iteration, evaluate_physical_path, get_final, is_final};
use crate::exec::physical_expr::{EvalContext, PhysicalExpr};
use crate::val::Value;
pub(crate) async fn evaluate_recurse_default(
start: &Value,
path: &[Arc<dyn PhysicalExpr>],
bounds: RecursionBounds,
ctx: EvalContext<'_>,
) -> FlowResult<Value> {
let min_depth = bounds.min;
let max_depth = bounds.cap();
let mut current = start.clone();
let mut depth = 0u32;
while depth < max_depth {
let next = evaluate_physical_path(¤t, path, ctx.with_value(¤t)).await?;
depth += 1;
let next = clean_iteration(next);
if is_final(&next) || next == current {
return if depth > min_depth {
Ok(current)
} else {
Ok(get_final(&next))
};
}
if !is_recursion_target(&next) {
return Err(crate::exec::Error::InvalidRecursionTarget {
value: next.to_sql(),
}
.into());
}
current = next;
}
if bounds.errors_on_limit() && depth >= bounds.system_limit {
return Err(crate::exec::Error::IdiomRecursionLimitExceeded {
limit: bounds.system_limit,
}
.into());
}
if depth >= min_depth {
Ok(current)
} else {
Ok(Value::None)
}
}
#[cfg(test)]
mod tests {
use super::super::tests::{
FIXTURES, Raise, SYSTEM_LIMIT, body_path, bounds, exec_error, raise_path,
};
use super::*;
use crate::exec::ExecutionContext;
use crate::exec::operators::test_util::{TestDb, val};
use crate::expr::ControlFlow;
async fn run(
start: &Value,
src: &str,
min: u32,
max: Option<u32>,
system_limit: u32,
ctx: &ExecutionContext,
) -> FlowResult<Value> {
let path = body_path(src, ctx).await;
let base = crate::exec::physical_expr::EvalContext::from_exec_ctx(ctx);
evaluate_recurse_default(
start,
&path,
bounds(min, max, system_limit),
base.with_value(start),
)
.await
}
async fn chain(
start: &str,
min: u32,
max: Option<u32>,
ctx: &ExecutionContext,
) -> FlowResult<Value> {
let start = val(start).await;
run(&start, "link:a.next", min, max, SYSTEM_LIMIT, ctx).await
}
#[tokio::test]
async fn an_exact_depth_returns_the_value_reached_at_that_depth() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(chain("link:a", 1, Some(1), &ctx).await.unwrap(), val("link:b").await);
assert_eq!(chain("link:a", 2, Some(2), &ctx).await.unwrap(), val("link:c").await);
assert_eq!(chain("link:a", 3, Some(3), &ctx).await.unwrap(), val("link:d").await);
}
#[tokio::test]
async fn a_range_terminates_at_the_upper_bound() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(chain("link:a", 1, Some(2), &ctx).await.unwrap(), val("link:c").await);
}
#[tokio::test]
async fn a_dead_end_short_of_the_upper_bound_returns_the_last_live_value() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(chain("link:a", 1, Some(9), &ctx).await.unwrap(), val("link:d").await);
}
#[tokio::test]
async fn an_unbounded_recursion_that_dead_ends_never_reaches_the_limit() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
let start = val("link:a").await;
let result = run(&start, "link:a.next", 1, None, 8, &ctx).await.unwrap();
assert_eq!(result, val("link:d").await);
}
#[tokio::test]
async fn a_zero_depth_bound_returns_the_start_untouched() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(chain("link:a", 0, Some(0), &ctx).await.unwrap(), val("link:a").await);
}
#[tokio::test]
async fn a_branch_that_dies_before_min_depth_is_dropped_not_truncated() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(chain("link:c", 3, Some(5), &ctx).await.unwrap(), Value::None);
assert_eq!(chain("link:c", 2, Some(2), &ctx).await.unwrap(), Value::None);
assert_eq!(chain("link:c", 1, Some(5), &ctx).await.unwrap(), val("link:d").await);
}
#[tokio::test]
async fn the_dead_end_keeps_the_shape_of_the_value_that_produced_it() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(chain("link:d", 1, Some(1), &ctx).await.unwrap(), Value::None);
let start = val("[link:d]").await;
assert_eq!(
run(&start, "link:a.next", 1, Some(1), SYSTEM_LIMIT, &ctx).await.unwrap(),
val("[]").await
);
}
#[tokio::test]
async fn a_self_loop_terminates_on_value_equality_with_the_previous_step() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(chain("link:s", 1, Some(4), &ctx).await.unwrap(), Value::None);
assert_eq!(chain("link:s", 0, Some(4), &ctx).await.unwrap(), val("link:s").await);
let start = val("node:s").await;
assert_eq!(
run(&start, "node:a->step->node", 1, Some(4), SYSTEM_LIMIT, &ctx).await.unwrap(),
val("[node:s]").await
);
}
#[tokio::test]
async fn a_two_cycle_is_walked_to_the_bound_because_there_is_no_visited_set() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(chain("link:p", 1, Some(6), &ctx).await.unwrap(), val("link:p").await);
assert_eq!(chain("link:p", 1, Some(5), &ctx).await.unwrap(), val("link:q").await);
}
#[tokio::test]
async fn an_unbounded_walk_of_a_cycle_raises_the_iteration_limit() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
let start = val("link:p").await;
let err = run(&start, "link:a.next", 1, None, 8, &ctx).await.unwrap_err();
assert!(matches!(
exec_error(err),
crate::exec::Error::IdiomRecursionLimitExceeded {
limit: 8
}
));
}
#[tokio::test]
async fn a_non_record_step_is_rejected_because_recursion_is_record_only() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
let start = val("link:a").await;
let err = run(&start, "link:a.name", 1, Some(2), SYSTEM_LIMIT, &ctx).await.unwrap_err();
match exec_error(err) {
crate::exec::Error::InvalidRecursionTarget {
value,
} => assert_eq!(value, "'A'"),
other => panic!("expected InvalidRecursionTarget, got {other:?}"),
}
}
#[tokio::test]
async fn control_flow_out_of_the_body_aborts_the_walk() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
let start = val("link:a").await;
let base = crate::exec::physical_expr::EvalContext::from_exec_ctx(&ctx);
let path = raise_path(Raise::Return);
let err = evaluate_recurse_default(
&start,
&path,
bounds(1, Some(3), SYSTEM_LIMIT),
base.with_value(&start),
)
.await
.unwrap_err();
match err {
ControlFlow::Return(v) => assert_eq!(v, Value::from(7)),
other => panic!("expected RETURN, got {other}"),
}
let path = raise_path(Raise::Error);
let err = evaluate_recurse_default(
&start,
&path,
bounds(1, Some(3), SYSTEM_LIMIT),
base.with_value(&start),
)
.await
.unwrap_err();
assert!(matches!(err, ControlFlow::Err(_)));
}
}