use std::sync::Arc;
use surrealdb_types::{SqlFormat, ToSql};
use crate::exec::physical_expr::{EvalContext, PhysicalExpr};
use crate::exec::{AccessMode, BoxFut, CombineAccessModes, ContextLevel};
use crate::expr::FlowResult;
use crate::val::Value;
#[derive(Debug, Clone)]
pub struct OptionalChainPart {
pub tail: Vec<Arc<dyn PhysicalExpr>>,
}
impl PhysicalExpr for OptionalChainPart {
fn name(&self) -> &'static str {
"OptionalChain"
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn required_context(&self) -> ContextLevel {
self.tail.iter().map(|p| p.required_context()).max().unwrap_or(ContextLevel::Root)
}
fn evaluate<'a>(&'a self, ctx: EvalContext<'a>) -> BoxFut<'a, FlowResult<Value>> {
Box::pin(async move {
let value = ctx.current_value.cloned().unwrap_or(Value::None);
if matches!(value, Value::None | Value::Null) {
return Ok(value);
}
let mut current = value;
for part in &self.tail {
current = part.evaluate(ctx.with_value(¤t)).await?;
}
Ok(current)
})
}
fn access_mode(&self) -> AccessMode {
self.tail.iter().map(|p| p.access_mode()).combine_all()
}
}
impl ToSql for OptionalChainPart {
fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
f.push('?');
for part in &self.tail {
part.fmt_sql(f, fmt);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::exec::operators::test_util::{eval_on, root_ctx, val};
use crate::exec::parts::FieldPart;
fn field_tail(names: &[&str]) -> Vec<Arc<dyn PhysicalExpr>> {
names
.iter()
.map(|name| {
Arc::new(FieldPart {
name: (*name).to_owned(),
}) as Arc<dyn PhysicalExpr>
})
.collect()
}
#[tokio::test]
async fn none_and_null_skip_the_tail_and_keep_their_own_kind() {
let ctx = root_ctx();
assert_eq!(eval_on("a.?.b", &val("{ a: NULL }").await, &ctx).await.unwrap(), Value::Null);
assert_eq!(eval_on("a.?.b", &val("{ a: NONE }").await, &ctx).await.unwrap(), Value::None);
assert_eq!(eval_on("a.?.b", &val("{ }").await, &ctx).await.unwrap(), Value::None);
}
#[tokio::test]
async fn a_present_value_is_threaded_through_the_whole_tail() {
let ctx = root_ctx();
let doc = val("{ a: { b: { c: 7 } } }").await;
assert_eq!(eval_on("a.?.b.c", &doc, &ctx).await.unwrap(), Value::from(7));
assert_eq!(
eval_on("a.?.missing", &val("{ a: { b: 1 } }").await, &ctx).await.unwrap(),
Value::None
);
assert_eq!(
eval_on("a.?.b", &val("{ a: [NONE, { b: 1 }] }").await, &ctx).await.unwrap(),
val("[NONE, 1]").await
);
assert_eq!(eval_on("a.?.b", &val("{ a: 42 }").await, &ctx).await.unwrap(), Value::None);
}
#[tokio::test]
async fn nested_optionals_stop_at_the_first_none_or_null() {
let ctx = root_ctx();
assert_eq!(
eval_on("a.?.b.?.c", &val("{ a: { b: NULL } }").await, &ctx).await.unwrap(),
Value::Null
);
assert_eq!(
eval_on("a.?.b.?.c", &val("{ a: NULL }").await, &ctx).await.unwrap(),
Value::Null
);
assert_eq!(
eval_on("a.?.b.?.c", &val("{ a: { b: { c: 1 } } }").await, &ctx).await.unwrap(),
Value::from(1)
);
}
#[tokio::test]
async fn an_empty_tail_passes_the_input_through() {
let ctx = root_ctx();
let part = OptionalChainPart {
tail: vec![],
};
let base = EvalContext::from_exec_ctx(&ctx);
for src in ["42", "'text'", "{ a: 1 }", "NULL", "NONE"] {
let value = val(src).await;
let out = part.evaluate(base.with_value(&value)).await.unwrap();
assert_eq!(out, value, "an empty tail should pass {src} through");
}
}
#[tokio::test]
async fn declared_metadata_is_the_maximum_over_the_tail() {
let empty = OptionalChainPart {
tail: vec![],
};
assert_eq!(empty.required_context(), ContextLevel::Root);
assert_eq!(empty.access_mode(), AccessMode::ReadOnly);
let with_field = OptionalChainPart {
tail: field_tail(&["a", "b"]),
};
assert_eq!(with_field.required_context(), ContextLevel::Database);
assert_eq!(with_field.access_mode(), AccessMode::ReadOnly);
}
}