Idiom destructuring (#4443)

This commit is contained in:
Micha de Vries 2024-07-31 17:20:19 +02:00 committed by GitHub
parent 8f7392983f
commit eab548c5a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 299 additions and 2 deletions

View file

@ -1013,6 +1013,12 @@ pub enum Error {
Return { Return {
value: Value, 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<Error> for String { impl From<Error> for String {

View file

@ -1,4 +1,5 @@
use crate::idx::planner::executor::KnnExpressions; use crate::idx::planner::executor::KnnExpressions;
use crate::sql::part::DestructurePart;
use crate::sql::{ use crate::sql::{
Array, Cast, Cond, Expression, Function, Id, Idiom, Model, Object, Part, Range, Thing, Value, Array, Cast, Cond, Expression, Function, Id, Idiom, Model, Object, Part, Range, Thing, Value,
}; };
@ -70,6 +71,30 @@ impl<'a> KnnConditionRewriter<'a> {
Some(new_vec) Some(new_vec)
} }
fn eval_destructure_part(&self, part: &DestructurePart) -> Option<DestructurePart> {
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<Vec<DestructurePart>> {
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<Value> { fn eval_value_object(&self, o: &Object) -> Option<Value> {
self.eval_object(o).map(|o| o.into()) 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::Value(v) => self.eval_value(v).map(Part::Value),
Part::Start(v) => self.eval_value(v).map(Part::Start), 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::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),
} }
} }

View file

@ -10,9 +10,12 @@ use geo_types::{MultiLineString, MultiPoint, MultiPolygon};
use revision::revisioned; use revision::revisioned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::iter::once; use std::iter::once;
use std::{fmt, hash}; use std::{fmt, hash};
use super::Object;
pub(crate) const TOKEN: &str = "$surrealdb::private::sql::Geometry"; pub(crate) const TOKEN: &str = "$surrealdb::private::sql::Geometry";
#[revisioned(revision = 1)] #[revisioned(revision = 1)]
@ -116,6 +119,21 @@ impl Geometry {
Self::Collection(v) => collection(v), Self::Collection(v) => collection(v),
} }
} }
/// Get the GeoJSON object representation for this geometry
pub fn as_object(&self) -> Object {
let mut obj = BTreeMap::<String, Value>::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 { impl PartialOrd for Geometry {

View file

@ -2,9 +2,12 @@ use crate::sql::{fmt::Fmt, strand::no_nul_bytes, Graph, Ident, Idiom, Number, Va
use revision::revisioned; use revision::revisioned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
use std::fmt::Write;
use std::str; 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)] #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive] #[non_exhaustive]
@ -20,6 +23,8 @@ pub enum Part {
Value(Value), Value(Value),
Start(Value), Start(Value),
Method(#[serde(with = "no_nul_bytes")] String, Vec<Value>), Method(#[serde(with = "no_nul_bytes")] String, Vec<Value>),
#[revision(start = 2)]
Destructure(Vec<DestructurePart>),
} }
impl From<i32> for Part { impl From<i32> for Part {
@ -107,6 +112,22 @@ impl fmt::Display for Part {
Part::Graph(v) => write!(f, "{v}"), Part::Graph(v) => write!(f, "{v}"),
Part::Value(v) => write!(f, "[{v}]"), Part::Value(v) => write!(f, "[{v}]"),
Part::Method(v, a) => write!(f, ".{v}({})", Fmt::comma_separated(a)), 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<DestructurePart>),
}
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<Part> {
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()))
}
}
}
}

View file

@ -3,6 +3,7 @@ use crate::dbs::Options;
use crate::err::Error; use crate::err::Error;
use crate::exe::try_join_all_buffered; use crate::exe::try_join_all_buffered;
use crate::sql::array::Abolish; use crate::sql::array::Abolish;
use crate::sql::part::DestructurePart;
use crate::sql::part::Next; use crate::sql::part::Next;
use crate::sql::part::Part; use crate::sql::part::Part;
use crate::sql::value::Value; use crate::sql::value::Value;
@ -25,6 +26,19 @@ impl Value {
Some(p) => match self { Some(p) => match self {
// Current value at path is an object // Current value at path is an object
Value::Object(v) => match p { 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() { Part::Field(f) => match path.len() {
1 => { 1 => {
v.remove(f.as_str()); v.remove(f.as_str());
@ -64,6 +78,20 @@ impl Value {
}, },
_ => Ok(()), _ => 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(()), _ => Ok(()),
}, },
// Current value at path is an array // Current value at path is an array

View file

@ -40,6 +40,14 @@ impl Value {
None => Ok(()), None => Ok(()),
}, },
Part::All => stk.run(|stk| self.fetch(stk, ctx, opt, path.next())).await, 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(()), _ => Ok(()),
}, },
// Current path part is an array // Current path part is an array

View file

@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use crate::cnf::MAX_COMPUTATION_DEPTH; use crate::cnf::MAX_COMPUTATION_DEPTH;
use crate::ctx::Context; use crate::ctx::Context;
use crate::dbs::Options; use crate::dbs::Options;
@ -51,6 +53,10 @@ impl Value {
let v = v.as_coordinates(); let v = v.as_coordinates();
stk.run(|stk| v.get(stk, ctx, opt, doc, path.next())).await 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 // Otherwise return none
_ => Ok(Value::None), _ => Ok(Value::None),
}, },
@ -115,6 +121,19 @@ impl Value {
_ => Ok(Value::None), _ => Ok(Value::None),
}, },
Part::All => stk.run(|stk| self.get(stk, ctx, opt, doc, path.next())).await, Part::All => stk.run(|stk| self.get(stk, ctx, opt, doc, path.next())).await,
Part::Destructure(p) => {
let mut obj = BTreeMap::<String, Value>::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), _ => Ok(Value::None),
}, },
// Current value at path is an array // Current value at path is an array

View file

@ -1,7 +1,10 @@
use reblessive::Stk; use reblessive::Stk;
use crate::{ 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}, syn::token::{t, Span, TokenKind},
}; };
@ -252,10 +255,61 @@ impl Parser<'_> {
self.pop_peek(); self.pop_peek();
Part::All Part::All
} }
t!("{") => {
self.pop_peek();
self.parse_destructure_part()?
}
_ => Part::Field(self.next_token_value()?), _ => Part::Field(self.next_token_value()?),
}; };
Ok(res) Ok(res)
} }
/// Parse the part after the `.{` in an idiom
pub fn parse_destructure_part(&mut self) -> ParseResult<Part> {
let start = self.last_span();
let mut destructured: Vec<DestructurePart> = 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 /// Parse the part after the `[` in a idiom
pub async fn parse_bracket_part(&mut self, ctx: &mut Stk, start: Span) -> ParseResult<Part> { pub async fn parse_bracket_part(&mut self, ctx: &mut Stk, start: Span) -> ParseResult<Part> {
let res = match self.peek_kind() { let res = match self.peek_kind() {

View file

@ -1155,3 +1155,72 @@ async fn select_issue_3510() -> Result<(), Error> {
assert_eq!(format!("{:#}", tmp), format!("{:#}", val)); assert_eq!(format!("{:#}", tmp), format!("{:#}", val));
Ok(()) 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(())
}