Path processing improvements (#2250)

This commit is contained in:
Tobie Morgan Hitchcock 2023-07-15 08:08:26 +01:00 committed by GitHub
parent 3a34821dc0
commit a12348db8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 386 additions and 121 deletions

View file

@ -6,7 +6,7 @@ use crate::sql::common::commas;
use crate::sql::error::IResult; use crate::sql::error::IResult;
use crate::sql::fmt::{fmt_separated_by, Fmt}; use crate::sql::fmt::{fmt_separated_by, Fmt};
use crate::sql::part::Next; use crate::sql::part::Next;
use crate::sql::part::{all, field, first, graph, index, last, part, value, Part}; use crate::sql::part::{all, field, first, graph, index, last, part, start, Part};
use crate::sql::paths::{ID, IN, META, OUT}; use crate::sql::paths::{ID, IN, META, OUT};
use crate::sql::value::Value; use crate::sql::value::Value;
use md5::Digest; use md5::Digest;
@ -92,7 +92,9 @@ impl Idiom {
self.0 self.0
.iter() .iter()
.cloned() .cloned()
.filter(|p| matches!(p, Part::Field(_) | Part::Value(_) | Part::Graph(_))) .filter(|p| {
matches!(p, Part::Field(_) | Part::Start(_) | Part::Value(_) | Part::Graph(_))
})
.collect::<Vec<_>>() .collect::<Vec<_>>()
.into() .into()
} }
@ -137,7 +139,7 @@ impl Idiom {
) -> Result<Value, Error> { ) -> Result<Value, Error> {
match self.first() { match self.first() {
// The starting part is a value // The starting part is a value
Some(Part::Value(v)) => { Some(Part::Start(v)) => {
v.compute(ctx, opt, txn, doc) v.compute(ctx, opt, txn, doc)
.await? .await?
.get(ctx, opt, txn, doc, self.as_ref().next()) .get(ctx, opt, txn, doc, self.as_ref().next())
@ -209,7 +211,7 @@ pub fn multi(i: &str) -> IResult<&str, Idiom> {
Ok((i, Idiom::from(v))) Ok((i, Idiom::from(v)))
}, },
|i| { |i| {
let (i, p) = alt((first, value))(i)?; let (i, p) = alt((first, start))(i)?;
let (i, mut v) = many1(part)(i)?; let (i, mut v) = many1(part)(i)?;
v.insert(0, p); v.insert(0, p);
Ok((i, Idiom::from(v))) Ok((i, Idiom::from(v)))
@ -381,7 +383,7 @@ mod tests {
assert_eq!( assert_eq!(
out, out,
Idiom(vec![ Idiom(vec![
Part::Value(Param::from("test").into()), Part::Start(Param::from("test").into()),
Part::from("temporary"), Part::from("temporary"),
Part::Index(Number::Int(0)), Part::Index(Number::Int(0)),
Part::from("embedded"), Part::from("embedded"),
@ -399,7 +401,7 @@ mod tests {
assert_eq!( assert_eq!(
out, out,
Idiom(vec![ Idiom(vec![
Part::Value(Thing::from(("person", "test")).into()), Part::Start(Thing::from(("person", "test")).into()),
Part::from("friend"), Part::from("friend"),
Part::Graph(Graph { Part::Graph(Graph {
dir: Dir::Out, dir: Dir::Out,

View file

@ -5,16 +5,16 @@ use crate::sql::error::IResult;
use crate::sql::fmt::Fmt; use crate::sql::fmt::Fmt;
use crate::sql::graph::{self, Graph}; use crate::sql::graph::{self, Graph};
use crate::sql::ident::{self, Ident}; use crate::sql::ident::{self, Ident};
use crate::sql::idiom::Idiom; use crate::sql::idiom::{self, Idiom};
use crate::sql::number::{number, Number}; use crate::sql::number::{number, Number};
use crate::sql::strand::no_nul_bytes; use crate::sql::param::{self};
use crate::sql::strand::{self, no_nul_bytes};
use crate::sql::value::{self, Value}; use crate::sql::value::{self, Value};
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::tag; use nom::bytes::complete::tag;
use nom::bytes::complete::tag_no_case; use nom::bytes::complete::tag_no_case;
use nom::character::complete::char; use nom::character::complete::char;
use nom::combinator::not; use nom::combinator::{map, not, peek};
use nom::combinator::peek;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
use std::str; use std::str;
@ -29,6 +29,7 @@ pub enum Part {
Where(Value), Where(Value),
Graph(Graph), Graph(Graph),
Value(Value), Value(Value),
Start(Value),
Method(#[serde(with = "no_nul_bytes")] String, Vec<Value>), Method(#[serde(with = "no_nul_bytes")] String, Vec<Value>),
} }
@ -68,12 +69,6 @@ impl From<Ident> for Part {
} }
} }
impl From<Value> for Part {
fn from(v: Value) -> Self {
Self::Where(v)
}
}
impl From<Graph> for Part { impl From<Graph> for Part {
fn from(v: Graph) -> Self { fn from(v: Graph) -> Self {
Self::Graph(v) Self::Graph(v)
@ -93,6 +88,7 @@ impl Part {
/// Check if we require a writeable transaction /// Check if we require a writeable transaction
pub(crate) fn writeable(&self) -> bool { pub(crate) fn writeable(&self) -> bool {
match self { match self {
Part::Start(v) => v.writeable(),
Part::Where(v) => v.writeable(), Part::Where(v) => v.writeable(),
Part::Value(v) => v.writeable(), Part::Value(v) => v.writeable(),
Part::Method(_, v) => v.iter().any(Value::writeable), Part::Method(_, v) => v.iter().any(Value::writeable),
@ -114,11 +110,12 @@ impl fmt::Display for Part {
Part::All => f.write_str("[*]"), Part::All => f.write_str("[*]"),
Part::Last => f.write_str("[$]"), Part::Last => f.write_str("[$]"),
Part::First => f.write_str("[0]"), Part::First => f.write_str("[0]"),
Part::Start(v) => write!(f, "{v}"),
Part::Field(v) => write!(f, ".{v}"), Part::Field(v) => write!(f, ".{v}"),
Part::Index(v) => write!(f, "[{v}]"), Part::Index(v) => write!(f, "[{v}]"),
Part::Where(v) => write!(f, "[WHERE {v}]"), Part::Where(v) => write!(f, "[WHERE {v}]"),
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)),
} }
} }
@ -142,7 +139,7 @@ impl<'a> Next<'a> for &'a [Part] {
// ------------------------------ // ------------------------------
pub fn part(i: &str) -> IResult<&str, Part> { pub fn part(i: &str) -> IResult<&str, Part> {
alt((all, last, index, field, graph, filter))(i) alt((all, last, index, field, value, graph, filter))(i)
} }
pub fn first(i: &str) -> IResult<&str, Part> { pub fn first(i: &str) -> IResult<&str, Part> {
@ -200,10 +197,21 @@ pub fn filter(i: &str) -> IResult<&str, Part> {
} }
pub fn value(i: &str) -> IResult<&str, Part> { pub fn value(i: &str) -> IResult<&str, Part> {
let (i, v) = value::start(i)?; let (i, _) = openbracket(i)?;
let (i, v) = alt((
map(strand::strand, Value::Strand),
map(param::param, Value::Param),
map(idiom::basic, Value::Idiom),
))(i)?;
let (i, _) = closebracket(i)?;
Ok((i, Part::Value(v))) Ok((i, Part::Value(v)))
} }
pub fn start(i: &str) -> IResult<&str, Part> {
let (i, v) = value::start(i)?;
Ok((i, Part::Start(v)))
}
pub fn graph(i: &str) -> IResult<&str, Part> { pub fn graph(i: &str) -> IResult<&str, Part> {
let (i, v) = graph::graph(i)?; let (i, v) = graph::graph(i)?;
Ok((i, Part::Graph(v))) Ok((i, Part::Graph(v)))

View file

@ -6,9 +6,9 @@ impl Value {
/// Synchronous method for deleting a field from a `Value` /// Synchronous method for deleting a field from a `Value`
pub(crate) fn cut(&mut self, path: &[Part]) { pub(crate) fn cut(&mut self, path: &[Part]) {
if let Some(p) = path.first() { if let Some(p) = path.first() {
// Get the current path part // Get the current value at path
match self { match self {
// Current path part is an object // Current value at path is an object
Value::Object(v) => match p { Value::Object(v) => match p {
Part::Field(f) => match path.len() { Part::Field(f) => match path.len() {
1 => { 1 => {
@ -32,7 +32,7 @@ impl Value {
}, },
_ => {} _ => {}
}, },
// Current path part is an array // Current value at path is an array
Value::Array(v) => match p { Value::Array(v) => match p {
Part::All => match path.len() { Part::All => match path.len() {
1 => { 1 => {
@ -96,127 +96,93 @@ impl Value {
mod tests { mod tests {
use super::*; use super::*;
use crate::dbs::test::mock;
use crate::sql::idiom::Idiom; use crate::sql::idiom::Idiom;
use crate::sql::test::Parse; use crate::sql::test::Parse;
#[tokio::test] #[tokio::test]
async fn del_none() { async fn cut_none() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::default(); let idi = Idiom::default();
let mut val = Value::parse("{ test: { other: null, something: 123 } }"); let mut val = Value::parse("{ test: { other: null, something: 123 } }");
let res = Value::parse("{ test: { other: null, something: 123 } }"); let res = Value::parse("{ test: { other: null, something: 123 } }");
val.del(&ctx, &opt, &txn, &idi).await.unwrap(); val.cut(&idi);
assert_eq!(res, val); assert_eq!(res, val);
} }
#[tokio::test] #[tokio::test]
async fn del_reset() { async fn cut_reset() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test"); let idi = Idiom::parse("test");
let mut val = Value::parse("{ test: { other: null, something: 123 } }"); let mut val = Value::parse("{ test: { other: null, something: 123 } }");
let res = Value::parse("{ }"); let res = Value::parse("{ }");
val.del(&ctx, &opt, &txn, &idi).await.unwrap(); val.cut(&idi);
assert_eq!(res, val); assert_eq!(res, val);
} }
#[tokio::test] #[tokio::test]
async fn del_basic() { async fn cut_basic() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.something"); let idi = Idiom::parse("test.something");
let mut val = Value::parse("{ test: { other: null, something: 123 } }"); let mut val = Value::parse("{ test: { other: null, something: 123 } }");
let res = Value::parse("{ test: { other: null } }"); let res = Value::parse("{ test: { other: null } }");
val.del(&ctx, &opt, &txn, &idi).await.unwrap(); val.cut(&idi);
assert_eq!(res, val); assert_eq!(res, val);
} }
#[tokio::test] #[tokio::test]
async fn del_wrong() { async fn cut_wrong() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.something.wrong"); let idi = Idiom::parse("test.something.wrong");
let mut val = Value::parse("{ test: { other: null, something: 123 } }"); let mut val = Value::parse("{ test: { other: null, something: 123 } }");
let res = Value::parse("{ test: { other: null, something: 123 } }"); let res = Value::parse("{ test: { other: null, something: 123 } }");
val.del(&ctx, &opt, &txn, &idi).await.unwrap(); val.cut(&idi);
assert_eq!(res, val); assert_eq!(res, val);
} }
#[tokio::test] #[tokio::test]
async fn del_other() { async fn cut_other() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.other.something"); let idi = Idiom::parse("test.other.something");
let mut val = Value::parse("{ test: { other: null, something: 123 } }"); let mut val = Value::parse("{ test: { other: null, something: 123 } }");
let res = Value::parse("{ test: { other: null, something: 123 } }"); let res = Value::parse("{ test: { other: null, something: 123 } }");
val.del(&ctx, &opt, &txn, &idi).await.unwrap(); val.cut(&idi);
assert_eq!(res, val); assert_eq!(res, val);
} }
#[tokio::test] #[tokio::test]
async fn del_array() { async fn cut_array() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.something[1]"); let idi = Idiom::parse("test.something[1]");
let mut val = Value::parse("{ test: { something: [123, 456, 789] } }"); let mut val = Value::parse("{ test: { something: [123, 456, 789] } }");
let res = Value::parse("{ test: { something: [123, 789] } }"); let res = Value::parse("{ test: { something: [123, 789] } }");
val.del(&ctx, &opt, &txn, &idi).await.unwrap(); val.cut(&idi);
assert_eq!(res, val); assert_eq!(res, val);
} }
#[tokio::test] #[tokio::test]
async fn del_array_field() { async fn cut_array_field() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.something[1].age"); let idi = Idiom::parse("test.something[1].age");
let mut val = Value::parse( let mut val = Value::parse(
"{ test: { something: [{ name: 'A', age: 34 }, { name: 'B', age: 36 }] } }", "{ test: { something: [{ name: 'A', age: 34 }, { name: 'B', age: 36 }] } }",
); );
let res = Value::parse("{ test: { something: [{ name: 'A', age: 34 }, { name: 'B' }] } }"); let res = Value::parse("{ test: { something: [{ name: 'A', age: 34 }, { name: 'B' }] } }");
val.del(&ctx, &opt, &txn, &idi).await.unwrap(); val.cut(&idi);
assert_eq!(res, val); assert_eq!(res, val);
} }
#[tokio::test] #[tokio::test]
async fn del_array_fields() { async fn cut_array_fields() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.something[*].age"); let idi = Idiom::parse("test.something[*].age");
let mut val = Value::parse( let mut val = Value::parse(
"{ test: { something: [{ name: 'A', age: 34 }, { name: 'B', age: 36 }] } }", "{ test: { something: [{ name: 'A', age: 34 }, { name: 'B', age: 36 }] } }",
); );
let res = Value::parse("{ test: { something: [{ name: 'A' }, { name: 'B' }] } }"); let res = Value::parse("{ test: { something: [{ name: 'A' }, { name: 'B' }] } }");
val.del(&ctx, &opt, &txn, &idi).await.unwrap(); val.cut(&idi);
assert_eq!(res, val); assert_eq!(res, val);
} }
#[tokio::test] #[tokio::test]
async fn del_array_fields_flat() { async fn cut_array_fields_flat() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.something.age"); let idi = Idiom::parse("test.something.age");
let mut val = Value::parse( let mut val = Value::parse(
"{ test: { something: [{ name: 'A', age: 34 }, { name: 'B', age: 36 }] } }", "{ test: { something: [{ name: 'A', age: 34 }, { name: 'B', age: 36 }] } }",
); );
let res = Value::parse("{ test: { something: [{ name: 'A' }, { name: 'B' }] } }"); let res = Value::parse("{ test: { something: [{ name: 'A' }, { name: 'B' }] } }");
val.del(&ctx, &opt, &txn, &idi).await.unwrap(); val.cut(&idi);
assert_eq!(res, val);
}
#[tokio::test]
async fn del_array_where_field() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.something[WHERE age > 35].age");
let mut val = Value::parse(
"{ test: { something: [{ name: 'A', age: 34 }, { name: 'B', age: 36 }] } }",
);
let res = Value::parse("{ test: { something: [{ name: 'A', age: 34 }, { name: 'B' }] } }");
val.del(&ctx, &opt, &txn, &idi).await.unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn del_array_where_fields() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.something[WHERE age > 35]");
let mut val = Value::parse(
"{ test: { something: [{ name: 'A', age: 34 }, { name: 'B', age: 36 }] } }",
);
let res = Value::parse("{ test: { something: [{ name: 'A', age: 34 }] } }");
val.del(&ctx, &opt, &txn, &idi).await.unwrap();
assert_eq!(res, val); assert_eq!(res, val);
} }
} }

View file

@ -22,9 +22,9 @@ impl Value {
path: &[Part], path: &[Part],
) -> Result<(), Error> { ) -> Result<(), Error> {
match path.first() { match path.first() {
// Get the current path part // Get the current value at path
Some(p) => match self { Some(p) => match self {
// Current path part is an object // Current value at path is an object
Value::Object(v) => match p { Value::Object(v) => match p {
Part::Field(f) => match path.len() { Part::Field(f) => match path.len() {
1 => { 1 => {
@ -46,9 +46,22 @@ impl Value {
_ => Ok(()), _ => Ok(()),
}, },
}, },
Part::Value(x) => match x.compute(ctx, opt, txn, None).await? {
Value::Strand(f) => match path.len() {
1 => {
v.remove(f.as_str());
Ok(())
}
_ => match v.get_mut(f.as_str()) {
Some(v) if v.is_some() => v.del(ctx, opt, txn, path.next()).await,
_ => Ok(()),
},
},
_ => Ok(()),
},
_ => Ok(()), _ => Ok(()),
}, },
// Current path part is an array // Current value at path is an array
Value::Array(v) => match p { Value::Array(v) => match p {
Part::All => match path.len() { Part::All => match path.len() {
1 => { 1 => {
@ -114,16 +127,59 @@ impl Value {
v.abolish(|i| m.contains(&i)); v.abolish(|i| m.contains(&i));
Ok(()) Ok(())
} }
_ => { _ => match path.next().first() {
let path = path.next(); Some(Part::Index(_)) => {
for v in v.iter_mut() { let mut a = Vec::new();
let cur = CursorDoc::new(None, None, v); let mut p = Vec::new();
if w.compute(ctx, opt, txn, Some(&cur)).await?.is_truthy() { // Store the elements and positions to update
v.del(ctx, opt, txn, path).await?; for (i, o) in v.iter_mut().enumerate() {
let cur = CursorDoc::new(None, None, o);
if w.compute(ctx, opt, txn, Some(&cur)).await?.is_truthy() {
a.push(o.clone());
p.push(i);
}
} }
// Convert the matched elements array to a value
let mut a = Value::from(a);
// Set the new value on the matches elements
a.del(ctx, opt, txn, path.next()).await?;
// Push the new values into the original array
for (i, p) in p.into_iter().enumerate().rev() {
match a.pick(&[Part::Index(i.into())]) {
Value::None => {
v.remove(i);
}
x => v[p] = x,
}
}
Ok(())
} }
Ok(()) _ => {
} let path = path.next();
for v in v.iter_mut() {
let cur = CursorDoc::new(None, None, v);
if w.compute(ctx, opt, txn, Some(&cur)).await?.is_truthy() {
v.del(ctx, opt, txn, path).await?;
}
}
Ok(())
}
},
},
Part::Value(x) => match x.compute(ctx, opt, txn, None).await? {
Value::Number(i) => match path.len() {
1 => {
if v.len().gt(&i.to_usize()) {
v.remove(i.to_usize());
}
Ok(())
}
_ => match v.get_mut(i.to_usize()) {
Some(v) => v.del(ctx, opt, txn, path.next()).await,
None => Ok(()),
},
},
_ => Ok(()),
}, },
_ => { _ => {
let futs = v.iter_mut().map(|v| v.del(ctx, opt, txn, path)); let futs = v.iter_mut().map(|v| v.del(ctx, opt, txn, path));
@ -267,4 +323,16 @@ mod tests {
val.del(&ctx, &opt, &txn, &idi).await.unwrap(); val.del(&ctx, &opt, &txn, &idi).await.unwrap();
assert_eq!(res, val); assert_eq!(res, val);
} }
#[tokio::test]
async fn del_array_where_fields_array_index() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.something[WHERE age > 30][0]");
let mut val = Value::parse(
"{ test: { something: [{ name: 'A', age: 34 }, { name: 'B', age: 36 }] } }",
);
let res = Value::parse("{ test: { something: [{ name: 'B', age: 36 }] } }");
val.del(&ctx, &opt, &txn, &idi).await.unwrap();
assert_eq!(res, val);
}
} }

View file

@ -27,9 +27,9 @@ impl Value {
path: &[Part], path: &[Part],
) -> Result<Self, Error> { ) -> Result<Self, Error> {
match path.first() { match path.first() {
// Get the current path part // Get the current value at the path
Some(p) => match self { Some(p) => match self {
// Current path part is a geometry // Current value at path is a geometry
Value::Geometry(v) => match p { Value::Geometry(v) => match p {
// If this is the 'type' field then continue // If this is the 'type' field then continue
Part::Field(f) if f.is_type() => { Part::Field(f) if f.is_type() => {
@ -43,10 +43,10 @@ impl Value {
Part::Field(f) if f.is_geometries() && v.is_collection() => { Part::Field(f) if f.is_geometries() && v.is_collection() => {
v.as_coordinates().get(ctx, opt, txn, doc, path.next()).await v.as_coordinates().get(ctx, opt, txn, doc, path.next()).await
} }
// otherwise return none // Otherwise return none
_ => Ok(Value::None), _ => Ok(Value::None),
}, },
// Current path part is a future // Current value at path is a future
Value::Future(v) => { Value::Future(v) => {
// Check how many path parts are remaining // Check how many path parts are remaining
match path.len() { match path.len() {
@ -63,10 +63,10 @@ impl Value {
} }
} }
} }
// Current path part is an object // Current value at path is an object
Value::Object(v) => match p { Value::Object(v) => match p {
// If requesting an `id` field, check if it is a complex Record ID // If requesting an `id` field, check if it is a complex Record ID
Part::Field(f) if f.is_id() && path.len() > 1 => match v.get(f as &str) { Part::Field(f) if f.is_id() && path.len() > 1 => match v.get(f.as_str()) {
Some(Value::Thing(Thing { Some(Value::Thing(Thing {
id: Id::Object(v), id: Id::Object(v),
.. ..
@ -82,15 +82,27 @@ impl Value {
Some(v) => Value::Thing(v).get(ctx, opt, txn, doc, path).await, Some(v) => Value::Thing(v).get(ctx, opt, txn, doc, path).await,
None => Ok(Value::None), None => Ok(Value::None),
}, },
Part::Field(f) => match v.get(f as &str) { Part::Field(f) => match v.get(f.as_str()) {
Some(v) => v.get(ctx, opt, txn, doc, path.next()).await, Some(v) => v.get(ctx, opt, txn, doc, path.next()).await,
None => Ok(Value::None), None => Ok(Value::None),
}, },
Part::Index(i) => match v.get(&i.to_string()) {
Some(v) => v.get(ctx, opt, txn, doc, path.next()).await,
None => Ok(Value::None),
},
Part::Value(x) => match x.compute(ctx, opt, txn, doc).await? {
Value::Strand(f) => match v.get(f.as_str()) {
Some(v) => v.get(ctx, opt, txn, doc, path.next()).await,
None => Ok(Value::None),
},
_ => Ok(Value::None),
},
Part::All => self.get(ctx, opt, txn, doc, path.next()).await, Part::All => self.get(ctx, opt, txn, doc, path.next()).await,
_ => Ok(Value::None), _ => Ok(Value::None),
}, },
// Current path part is an array // Current value at path is an array
Value::Array(v) => match p { Value::Array(v) => match p {
// Current path is an `*` part
Part::All => { Part::All => {
let path = path.next(); let path = path.next();
let futs = v.iter().map(|v| v.get(ctx, opt, txn, doc, path)); let futs = v.iter().map(|v| v.get(ctx, opt, txn, doc, path));
@ -109,22 +121,28 @@ impl Value {
None => Ok(Value::None), None => Ok(Value::None),
}, },
Part::Where(w) => { Part::Where(w) => {
let path = path.next();
let mut a = Vec::new(); let mut a = Vec::new();
for v in v.iter() { for v in v.iter() {
let cur = Some(CursorDoc::new(None, None, v)); let cur = Some(CursorDoc::new(None, None, v));
if w.compute(ctx, opt, txn, cur.as_ref()).await?.is_truthy() { if w.compute(ctx, opt, txn, cur.as_ref()).await?.is_truthy() {
a.push(v.get(ctx, opt, txn, cur.as_ref(), path).await?) a.push(v.clone());
} }
} }
Ok(a.into()) Value::from(a).get(ctx, opt, txn, doc, path.next()).await
} }
Part::Value(x) => match x.compute(ctx, opt, txn, doc).await? {
Value::Number(i) => match v.get(i.to_usize()) {
Some(v) => v.get(ctx, opt, txn, doc, path.next()).await,
None => Ok(Value::None),
},
_ => Ok(Value::None),
},
_ => { _ => {
let futs = v.iter().map(|v| v.get(ctx, opt, txn, doc, path)); let futs = v.iter().map(|v| v.get(ctx, opt, txn, doc, path));
try_join_all_buffered(futs).await.map(Into::into) try_join_all_buffered(futs).await.map(Into::into)
} }
}, },
// Current path part is an edges // Current value at path is an edges
Value::Edges(v) => { Value::Edges(v) => {
// Clone the thing // Clone the thing
let val = v.clone(); let val = v.clone();
@ -147,7 +165,7 @@ impl Value {
} }
} }
} }
// Current path part is a thing // Current value at path is a thing
Value::Thing(v) => { Value::Thing(v) => {
// Clone the thing // Clone the thing
let val = v.clone(); let val = v.clone();
@ -330,6 +348,20 @@ mod tests {
); );
} }
#[tokio::test]
async fn get_array_where_fields_array_index() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.something[WHERE age > 30][0]");
let val = Value::parse("{ test: { something: [{ age: 34 }, { age: 36 }] } }");
let res = val.get(&ctx, &opt, &txn, None, &idi).await.unwrap();
assert_eq!(
res,
Value::from(map! {
"age".to_string() => Value::from(34),
})
);
}
#[tokio::test] #[tokio::test]
async fn get_future_embedded_field() { async fn get_future_embedded_field() {
let (ctx, opt, txn) = mock().await; let (ctx, opt, txn) = mock().await;

View file

@ -6,9 +6,9 @@ impl Value {
/// Synchronous method for getting a field from a `Value` /// Synchronous method for getting a field from a `Value`
pub fn pick(&self, path: &[Part]) -> Self { pub fn pick(&self, path: &[Part]) -> Self {
match path.first() { match path.first() {
// Get the current path part // Get the current value at path
Some(p) => match self { Some(p) => match self {
// Current path part is an object // Current value at path is an object
Value::Object(v) => match p { Value::Object(v) => match p {
Part::Field(f) => match v.get(f as &str) { Part::Field(f) => match v.get(f as &str) {
Some(v) => v.pick(path.next()), Some(v) => v.pick(path.next()),
@ -21,7 +21,7 @@ impl Value {
Part::All => self.pick(path.next()), Part::All => self.pick(path.next()),
_ => Value::None, _ => Value::None,
}, },
// Current path part is an array // Current value at path is an array
Value::Array(v) => match p { Value::Array(v) => match p {
Part::All => v.iter().map(|v| v.pick(path.next())).collect::<Vec<_>>().into(), Part::All => v.iter().map(|v| v.pick(path.next())).collect::<Vec<_>>().into(),
Part::First => match v.first() { Part::First => match v.first() {

View file

@ -6,9 +6,9 @@ impl Value {
/// Synchronous method for setting a field on a `Value` /// Synchronous method for setting a field on a `Value`
pub fn put(&mut self, path: &[Part], val: Value) { pub fn put(&mut self, path: &[Part], val: Value) {
match path.first() { match path.first() {
// Get the current path part // Get the current value at path
Some(p) => match self { Some(p) => match self {
// Current path part is an object // Current value at path is an object
Value::Object(v) => match p { Value::Object(v) => match p {
Part::Graph(g) => match v.get_mut(g.to_raw().as_str()) { Part::Graph(g) => match v.get_mut(g.to_raw().as_str()) {
Some(v) if v.is_some() => v.put(path.next(), val), Some(v) if v.is_some() => v.put(path.next(), val),
@ -36,7 +36,7 @@ impl Value {
}, },
_ => (), _ => (),
}, },
// Current path part is an array // Current value at path is an array
Value::Array(v) => match p { Value::Array(v) => match p {
Part::All => { Part::All => {
let path = path.next(); let path = path.next();
@ -61,12 +61,12 @@ impl Value {
v.iter_mut().for_each(|v| v.put(path, val.clone())); v.iter_mut().for_each(|v| v.put(path, val.clone()));
} }
}, },
// Current path part is empty // Current value at path is empty
Value::Null => { Value::Null => {
*self = Value::base(); *self = Value::base();
self.put(path, val) self.put(path, val)
} }
// Current path part is empty // Current value at path is empty
Value::None => { Value::None => {
*self = Value::base(); *self = Value::base();
self.put(path, val) self.put(path, val)

View file

@ -55,6 +55,7 @@ impl ser::Serializer for Serializer {
"Index" => Ok(Part::Index(value.serialize(ser::number::Serializer.wrap())?)), "Index" => Ok(Part::Index(value.serialize(ser::number::Serializer.wrap())?)),
"Where" => Ok(Part::Where(value.serialize(ser::value::Serializer.wrap())?)), "Where" => Ok(Part::Where(value.serialize(ser::value::Serializer.wrap())?)),
"Graph" => Ok(Part::Graph(value.serialize(ser::graph::Serializer.wrap())?)), "Graph" => Ok(Part::Graph(value.serialize(ser::graph::Serializer.wrap())?)),
"Start" => Ok(Part::Start(value.serialize(ser::value::Serializer.wrap())?)),
"Value" => Ok(Part::Value(value.serialize(ser::value::Serializer.wrap())?)), "Value" => Ok(Part::Value(value.serialize(ser::value::Serializer.wrap())?)),
variant => { variant => {
Err(Error::custom(format!("unexpected newtype variant `{name}::{variant}`"))) Err(Error::custom(format!("unexpected newtype variant `{name}::{variant}`")))
@ -119,6 +120,13 @@ mod tests {
assert_eq!(part, serialized); assert_eq!(part, serialized);
} }
#[test]
fn start() {
let part = Part::Start(sql::thing("foo:bar").unwrap().into());
let serialized = part.serialize(Serializer.wrap()).unwrap();
assert_eq!(part, serialized);
}
#[test] #[test]
fn value() { fn value() {
let part = Part::Value(sql::thing("foo:bar").unwrap().into()); let part = Part::Value(sql::thing("foo:bar").unwrap().into());

View file

@ -21,9 +21,9 @@ impl Value {
val: Value, val: Value,
) -> Result<(), Error> { ) -> Result<(), Error> {
match path.first() { match path.first() {
// Get the current path part // Get the current value at path
Some(p) => match self { Some(p) => match self {
// Current path part is an object // Current value at path is an object
Value::Object(v) => match p { Value::Object(v) => match p {
Part::Graph(g) => match v.get_mut(g.to_raw().as_str()) { Part::Graph(g) => match v.get_mut(g.to_raw().as_str()) {
Some(v) if v.is_some() => v.set(ctx, opt, txn, path.next(), val).await, Some(v) if v.is_some() => v.set(ctx, opt, txn, path.next(), val).await,
@ -34,7 +34,7 @@ impl Value {
Ok(()) Ok(())
} }
}, },
Part::Field(f) => match v.get_mut(f as &str) { Part::Field(f) => match v.get_mut(f.as_str()) {
Some(v) if v.is_some() => v.set(ctx, opt, txn, path.next(), val).await, Some(v) if v.is_some() => v.set(ctx, opt, txn, path.next(), val).await,
_ => { _ => {
let mut obj = Value::base(); let mut obj = Value::base();
@ -52,9 +52,21 @@ impl Value {
Ok(()) Ok(())
} }
}, },
Part::Value(x) => match x.compute(ctx, opt, txn, None).await? {
Value::Strand(f) => match v.get_mut(f.as_str()) {
Some(v) if v.is_some() => v.set(ctx, opt, txn, path.next(), val).await,
_ => {
let mut obj = Value::base();
obj.set(ctx, opt, txn, path.next(), val).await?;
v.insert(f.to_string(), obj);
Ok(())
}
},
_ => Ok(()),
},
_ => Ok(()), _ => Ok(()),
}, },
// Current path part is an array // Current value at path is an array
Value::Array(v) => match p { Value::Array(v) => match p {
Part::All => { Part::All => {
let path = path.next(); let path = path.next();
@ -74,28 +86,58 @@ impl Value {
Some(v) => v.set(ctx, opt, txn, path.next(), val).await, Some(v) => v.set(ctx, opt, txn, path.next(), val).await,
None => Ok(()), None => Ok(()),
}, },
Part::Where(w) => { Part::Where(w) => match path.next().first() {
let path = path.next(); Some(Part::Index(_)) => {
for v in v.iter_mut() { let mut a = Vec::new();
let cur = CursorDoc::new(None, None, v); let mut p = Vec::new();
if w.compute(ctx, opt, txn, Some(&cur)).await?.is_truthy() { // Store the elements and positions to update
v.set(ctx, opt, txn, path, val.clone()).await?; for (i, o) in v.iter_mut().enumerate() {
let cur = CursorDoc::new(None, None, o);
if w.compute(ctx, opt, txn, Some(&cur)).await?.is_truthy() {
a.push(o.clone());
p.push(i);
}
} }
// Convert the matched elements array to a value
let mut a = Value::from(a);
// Set the new value on the matches elements
a.set(ctx, opt, txn, path.next(), val.clone()).await?;
// Push the new values into the original array
for (i, p) in p.into_iter().enumerate() {
v[p] = a.pick(&[Part::Index(i.into())]);
}
Ok(())
} }
Ok(()) _ => {
} let path = path.next();
for v in v.iter_mut() {
let cur = CursorDoc::new(None, None, v);
if w.compute(ctx, opt, txn, Some(&cur)).await?.is_truthy() {
v.set(ctx, opt, txn, path, val.clone()).await?;
}
}
Ok(())
}
},
Part::Value(x) => match x.compute(ctx, opt, txn, None).await? {
Value::Number(i) => match v.get_mut(i.to_usize()) {
Some(v) => v.set(ctx, opt, txn, path.next(), val).await,
None => Ok(()),
},
_ => Ok(()),
},
_ => { _ => {
let futs = v.iter_mut().map(|v| v.set(ctx, opt, txn, path, val.clone())); let futs = v.iter_mut().map(|v| v.set(ctx, opt, txn, path, val.clone()));
try_join_all_buffered(futs).await?; try_join_all_buffered(futs).await?;
Ok(()) Ok(())
} }
}, },
// Current path part is empty // Current value at path is empty
Value::Null => { Value::Null => {
*self = Value::base(); *self = Value::base();
self.set(ctx, opt, txn, path, val).await self.set(ctx, opt, txn, path, val).await
} }
// Current path part is empty // Current value at path is empty
Value::None => { Value::None => {
*self = Value::base(); *self = Value::base();
self.set(ctx, opt, txn, path, val).await self.set(ctx, opt, txn, path, val).await
@ -259,4 +301,24 @@ mod tests {
val.set(&ctx, &opt, &txn, &idi, Value::from(21)).await.unwrap(); val.set(&ctx, &opt, &txn, &idi, Value::from(21)).await.unwrap();
assert_eq!(res, val); assert_eq!(res, val);
} }
#[tokio::test]
async fn set_array_where_fields_array_index() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.something[WHERE age > 30][0]");
let mut val = Value::parse("{ test: { something: [{ age: 34 }, { age: 36 }] } }");
let res = Value::parse("{ test: { something: [21, { age: 36 }] } }");
val.set(&ctx, &opt, &txn, &idi, Value::from(21)).await.unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn set_array_where_fields_array_index_field() {
let (ctx, opt, txn) = mock().await;
let idi = Idiom::parse("test.something[WHERE age > 30][0].age");
let mut val = Value::parse("{ test: { something: [{ age: 34 }, { age: 36 }] } }");
let res = Value::parse("{ test: { something: [{ age: 21 }, { age: 36 }] } }");
val.set(&ctx, &opt, &txn, &idi, Value::from(21)).await.unwrap();
assert_eq!(res, val);
}
} }

View file

@ -143,6 +143,125 @@ async fn select_expression_value() -> Result<(), Error> {
Ok(()) Ok(())
} }
#[tokio::test]
async fn select_dynamic_array_keys_and_object_keys() -> Result<(), Error> {
let sql = "
LET $lang = 'en';
UPDATE documentation:test CONTENT {
primarylang: 'en',
languages: {
'en': 'this is english',
'es': 'esto es español',
'de': 'das ist Englisch',
},
tags: [
{ type: 'library', value: 'client-side' },
{ type: 'library', value: 'server-side' },
{ type: 'environment', value: 'frontend' },
]
};
-- An array filter, followed by an array index operation
SELECT tags[WHERE type = 'library'][0].value FROM documentation:test;
-- Selecting an object value or array index using a string as a key
SELECT languages['en'] AS content FROM documentation:test;
-- Updating an object value or array index using a string as a key
UPDATE documentation:test SET languages['en'] = 'my primary text';
-- Selecting an object value or array index using a parameter as a key
SELECT languages[$lang] AS content FROM documentation:test;
-- Updating an object value or array index using a parameter as a key
UPDATE documentation:test SET languages[$lang] = 'my secondary text';
-- Selecting an object or array index value using the value of another document field as a key
SELECT languages[primarylang] AS content FROM documentation;
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 8);
//
let tmp = res.remove(0).result;
assert!(tmp.is_ok());
//
let tmp = res.remove(0).result?;
let val = Value::parse(
"[
{
id: documentation:test,
languages: {
de: 'das ist Englisch',
en: 'this is english',
es: 'esto es español',
},
primarylang: 'en',
tags: [
{
type: 'library',
value: 'client-side',
},
{
type: 'library',
value: 'server-side',
},
{
type: 'environment',
value: 'frontend',
}
]
}
]",
);
assert_eq!(tmp, val);
//
let tmp = res.remove(0).result?;
let val = Value::parse(
"[
{
tags: {
value: 'client-side'
}
}
]",
);
assert_eq!(tmp, val);
//
let tmp = res.remove(0).result?;
let val = Value::parse(
"[
{
content: 'this is english'
}
]",
);
assert_eq!(tmp, val);
//
let tmp = res.remove(0).result;
assert!(tmp.is_ok());
//
let tmp = res.remove(0).result?;
let val = Value::parse(
"[
{
content: 'my primary text'
}
]",
);
assert_eq!(tmp, val);
//
let tmp = res.remove(0).result;
assert!(tmp.is_ok());
//
let tmp = res.remove(0).result?;
let val = Value::parse(
"[
{
content: 'my secondary text'
}
]",
);
assert_eq!(tmp, val);
//
Ok(())
}
#[tokio::test] #[tokio::test]
async fn select_writeable_subqueries() -> Result<(), Error> { async fn select_writeable_subqueries() -> Result<(), Error> {
let sql = " let sql = "