use std::sync::Arc;
use surrealdb_types::ToSql;
use super::common::{RecursionBounds, eval_buffered, is_recursion_target};
use crate::exec::FlowResult;
use crate::exec::parts::{evaluate_physical_path, is_final};
use crate::exec::physical_expr::{EvalContext, PhysicalExpr};
use crate::val::Value;
pub(crate) async fn evaluate_recurse_path(
start: &Value,
path: &[Arc<dyn PhysicalExpr>],
bounds: RecursionBounds,
inclusive: bool,
ctx: EvalContext<'_>,
) -> FlowResult<Value> {
let min_depth = bounds.min;
let max_depth = bounds.cap();
let mut completed_paths: Vec<Value> = Vec::new();
let mut active_paths: Vec<Vec<Value>> = if inclusive {
vec![vec![start.clone()]]
} else {
vec![vec![]]
};
let mut depth = 0u32;
while depth < max_depth && !active_paths.is_empty() {
let mut next_paths = Vec::new();
let futures: Vec<_> = active_paths
.iter()
.map(|current_path| {
let current_value = current_path.last().unwrap_or(start);
evaluate_physical_path(current_value, path, ctx.with_value(current_value))
})
.collect();
let eval_results = eval_buffered(futures).await?;
for (mut current_path, result) in active_paths.into_iter().zip(eval_results) {
let values = match result {
Value::Array(arr) => arr.0,
Value::None | Value::Null => {
if depth >= min_depth && !current_path.is_empty() {
completed_paths.push(Value::Array(current_path.into()));
}
continue;
}
other => vec![other],
};
let mut valid_targets = Vec::new();
for v in values {
if is_final(&v) {
continue;
}
if !is_recursion_target(&v) {
return Err(crate::exec::Error::InvalidRecursionTarget {
value: v.to_sql(),
}
.into());
}
valid_targets.push(v);
}
if valid_targets.is_empty() {
if depth >= min_depth && !current_path.is_empty() {
completed_paths.push(Value::Array(current_path.into()));
}
} else {
let mut iter = valid_targets.into_iter().peekable();
while let Some(v) = iter.next() {
if iter.peek().is_some() {
let mut new_path = current_path.clone();
new_path.push(v);
next_paths.push(new_path);
} else {
current_path.push(v);
next_paths.push(current_path);
break;
}
}
}
}
active_paths = next_paths;
depth += 1;
}
if bounds.errors_on_limit() && !active_paths.is_empty() {
return Err(crate::exec::Error::IdiomRecursionLimitExceeded {
limit: bounds.system_limit,
}
.into());
}
for p in active_paths {
if !p.is_empty() && depth >= min_depth {
completed_paths.push(Value::Array(p.into()));
}
}
Ok(Value::Array(completed_paths.into()))
}
#[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_from(
start: &Value,
src: &str,
min: u32,
max: Option<u32>,
inclusive: bool,
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_path(
start,
&path,
bounds(min, max, system_limit),
inclusive,
base.with_value(start),
)
.await
}
async fn links(
start: &str,
min: u32,
max: Option<u32>,
inclusive: bool,
ctx: &ExecutionContext,
) -> FlowResult<Value> {
let start = val(start).await;
run_from(&start, "link:a.next", min, max, inclusive, SYSTEM_LIMIT, ctx).await
}
#[tokio::test]
async fn each_walk_is_returned_as_its_own_array() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(
links("link:x", 1, Some(1), false, &ctx).await.unwrap(),
val("[[link:y], [link:z]]").await
);
assert_eq!(
links("link:x", 1, Some(2), false, &ctx).await.unwrap(),
val("[[link:y, link:w], [link:z, link:w]]").await
);
assert_eq!(
links("link:x", 1, Some(2), true, &ctx).await.unwrap(),
val("[[link:x, link:y, link:w], [link:x, link:z, link:w]]").await
);
}
#[tokio::test]
async fn a_walk_that_dead_ends_at_or_past_min_depth_is_completed() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(
links("link:x", 1, Some(9), false, &ctx).await.unwrap(),
val("[[link:y, link:w], [link:z, link:w]]").await
);
}
#[tokio::test]
async fn a_walk_that_dies_before_min_depth_is_dropped_not_truncated() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(links("link:x", 3, Some(4), false, &ctx).await.unwrap(), val("[]").await);
}
#[tokio::test]
async fn walks_still_active_at_the_upper_bound_are_returned() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(
links("link:a", 1, Some(2), false, &ctx).await.unwrap(),
val("[[link:b, link:c]]").await
);
}
#[tokio::test]
async fn a_dead_end_start_yields_no_walks_at_all() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(links("link:d", 1, Some(3), false, &ctx).await.unwrap(), val("[]").await);
assert_eq!(links("link:d", 1, Some(3), true, &ctx).await.unwrap(), val("[]").await);
}
#[tokio::test]
async fn dead_end_elements_inside_a_step_result_drop_only_their_own_branch() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
let start = val("[link:d, link:c]").await;
assert_eq!(
run_from(&start, "link:a.next", 1, Some(3), false, SYSTEM_LIMIT, &ctx).await.unwrap(),
val("[[link:d]]").await
);
}
#[tokio::test]
async fn a_cycle_is_walked_to_the_bound_because_walks_carry_no_visited_set() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(
links("link:p", 1, Some(4), false, &ctx).await.unwrap(),
val("[[link:q, link:p, link:q, link:p]]").await
);
}
#[tokio::test]
async fn a_self_loop_repeats_inside_the_walk_rather_than_closing_it() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
assert_eq!(
links("link:s", 1, Some(3), false, &ctx).await.unwrap(),
val("[[link:s, link:s, link:s]]").await
);
}
#[tokio::test]
async fn an_unbounded_walk_still_active_at_the_cap_raises_the_limit() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
let start = val("link:a").await;
let err = run_from(&start, "link:a.next", 1, None, false, 2, &ctx).await.unwrap_err();
assert!(matches!(
exec_error(err),
crate::exec::Error::IdiomRecursionLimitExceeded {
limit: 2
}
));
}
#[tokio::test]
async fn an_explicit_bound_truncates_silently_instead_of_raising() {
let db = TestDb::new(FIXTURES).await;
let ctx = db.exec_ctx().await;
let start = val("link:a").await;
assert_eq!(
run_from(&start, "link:a.next", 1, Some(2), false, 2, &ctx).await.unwrap(),
val("[[link:b, link:c]]").await
);
}
#[tokio::test]
async fn a_non_record_value_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_from(&start, "link:a.name", 1, Some(2), false, 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_traversal() {
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::Continue);
let err = evaluate_recurse_path(
&start,
&path,
bounds(1, Some(3), SYSTEM_LIMIT),
false,
base.with_value(&start),
)
.await
.unwrap_err();
assert!(matches!(err, ControlFlow::Continue));
}
}