use std::sync::Arc;
use futures::StreamExt;
use surrealdb_types::{SqlFormat, ToSql};
use crate::err::Error;
use crate::exec::physical_expr::{EvalContext, PhysicalExpr};
use crate::exec::{AccessMode, BoxFut, ContextLevel, ExecOperator};
use crate::expr::FlowResult;
use crate::val::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LookupDirection {
Out,
In,
Both,
Reference,
}
impl From<crate::expr::Dir> for LookupDirection {
fn from(dir: crate::expr::Dir) -> Self {
match dir {
crate::expr::Dir::Out => LookupDirection::Out,
crate::expr::Dir::In => LookupDirection::In,
crate::expr::Dir::Both => LookupDirection::Both,
}
}
}
impl From<&crate::expr::Dir> for LookupDirection {
fn from(dir: &crate::expr::Dir) -> Self {
match dir {
crate::expr::Dir::Out => LookupDirection::Out,
crate::expr::Dir::In => LookupDirection::In,
crate::expr::Dir::Both => LookupDirection::Both,
}
}
}
#[derive(Debug, Clone)]
pub struct LookupPart {
pub direction: LookupDirection,
pub plan: Arc<dyn ExecOperator>,
pub extract_id: bool,
pub fused: bool,
pub only: bool,
}
impl PhysicalExpr for LookupPart {
fn name(&self) -> &'static str {
"Lookup"
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn required_context(&self) -> ContextLevel {
self.plan.required_context().max(ContextLevel::Database)
}
fn evaluate<'a>(&'a self, ctx: EvalContext<'a>) -> BoxFut<'a, FlowResult<Value>> {
Box::pin(async move {
let value = ctx.current_value.unwrap_or(&Value::NONE);
Ok(evaluate_lookup(value, self, ctx).await?)
})
}
fn evaluate_batch<'a>(
&'a self,
ctx: EvalContext<'a>,
values: &'a [Value],
) -> BoxFut<'a, FlowResult<Vec<Value>>> {
Box::pin(async move {
if values.len() < 2 || self.access_mode() == AccessMode::ReadWrite {
let mut results = Vec::with_capacity(values.len());
for value in values {
results.push(self.evaluate(ctx.with_value(value)).await?);
}
return Ok(results);
}
let futures: Vec<_> =
values.iter().map(|value| self.evaluate(ctx.with_value(value))).collect();
futures::future::try_join_all(futures).await
})
}
fn access_mode(&self) -> AccessMode {
self.plan.access_mode()
}
fn embedded_operators(&self) -> Vec<(&str, &Arc<dyn ExecOperator>)> {
vec![("lookup", &self.plan)]
}
fn is_fused_lookup(&self) -> bool {
self.fused
}
}
impl ToSql for LookupPart {
fn fmt_sql(&self, f: &mut String, _fmt: SqlFormat) {
match self.direction {
LookupDirection::Out => f.push_str("->..."),
LookupDirection::In => f.push_str("<-..."),
LookupDirection::Both => f.push_str("<->..."),
LookupDirection::Reference => f.push_str("<~..."),
}
}
}
async fn evaluate_lookup(
value: &Value,
lookup: &LookupPart,
ctx: EvalContext<'_>,
) -> anyhow::Result<Value> {
match value {
Value::RecordId(_) | Value::Object(_) => {
evaluate_lookup_for_value(value, lookup, ctx).await
}
Value::Array(arr) => {
let mut results = Vec::new();
for item in arr.iter() {
let result = Box::pin(evaluate_lookup(item, lookup, ctx.clone())).await?;
match result {
Value::Array(inner) => results.extend(inner),
other => results.push(other),
}
}
Ok(Value::Array(results.into()))
}
_ => Ok(Value::None),
}
}
async fn evaluate_lookup_for_value(
value: &Value,
lookup: &LookupPart,
ctx: EvalContext<'_>,
) -> anyhow::Result<Value> {
let bound_ctx = ctx.exec_ctx.with_current_value(value.clone());
let bound_ctx = if let Some(parent) = ctx.document_root {
bound_ctx.with_param("parent", parent.clone())
} else {
bound_ctx
};
let bound_ctx = if ctx.skip_fetch_perms {
bound_ctx.with_skip_fetch_perms(true)
} else {
bound_ctx
};
let stream = lookup.plan.execute(&bound_ctx).map_err(|e| match e {
crate::expr::ControlFlow::Err(e) => e,
crate::expr::ControlFlow::Return(v) => {
anyhow::anyhow!("Unexpected return in lookup: {:?}", v)
}
crate::expr::ControlFlow::Break => anyhow::anyhow!("Unexpected break in lookup"),
crate::expr::ControlFlow::Continue => anyhow::anyhow!("Unexpected continue in lookup"),
})?;
let mut results = Vec::new();
futures::pin_mut!(stream);
while let Some(batch_result) = stream.next().await {
let batch = batch_result.map_err(|e| match e {
crate::expr::ControlFlow::Err(e) => e,
crate::expr::ControlFlow::Return(v) => {
anyhow::anyhow!("Unexpected return in lookup: {:?}", v)
}
crate::expr::ControlFlow::Break => anyhow::anyhow!("Unexpected break in lookup"),
crate::expr::ControlFlow::Continue => {
anyhow::anyhow!("Unexpected continue in lookup")
}
})?;
results.extend(batch.values);
}
if lookup.extract_id {
let results: Vec<Value> = results
.into_iter()
.filter_map(|v| match v {
Value::Object(ref obj) => {
obj.get("id").filter(|id| matches!(id, Value::RecordId(_))).cloned()
}
Value::RecordId(_) => Some(v),
_ => None,
})
.collect();
if lookup.only {
return match results.len() {
0 => Ok(Value::None),
1 => Ok(results.into_iter().next().expect("Exactly one result in this branch")),
_ => Err(anyhow::anyhow!(Error::SingleOnlyOutput)),
};
}
return Ok(Value::Array(results.into()));
}
if lookup.only {
return match results.len() {
0 => Ok(Value::None),
1 => Ok(results.into_iter().next().expect("Exactly one result in this branch")),
_ => Err(anyhow::anyhow!(Error::SingleOnlyOutput)),
};
}
Ok(Value::Array(results.into()))
}