use std::sync::Arc;
use surrealdb_types::{SqlFormat, ToSql};
use crate::exec::physical_expr::{EvalContext, PhysicalExpr};
use crate::exec::{AccessMode, BoxFut, ContextLevel};
use crate::expr::FlowResult;
use crate::val::Value;
#[derive(Debug, Clone)]
pub struct IndexPart {
pub expr: Arc<dyn PhysicalExpr>,
}
impl PhysicalExpr for IndexPart {
fn name(&self) -> &'static str {
"Index"
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn required_context(&self) -> ContextLevel {
self.expr.required_context()
}
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);
let index_ctx = if let Some(doc) = ctx.document_root {
ctx.with_value(doc)
} else {
ctx
};
let index = self.expr.evaluate(index_ctx).await?;
Ok(evaluate_index(value, &index)?)
})
}
fn access_mode(&self) -> AccessMode {
self.expr.access_mode()
}
}
impl ToSql for IndexPart {
fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
f.push('[');
self.expr.fmt_sql(f, fmt);
f.push(']');
}
}
pub(crate) fn evaluate_index(value: &Value, index: &Value) -> anyhow::Result<Value> {
use crate::val::record_id::RecordIdKey;
match (value, index) {
(Value::Array(arr), Value::Number(n)) => {
Ok(n.as_array_index().and_then(|idx| arr.get(idx).cloned()).unwrap_or(Value::None))
}
(Value::Set(set), Value::Number(n)) => {
Ok(n.as_array_index().and_then(|idx| set.nth(idx).cloned()).unwrap_or(Value::None))
}
(Value::Array(arr), Value::Range(range)) => {
let slice = range
.as_ref()
.clone()
.coerce_to_typed::<i64>()
.map_err(|e| anyhow::anyhow!("Invalid range: {}", e))?
.slice(arr.as_slice())
.map(|s| Value::Array(s.to_vec().into()))
.unwrap_or(Value::None);
Ok(slice)
}
(Value::Object(obj), Value::String(key)) => {
Ok(obj.get(key.as_str()).cloned().unwrap_or(Value::None))
}
(Value::Object(obj), Value::Number(n)) => {
let key = n.to_string();
Ok(obj.get(&key).cloned().unwrap_or(Value::None))
}
(Value::RecordId(rid), Value::Number(n)) => match &rid.key {
RecordIdKey::Array(arr) => {
Ok(n.as_array_index().and_then(|idx| arr.get(idx).cloned()).unwrap_or(Value::None))
}
_ => Ok(Value::None),
},
_ => Ok(Value::None),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::exec::operators::test_util::{eval_on, physical_expr, root_ctx, val};
use crate::val::Set;
#[tokio::test]
async fn a_negative_or_fractional_numeric_index_is_none() {
let arr = val("[1, 2, 3]").await;
assert_eq!(evaluate_index(&arr, &Value::from(-1)).unwrap(), Value::None);
assert_eq!(evaluate_index(&arr, &Value::from(1.0)).unwrap(), Value::from(2));
assert_eq!(evaluate_index(&arr, &Value::from(1.5)).unwrap(), Value::None);
}
#[tokio::test]
async fn a_set_is_indexed_in_its_own_sorted_order() {
let set =
Value::Set(Set::from_iter(vec![Value::from(30), Value::from(10), Value::from(20)]));
assert_eq!(evaluate_index(&set, &Value::from(0)).unwrap(), Value::from(10));
assert_eq!(evaluate_index(&set, &Value::from(2)).unwrap(), Value::from(30));
assert_eq!(evaluate_index(&set, &Value::from(3)).unwrap(), Value::None);
}
#[tokio::test]
async fn an_object_is_indexed_by_string_key_or_by_the_stringified_number() {
let obj = val("{ a: 1, '2': 'two' }").await;
assert_eq!(evaluate_index(&obj, &Value::from("a")).unwrap(), Value::from(1));
assert_eq!(evaluate_index(&obj, &Value::from("nope")).unwrap(), Value::None);
assert_eq!(evaluate_index(&obj, &Value::from(2)).unwrap(), Value::from("two"));
assert_eq!(evaluate_index(&obj, &Value::from(0)).unwrap(), Value::None);
}
#[tokio::test]
async fn a_range_index_slices_an_array() {
let arr = val("[1, 2, 3]").await;
let range = val("0..2").await;
assert!(matches!(range, Value::Range(_)), "expected a range, got {range:?}");
assert_eq!(evaluate_index(&arr, &range).unwrap(), val("[1, 2]").await);
assert_eq!(evaluate_index(&arr, &val("0..=1").await).unwrap(), val("[1, 2]").await);
assert_eq!(evaluate_index(&arr, &val("1..").await).unwrap(), val("[2, 3]").await);
assert_eq!(evaluate_index(&arr, &val("..").await).unwrap(), val("[1, 2, 3]").await);
assert_eq!(evaluate_index(&arr, &val("1..1").await).unwrap(), val("[]").await);
}
#[tokio::test]
async fn a_range_reaching_past_the_end_yields_none_not_a_truncated_slice() {
let arr = val("[1, 2, 3]").await;
assert_eq!(evaluate_index(&arr, &val("0..10").await).unwrap(), Value::None);
assert_eq!(evaluate_index(&arr, &val("5..").await).unwrap(), Value::None);
}
#[tokio::test]
async fn a_range_whose_bounds_are_not_integers_is_an_error() {
let arr = val("[1, 2, 3]").await;
let err = evaluate_index(&arr, &val("'a'..'b'").await).unwrap_err();
assert!(err.to_string().contains("Invalid range"), "got {err}");
}
#[tokio::test]
async fn only_array_keyed_record_ids_support_numeric_indexing() {
let array_keyed = val("doc:['a', 'b']").await;
assert_eq!(evaluate_index(&array_keyed, &Value::from(1)).unwrap(), Value::from("b"));
assert_eq!(evaluate_index(&array_keyed, &Value::from(9)).unwrap(), Value::None);
assert_eq!(evaluate_index(&val("doc:1").await, &Value::from(0)).unwrap(), Value::None);
assert_eq!(evaluate_index(&val("doc:abc").await, &Value::from(0)).unwrap(), Value::None);
}
#[tokio::test]
async fn an_index_of_an_unsupported_type_or_container_is_none() {
let arr = val("[1, 2, 3]").await;
let obj = val("{ a: 1 }").await;
assert_eq!(evaluate_index(&arr, &Value::from("a")).unwrap(), Value::None);
assert_eq!(evaluate_index(&obj, &Value::Bool(true)).unwrap(), Value::None);
assert_eq!(evaluate_index(&arr, &Value::Bool(true)).unwrap(), Value::None);
assert_eq!(evaluate_index(&Value::from(42), &Value::from(0)).unwrap(), Value::None);
assert_eq!(evaluate_index(&Value::None, &Value::from(0)).unwrap(), Value::None);
assert_eq!(evaluate_index(&obj, &val("0..1").await).unwrap(), Value::None);
}
#[tokio::test]
async fn a_dynamic_key_resolves_against_the_document_root_not_the_chain_position() {
let ctx = root_ctx();
let doc = val("{ key: 'b', nested: { key: 'a', a: 1, b: 2 } }").await;
assert_eq!(eval_on("nested[key]", &doc, &ctx).await.unwrap(), Value::from(2));
let doc = val("{ key: 'b', nested: { a: 1, b: 2 } }").await;
assert_eq!(eval_on("nested[key]", &doc, &ctx).await.unwrap(), Value::from(2));
}
#[tokio::test]
async fn a_param_key_is_read_from_the_execution_context() {
let ctx = root_ctx().with_param("k", Value::from("b"));
let doc = val("{ nested: { a: 1, b: 2 } }").await;
assert_eq!(eval_on("nested[$k]", &doc, &ctx).await.unwrap(), Value::from(2));
let ctx = root_ctx().with_param("i", Value::from(1));
let doc = val("{ list: [10, 20, 30] }").await;
assert_eq!(eval_on("list[$i]", &doc, &ctx).await.unwrap(), Value::from(20));
let ctx = root_ctx().with_param("k", Value::from("nope"));
let doc = val("{ nested: { a: 1 } }").await;
assert_eq!(eval_on("nested[$k]", &doc, &ctx).await.unwrap(), Value::None);
}
#[tokio::test]
async fn a_computed_key_expression_is_evaluated_before_indexing() {
let ctx = root_ctx();
let doc = val("{ offset: 1, list: [10, 20, 30] }").await;
assert_eq!(eval_on("list[offset + 1]", &doc, &ctx).await.unwrap(), Value::from(30));
}
#[tokio::test]
async fn an_error_in_the_key_expression_propagates() {
let ctx = root_ctx();
let doc = val("{ list: [1, 2, 3] }").await;
let err = eval_on("list[(THROW 'boom')]", &doc, &ctx).await.unwrap_err();
assert!(err.to_string().contains("boom"), "got {err}");
}
#[tokio::test]
async fn metadata_is_delegated_to_the_key_expression() {
let ctx = root_ctx();
let literal_key = IndexPart {
expr: physical_expr("1", &ctx).await,
};
assert_eq!(literal_key.required_context(), ContextLevel::Root);
assert_eq!(literal_key.access_mode(), AccessMode::ReadOnly);
let field_key = IndexPart {
expr: physical_expr("k", &ctx).await,
};
assert_eq!(field_key.required_context(), ContextLevel::Database);
assert_eq!(field_key.access_mode(), AccessMode::ReadOnly);
}
}