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::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::part::{all, field, first, graph, index, last, part, start, Part};
use crate::sql::paths::{ID, IN, META, OUT};
use crate::sql::value::Value;
use md5::Digest;
@ -92,7 +92,9 @@ impl Idiom {
self.0
.iter()
.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<_>>()
.into()
}
@ -137,7 +139,7 @@ impl Idiom {
) -> Result<Value, Error> {
match self.first() {
// The starting part is a value
Some(Part::Value(v)) => {
Some(Part::Start(v)) => {
v.compute(ctx, opt, txn, doc)
.await?
.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)))
},
|i| {
let (i, p) = alt((first, value))(i)?;
let (i, p) = alt((first, start))(i)?;
let (i, mut v) = many1(part)(i)?;
v.insert(0, p);
Ok((i, Idiom::from(v)))
@ -381,7 +383,7 @@ mod tests {
assert_eq!(
out,
Idiom(vec![
Part::Value(Param::from("test").into()),
Part::Start(Param::from("test").into()),
Part::from("temporary"),
Part::Index(Number::Int(0)),
Part::from("embedded"),
@ -399,7 +401,7 @@ mod tests {
assert_eq!(
out,
Idiom(vec![
Part::Value(Thing::from(("person", "test")).into()),
Part::Start(Thing::from(("person", "test")).into()),
Part::from("friend"),
Part::Graph(Graph {
dir: Dir::Out,

View file

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

View file

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

View file

@ -22,9 +22,9 @@ impl Value {
path: &[Part],
) -> Result<(), Error> {
match path.first() {
// Get the current path part
// Get the current value at path
Some(p) => match self {
// Current path part is an object
// Current value at path is an object
Value::Object(v) => match p {
Part::Field(f) => match path.len() {
1 => {
@ -46,9 +46,22 @@ impl Value {
_ => 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(()),
},
// Current path part is an array
// Current value at path is an array
Value::Array(v) => match p {
Part::All => match path.len() {
1 => {
@ -114,16 +127,59 @@ impl Value {
v.abolish(|i| m.contains(&i));
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?;
_ => match path.next().first() {
Some(Part::Index(_)) => {
let mut a = Vec::new();
let mut p = Vec::new();
// Store the elements and positions to update
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));
@ -267,4 +323,16 @@ mod tests {
val.del(&ctx, &opt, &txn, &idi).await.unwrap();
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],
) -> Result<Self, Error> {
match path.first() {
// Get the current path part
// Get the current value at the path
Some(p) => match self {
// Current path part is a geometry
// Current value at path is a geometry
Value::Geometry(v) => match p {
// If this is the 'type' field then continue
Part::Field(f) if f.is_type() => {
@ -43,10 +43,10 @@ impl Value {
Part::Field(f) if f.is_geometries() && v.is_collection() => {
v.as_coordinates().get(ctx, opt, txn, doc, path.next()).await
}
// otherwise return none
// Otherwise return none
_ => Ok(Value::None),
},
// Current path part is a future
// Current value at path is a future
Value::Future(v) => {
// Check how many path parts are remaining
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 {
// 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 {
id: Id::Object(v),
..
@ -82,15 +82,27 @@ impl Value {
Some(v) => Value::Thing(v).get(ctx, opt, txn, doc, path).await,
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,
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,
_ => Ok(Value::None),
},
// Current path part is an array
// Current value at path is an array
Value::Array(v) => match p {
// Current path is an `*` part
Part::All => {
let path = path.next();
let futs = v.iter().map(|v| v.get(ctx, opt, txn, doc, path));
@ -109,22 +121,28 @@ impl Value {
None => Ok(Value::None),
},
Part::Where(w) => {
let path = path.next();
let mut a = Vec::new();
for v in v.iter() {
let cur = Some(CursorDoc::new(None, None, v));
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));
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) => {
// Clone the thing
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) => {
// Clone the thing
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]
async fn get_future_embedded_field() {
let (ctx, opt, txn) = mock().await;

View file

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

View file

@ -6,9 +6,9 @@ impl Value {
/// Synchronous method for setting a field on a `Value`
pub fn put(&mut self, path: &[Part], val: Value) {
match path.first() {
// Get the current path part
// Get the current value at path
Some(p) => match self {
// Current path part is an object
// Current value at path is an object
Value::Object(v) => match p {
Part::Graph(g) => match v.get_mut(g.to_raw().as_str()) {
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 {
Part::All => {
let path = path.next();
@ -61,12 +61,12 @@ impl Value {
v.iter_mut().for_each(|v| v.put(path, val.clone()));
}
},
// Current path part is empty
// Current value at path is empty
Value::Null => {
*self = Value::base();
self.put(path, val)
}
// Current path part is empty
// Current value at path is empty
Value::None => {
*self = Value::base();
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())?)),
"Where" => Ok(Part::Where(value.serialize(ser::value::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())?)),
variant => {
Err(Error::custom(format!("unexpected newtype variant `{name}::{variant}`")))
@ -119,6 +120,13 @@ mod tests {
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]
fn value() {
let part = Part::Value(sql::thing("foo:bar").unwrap().into());

View file

@ -21,9 +21,9 @@ impl Value {
val: Value,
) -> Result<(), Error> {
match path.first() {
// Get the current path part
// Get the current value at path
Some(p) => match self {
// Current path part is an object
// Current value at path is an object
Value::Object(v) => match p {
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,
@ -34,7 +34,7 @@ impl Value {
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,
_ => {
let mut obj = Value::base();
@ -52,9 +52,21 @@ impl Value {
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(()),
},
// Current path part is an array
// Current value at path is an array
Value::Array(v) => match p {
Part::All => {
let path = path.next();
@ -74,28 +86,58 @@ impl Value {
Some(v) => v.set(ctx, opt, txn, path.next(), val).await,
None => Ok(()),
},
Part::Where(w) => {
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?;
Part::Where(w) => match path.next().first() {
Some(Part::Index(_)) => {
let mut a = Vec::new();
let mut p = Vec::new();
// Store the elements and positions to update
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()));
try_join_all_buffered(futs).await?;
Ok(())
}
},
// Current path part is empty
// Current value at path is empty
Value::Null => {
*self = Value::base();
self.set(ctx, opt, txn, path, val).await
}
// Current path part is empty
// Current value at path is empty
Value::None => {
*self = Value::base();
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();
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(())
}
#[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]
async fn select_writeable_subqueries() -> Result<(), Error> {
let sql = "
@ -282,7 +401,7 @@ async fn select_where_field_is_bool() -> Result<(), Error> {
#[tokio::test]
async fn select_where_field_is_thing_and_with_index() -> Result<(), Error> {
let sql = "
CREATE person:tobie SET name = 'Tobie';
CREATE person:tobie SET name = 'Tobie';
DEFINE INDEX author ON TABLE post COLUMNS author;
CREATE post:1 SET author = person:tobie;
CREATE post:2 SET author = person:tobie;