From eab548c5a30abde7321d96f1eb08b102ea220461 Mon Sep 17 00:00:00 2001 From: Micha de Vries Date: Wed, 31 Jul 2024 17:20:19 +0200 Subject: [PATCH] Idiom destructuring (#4443) --- core/src/err/mod.rs | 6 +++ core/src/idx/planner/rewriter.rs | 26 ++++++++++++ core/src/sql/geometry.rs | 18 ++++++++ core/src/sql/part.rs | 71 +++++++++++++++++++++++++++++++- core/src/sql/value/del.rs | 28 +++++++++++++ core/src/sql/value/fetch.rs | 8 ++++ core/src/sql/value/get.rs | 19 +++++++++ core/src/syn/parser/idiom.rs | 56 ++++++++++++++++++++++++- lib/tests/select.rs | 69 +++++++++++++++++++++++++++++++ 9 files changed, 299 insertions(+), 2 deletions(-) diff --git a/core/src/err/mod.rs b/core/src/err/mod.rs index eb4ff810..7c3bae64 100644 --- a/core/src/err/mod.rs +++ b/core/src/err/mod.rs @@ -1013,6 +1013,12 @@ pub enum Error { Return { value: Value, }, + + /// A destructuring variant was used in a context where it is not supported + #[error("{variant} destructuring method is not supported here")] + UnsupportedDestructure { + variant: String, + }, } impl From for String { diff --git a/core/src/idx/planner/rewriter.rs b/core/src/idx/planner/rewriter.rs index 5bf43620..b72a46aa 100644 --- a/core/src/idx/planner/rewriter.rs +++ b/core/src/idx/planner/rewriter.rs @@ -1,4 +1,5 @@ use crate::idx::planner::executor::KnnExpressions; +use crate::sql::part::DestructurePart; use crate::sql::{ Array, Cast, Cond, Expression, Function, Id, Idiom, Model, Object, Part, Range, Thing, Value, }; @@ -70,6 +71,30 @@ impl<'a> KnnConditionRewriter<'a> { Some(new_vec) } + fn eval_destructure_part(&self, part: &DestructurePart) -> Option { + match part { + DestructurePart::Aliased(f, v) => { + self.eval_idiom(v).map(|v| DestructurePart::Aliased(f.clone(), v)) + } + DestructurePart::Destructure(f, v) => { + self.eval_destructure_parts(v).map(|v| DestructurePart::Destructure(f.clone(), v)) + } + p => Some(p.clone()), + } + } + + fn eval_destructure_parts(&self, parts: &[DestructurePart]) -> Option> { + let mut new_vec = Vec::with_capacity(parts.len()); + for part in parts { + if let Some(part) = self.eval_destructure_part(part) { + new_vec.push(part); + } else { + return None; + } + } + Some(new_vec) + } + fn eval_value_object(&self, o: &Object) -> Option { self.eval_object(o).map(|o| o.into()) } @@ -132,6 +157,7 @@ impl<'a> KnnConditionRewriter<'a> { Part::Value(v) => self.eval_value(v).map(Part::Value), Part::Start(v) => self.eval_value(v).map(Part::Start), Part::Method(n, p) => self.eval_values(p).map(|v| Part::Method(n.clone(), v)), + Part::Destructure(p) => self.eval_destructure_parts(p).map(Part::Destructure), } } diff --git a/core/src/sql/geometry.rs b/core/src/sql/geometry.rs index 715ecf64..82027257 100644 --- a/core/src/sql/geometry.rs +++ b/core/src/sql/geometry.rs @@ -10,9 +10,12 @@ use geo_types::{MultiLineString, MultiPoint, MultiPolygon}; use revision::revisioned; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; +use std::collections::BTreeMap; use std::iter::once; use std::{fmt, hash}; +use super::Object; + pub(crate) const TOKEN: &str = "$surrealdb::private::sql::Geometry"; #[revisioned(revision = 1)] @@ -116,6 +119,21 @@ impl Geometry { Self::Collection(v) => collection(v), } } + /// Get the GeoJSON object representation for this geometry + pub fn as_object(&self) -> Object { + let mut obj = BTreeMap::::new(); + obj.insert("type".into(), self.as_type().into()); + obj.insert( + match self { + Self::Collection(_) => "geometries", + _ => "coordinates", + } + .into(), + self.as_coordinates(), + ); + + obj.into() + } } impl PartialOrd for Geometry { diff --git a/core/src/sql/part.rs b/core/src/sql/part.rs index 0535dee5..829c9909 100644 --- a/core/src/sql/part.rs +++ b/core/src/sql/part.rs @@ -2,9 +2,12 @@ use crate::sql::{fmt::Fmt, strand::no_nul_bytes, Graph, Ident, Idiom, Number, Va use revision::revisioned; use serde::{Deserialize, Serialize}; use std::fmt; +use std::fmt::Write; use std::str; -#[revisioned(revision = 1)] +use super::fmt::{is_pretty, pretty_indent}; + +#[revisioned(revision = 2)] #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] @@ -20,6 +23,8 @@ pub enum Part { Value(Value), Start(Value), Method(#[serde(with = "no_nul_bytes")] String, Vec), + #[revision(start = 2)] + Destructure(Vec), } impl From for Part { @@ -107,6 +112,22 @@ impl fmt::Display for Part { Part::Graph(v) => write!(f, "{v}"), Part::Value(v) => write!(f, "[{v}]"), Part::Method(v, a) => write!(f, ".{v}({})", Fmt::comma_separated(a)), + Part::Destructure(v) => { + f.write_str(".{")?; + if !is_pretty() { + f.write_char(' ')?; + } + if !v.is_empty() { + let indent = pretty_indent(); + write!(f, "{}", Fmt::pretty_comma_separated(v))?; + drop(indent); + } + if is_pretty() { + f.write_char('}') + } else { + f.write_str(" }") + } + } } } } @@ -125,3 +146,51 @@ impl<'a> Next<'a> for &'a [Part] { } } } + +// ------------------------------ + +#[revisioned(revision = 1)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +pub enum DestructurePart { + All(Ident), + Field(Ident), + Aliased(Ident, Idiom), + Destructure(Ident, Vec), +} + +impl DestructurePart { + pub fn field(&self) -> &Ident { + match self { + DestructurePart::All(v) => v, + DestructurePart::Field(v) => v, + DestructurePart::Aliased(v, _) => v, + DestructurePart::Destructure(v, _) => v, + } + } + + pub fn path(&self) -> Vec { + match self { + DestructurePart::All(v) => vec![Part::Field(v.clone()), Part::All], + DestructurePart::Field(v) => vec![Part::Field(v.clone())], + DestructurePart::Aliased(_, v) => v.0.clone(), + DestructurePart::Destructure(f, d) => { + vec![Part::Field(f.clone()), Part::Destructure(d.clone())] + } + } + } +} + +impl fmt::Display for DestructurePart { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DestructurePart::All(fd) => write!(f, "{fd}.*"), + DestructurePart::Field(fd) => write!(f, "{fd}"), + DestructurePart::Aliased(fd, v) => write!(f, "{fd}: {v}"), + DestructurePart::Destructure(fd, d) => { + write!(f, "{fd}{}", Part::Destructure(d.clone())) + } + } + } +} diff --git a/core/src/sql/value/del.rs b/core/src/sql/value/del.rs index 9f2ed6ac..94f99988 100644 --- a/core/src/sql/value/del.rs +++ b/core/src/sql/value/del.rs @@ -3,6 +3,7 @@ use crate::dbs::Options; use crate::err::Error; use crate::exe::try_join_all_buffered; use crate::sql::array::Abolish; +use crate::sql::part::DestructurePart; use crate::sql::part::Next; use crate::sql::part::Part; use crate::sql::value::Value; @@ -25,6 +26,19 @@ impl Value { Some(p) => match self { // Current value at path is an object Value::Object(v) => match p { + Part::All => match path.len() { + 1 => { + v.clear(); + Ok(()) + } + _ => { + let path = path.next(); + for v in v.values_mut() { + stk.run(|stk| v.del(stk, ctx, opt, path)).await?; + } + Ok(()) + } + }, Part::Field(f) => match path.len() { 1 => { v.remove(f.as_str()); @@ -64,6 +78,20 @@ impl Value { }, _ => Ok(()), }, + Part::Destructure(parts) => { + for part in parts { + if matches!(part, DestructurePart::Aliased(_, _)) { + return Err(Error::UnsupportedDestructure { + variant: "An aliased".into(), + }); + } + + let path = [part.path().as_slice(), path.next()].concat(); + stk.run(|stk| self.del(stk, ctx, opt, &path)).await?; + } + + Ok(()) + } _ => Ok(()), }, // Current value at path is an array diff --git a/core/src/sql/value/fetch.rs b/core/src/sql/value/fetch.rs index cda36bf9..f4540a9e 100644 --- a/core/src/sql/value/fetch.rs +++ b/core/src/sql/value/fetch.rs @@ -40,6 +40,14 @@ impl Value { None => Ok(()), }, Part::All => stk.run(|stk| self.fetch(stk, ctx, opt, path.next())).await, + Part::Destructure(p) => { + for p in p.iter() { + let path = [(p.path().as_slice()), path].concat(); + stk.run(|stk| self.fetch(stk, ctx, opt, &path)).await?; + } + + Ok(()) + } _ => Ok(()), }, // Current path part is an array diff --git a/core/src/sql/value/get.rs b/core/src/sql/value/get.rs index 9f708311..519bf94a 100644 --- a/core/src/sql/value/get.rs +++ b/core/src/sql/value/get.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use crate::cnf::MAX_COMPUTATION_DEPTH; use crate::ctx::Context; use crate::dbs::Options; @@ -51,6 +53,10 @@ impl Value { let v = v.as_coordinates(); stk.run(|stk| v.get(stk, ctx, opt, doc, path.next())).await } + Part::Destructure(_) => { + let obj = Value::Object(v.as_object()); + stk.run(|stk| obj.get(stk, ctx, opt, doc, path)).await + } // Otherwise return none _ => Ok(Value::None), }, @@ -115,6 +121,19 @@ impl Value { _ => Ok(Value::None), }, Part::All => stk.run(|stk| self.get(stk, ctx, opt, doc, path.next())).await, + Part::Destructure(p) => { + let mut obj = BTreeMap::::new(); + for p in p.iter() { + let path = p.path(); + let v = stk + .run(|stk| self.get(stk, ctx, opt, doc, path.as_slice())) + .await?; + obj.insert(p.field().to_raw(), v); + } + + let obj = Value::from(obj); + stk.run(|stk| obj.get(stk, ctx, opt, doc, path.next())).await + } _ => Ok(Value::None), }, // Current value at path is an array diff --git a/core/src/syn/parser/idiom.rs b/core/src/syn/parser/idiom.rs index 7c15f18d..0e3c8355 100644 --- a/core/src/syn/parser/idiom.rs +++ b/core/src/syn/parser/idiom.rs @@ -1,7 +1,10 @@ use reblessive::Stk; use crate::{ - sql::{Dir, Edges, Field, Fields, Graph, Ident, Idiom, Part, Table, Tables, Value}, + sql::{ + part::DestructurePart, Dir, Edges, Field, Fields, Graph, Ident, Idiom, Part, Table, Tables, + Value, + }, syn::token::{t, Span, TokenKind}, }; @@ -252,10 +255,61 @@ impl Parser<'_> { self.pop_peek(); Part::All } + t!("{") => { + self.pop_peek(); + self.parse_destructure_part()? + } _ => Part::Field(self.next_token_value()?), }; Ok(res) } + /// Parse the part after the `.{` in an idiom + pub fn parse_destructure_part(&mut self) -> ParseResult { + let start = self.last_span(); + let mut destructured: Vec = Vec::new(); + loop { + if self.eat(t!("}")) { + // We've reached the end of the destructure + break; + } + + let field: Ident = self.next_token_value()?; + let part = match self.peek_kind() { + t!(":") => { + self.pop_peek(); + DestructurePart::Aliased(field, self.parse_local_idiom()?) + } + t!(".") => { + self.pop_peek(); + let found = self.peek_kind(); + match self.parse_dot_part()? { + Part::All => DestructurePart::All(field), + Part::Destructure(v) => DestructurePart::Destructure(field, v), + _ => { + return Err(ParseError::new( + ParseErrorKind::Unexpected { + found, + expected: "a star or a destructuring", + }, + self.last_span(), + )) + } + } + } + _ => DestructurePart::Field(field), + }; + + destructured.push(part); + + if !self.eat(t!(",")) { + // We've reached the end of the destructure + self.expect_closing_delimiter(t!("}"), start)?; + break; + } + } + + Ok(Part::Destructure(destructured)) + } /// Parse the part after the `[` in a idiom pub async fn parse_bracket_part(&mut self, ctx: &mut Stk, start: Span) -> ParseResult { let res = match self.peek_kind() { diff --git a/lib/tests/select.rs b/lib/tests/select.rs index 5a986a13..3848c89c 100644 --- a/lib/tests/select.rs +++ b/lib/tests/select.rs @@ -1155,3 +1155,72 @@ async fn select_issue_3510() -> Result<(), Error> { assert_eq!(format!("{:#}", tmp), format!("{:#}", val)); Ok(()) } + +#[tokio::test] +async fn select_destructure() -> Result<(), Error> { + let sql = " + CREATE person:1 SET name = 'John', age = 21, obj = { a: 1, b: 2, c: { d: 3, e: 4, f: 5 } }; + SELECT obj.{ a, c.{ e, f } } FROM person; + SELECT * OMIT obj.c.{ d, f } FROM person; + "; + let dbs = new_ds().await?; + let ses = Session::owner().with_ns("test").with_db("test"); + let res = &mut dbs.execute(sql, &ses, None).await?; + assert_eq!(res.len(), 3); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + id: person:1, + name: 'John', + age: 21, + obj: { + a: 1, + b: 2, + c: { + d: 3, + e: 4, + f: 5 + } + } + } + ]", + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + obj: { + a: 1, + c: { + e: 4, + f: 5 + } + } + } + ]", + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + id: person:1, + name: 'John', + age: 21, + obj: { + a: 1, + b: 2, + c: { e: 4 } + } + } + ]", + ); + assert_eq!(tmp, val); + // + Ok(()) +}