use crate::ctx::Context; use crate::dbs::Options; use crate::dbs::Transaction; use crate::err::Error; use crate::sql::common::commas; use crate::sql::error::IResult; use crate::sql::fmt::{fmt_separated_by, Fmt}; use crate::sql::part::Next; use crate::sql::part::{all, field, first, graph, index, last, part, value, Part}; use crate::sql::paths::{ID, IN, META, OUT}; use crate::sql::value::Value; use md5::Digest; use md5::Md5; use nom::branch::alt; use nom::multi::separated_list1; use nom::multi::{many0, many1}; use serde::{Deserialize, Serialize}; use std::fmt::{self, Display, Formatter}; use std::ops::Deref; use std::str; pub(crate) const TOKEN: &str = "$surrealdb::private::sql::Idiom"; #[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)] pub struct Idioms(pub Vec); impl Deref for Idioms { type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } } impl Display for Idioms { fn fmt(&self, f: &mut Formatter) -> fmt::Result { Display::fmt(&Fmt::comma_separated(&self.0), f) } } pub fn locals(i: &str) -> IResult<&str, Idioms> { let (i, v) = separated_list1(commas, local)(i)?; Ok((i, Idioms(v))) } #[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)] #[serde(rename = "$surrealdb::private::sql::Idiom")] pub struct Idiom(pub Vec); impl Deref for Idiom { type Target = [Part]; fn deref(&self) -> &Self::Target { self.0.as_slice() } } impl From for Idiom { fn from(v: String) -> Self { Self(vec![Part::from(v)]) } } impl From> for Idiom { fn from(v: Vec) -> Self { Self(v) } } impl From<&[Part]> for Idiom { fn from(v: &[Part]) -> Self { Self(v.to_vec()) } } impl Idiom { /// Appends a part to the end of this Idiom pub(crate) fn push(mut self, n: Part) -> Idiom { self.0.push(n); self } /// Convert this Idiom to a unique hash pub(crate) fn to_hash(&self) -> String { let mut hasher = Md5::new(); hasher.update(self.to_string().as_str()); format!("{:x}", hasher.finalize()) } /// Convert this Idiom to a JSON Path string pub(crate) fn to_path(&self) -> String { format!("/{self}").replace(']', "").replace(&['.', '['][..], "/") } /// Simplifies this Idiom for use in object keys pub(crate) fn simplify(&self) -> Idiom { self.0 .iter() .cloned() .filter(|p| matches!(p, Part::Field(_) | Part::Value(_) | Part::Graph(_))) .collect::>() .into() } /// Check if this expression is an 'id' field pub(crate) fn is_id(&self) -> bool { self.0.len() == 1 && self.0[0].eq(&ID[0]) } /// Check if this expression is an 'in' field pub(crate) fn is_in(&self) -> bool { self.0.len() == 1 && self.0[0].eq(&IN[0]) } /// Check if this expression is an 'out' field pub(crate) fn is_out(&self) -> bool { self.0.len() == 1 && self.0[0].eq(&OUT[0]) } /// Check if this expression is an 'out' field pub(crate) fn is_meta(&self) -> bool { self.0.len() == 1 && self.0[0].eq(&META[0]) } /// Check if this is an expression with multiple yields pub(crate) fn is_multi_yield(&self) -> bool { self.iter().any(Self::split_multi_yield) } /// Check if the path part is a yield in a multi-yield expression pub(crate) fn split_multi_yield(v: &Part) -> bool { matches!(v, Part::Graph(g) if g.alias.is_some()) } } impl Idiom { pub(crate) async fn compute( &self, ctx: &Context<'_>, opt: &Options, txn: &Transaction, doc: Option<&Value>, ) -> Result { match self.first() { // The starting part is a value Some(Part::Value(v)) => { v.compute(ctx, opt, txn, doc) .await? .get(ctx, opt, txn, self.as_ref().next()) .await? .compute(ctx, opt, txn, doc) .await } // Otherwise use the current document _ => match doc { // There is a current document Some(v) => v.get(ctx, opt, txn, self).await?.compute(ctx, opt, txn, doc).await, // There isn't any document None => Ok(Value::None), }, } } } impl Display for Idiom { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Display::fmt( &Fmt::new( self.0.iter().enumerate().map(|args| { Fmt::new(args, |(i, p), f| match (i, p) { (0, Part::Field(v)) => Display::fmt(v, f), _ => Display::fmt(p, f), }) }), fmt_separated_by(""), ), f, ) } } /// Used in DEFINE FIELD and DEFINE INDEX clauses pub fn local(i: &str) -> IResult<&str, Idiom> { let (i, p) = first(i)?; let (i, mut v) = many0(alt((all, index, field)))(i)?; v.insert(0, p); Ok((i, Idiom::from(v))) } /// Used in a SPLIT, ORDER, and GROUP clauses pub fn basic(i: &str) -> IResult<&str, Idiom> { let (i, p) = first(i)?; let (i, mut v) = many0(alt((all, last, index, field)))(i)?; v.insert(0, p); Ok((i, Idiom::from(v))) } /// A simple idiom with one or more parts pub fn plain(i: &str) -> IResult<&str, Idiom> { let (i, p) = alt((first, graph))(i)?; let (i, mut v) = many0(part)(i)?; v.insert(0, p); Ok((i, Idiom::from(v))) } /// A complex idiom with graph or many parts pub fn multi(i: &str) -> IResult<&str, Idiom> { alt(( |i| { let (i, p) = graph(i)?; let (i, mut v) = many0(part)(i)?; v.insert(0, p); Ok((i, Idiom::from(v))) }, |i| { let (i, p) = alt((first, value))(i)?; let (i, mut v) = many1(part)(i)?; v.insert(0, p); Ok((i, Idiom::from(v))) }, ))(i) } /// A simple field based idiom pub fn path(i: &str) -> IResult<&str, Idiom> { let (i, p) = first(i)?; let (i, mut v) = many0(part)(i)?; v.insert(0, p); Ok((i, Idiom::from(v))) } /// A full complex idiom with any number of parts #[cfg(test)] pub fn idiom(i: &str) -> IResult<&str, Idiom> { alt((plain, multi))(i) } #[cfg(test)] mod tests { use super::*; use crate::sql::dir::Dir; use crate::sql::expression::Expression; use crate::sql::field::Fields; use crate::sql::graph::Graph; use crate::sql::number::Number; use crate::sql::param::Param; use crate::sql::table::Table; use crate::sql::test::Parse; use crate::sql::thing::Thing; #[test] fn idiom_number() { let sql = "13.495"; let res = idiom(sql); assert!(res.is_err()); } #[test] fn idiom_normal() { let sql = "test"; let res = idiom(sql); assert!(res.is_ok()); let out = res.unwrap().1; assert_eq!("test", format!("{}", out)); assert_eq!(out, Idiom(vec![Part::from("test")])); } #[test] fn idiom_quoted_backtick() { let sql = "`test`"; let res = idiom(sql); assert!(res.is_ok()); let out = res.unwrap().1; assert_eq!("test", format!("{}", out)); assert_eq!(out, Idiom(vec![Part::from("test")])); } #[test] fn idiom_quoted_brackets() { let sql = "⟨test⟩"; let res = idiom(sql); assert!(res.is_ok()); let out = res.unwrap().1; assert_eq!("test", format!("{}", out)); assert_eq!(out, Idiom(vec![Part::from("test")])); } #[test] fn idiom_nested() { let sql = "test.temp"; let res = idiom(sql); assert!(res.is_ok()); let out = res.unwrap().1; assert_eq!("test.temp", format!("{}", out)); assert_eq!(out, Idiom(vec![Part::from("test"), Part::from("temp")])); } #[test] fn idiom_nested_quoted() { let sql = "test.`some key`"; let res = idiom(sql); assert!(res.is_ok()); let out = res.unwrap().1; assert_eq!("test.`some key`", format!("{}", out)); assert_eq!(out, Idiom(vec![Part::from("test"), Part::from("some key")])); } #[test] fn idiom_nested_array_all() { let sql = "test.temp[*]"; let res = idiom(sql); assert!(res.is_ok()); let out = res.unwrap().1; assert_eq!("test.temp[*]", format!("{}", out)); assert_eq!(out, Idiom(vec![Part::from("test"), Part::from("temp"), Part::All])); } #[test] fn idiom_nested_array_last() { let sql = "test.temp[$]"; let res = idiom(sql); assert!(res.is_ok()); let out = res.unwrap().1; assert_eq!("test.temp[$]", format!("{}", out)); assert_eq!(out, Idiom(vec![Part::from("test"), Part::from("temp"), Part::Last])); } #[test] fn idiom_nested_array_value() { let sql = "test.temp[*].text"; let res = idiom(sql); assert!(res.is_ok()); let out = res.unwrap().1; assert_eq!("test.temp[*].text", format!("{}", out)); assert_eq!( out, Idiom(vec![Part::from("test"), Part::from("temp"), Part::All, Part::from("text")]) ); } #[test] fn idiom_nested_array_question() { let sql = "test.temp[? test = true].text"; let res = idiom(sql); assert!(res.is_ok()); let out = res.unwrap().1; assert_eq!("test.temp[WHERE test = true].text", format!("{}", out)); assert_eq!( out, Idiom(vec![ Part::from("test"), Part::from("temp"), Part::Where(Value::from(Expression::parse("test = true"))), Part::from("text") ]) ); } #[test] fn idiom_nested_array_condition() { let sql = "test.temp[WHERE test = true].text"; let res = idiom(sql); assert!(res.is_ok()); let out = res.unwrap().1; assert_eq!("test.temp[WHERE test = true].text", format!("{}", out)); assert_eq!( out, Idiom(vec![ Part::from("test"), Part::from("temp"), Part::Where(Value::from(Expression::parse("test = true"))), Part::from("text") ]) ); } #[test] fn idiom_start_param_local_field() { let sql = "$test.temporary[0].embedded"; let res = idiom(sql); assert!(res.is_ok()); let out = res.unwrap().1; assert_eq!("$test.temporary[0].embedded", format!("{}", out)); assert_eq!( out, Idiom(vec![ Part::Value(Param::from("test").into()), Part::from("temporary"), Part::Index(Number::Int(0)), Part::from("embedded"), ]) ); } #[test] fn idiom_start_thing_remote_traversal() { let sql = "person:test.friend->like->person"; let res = idiom(sql); assert!(res.is_ok()); let out = res.unwrap().1; assert_eq!("person:test.friend->like->person", format!("{}", out)); assert_eq!( out, Idiom(vec![ Part::Value(Thing::from(("person", "test")).into()), Part::from("friend"), Part::Graph(Graph { dir: Dir::Out, expr: Fields::all(), what: Table::from("like").into(), cond: None, alias: None, split: None, group: None, order: None, limit: None, start: None, }), Part::Graph(Graph { dir: Dir::Out, expr: Fields::all(), what: Table::from("person").into(), cond: None, alias: None, split: None, group: None, order: None, limit: None, start: None, }), ]) ); } }