From a12348db8e099f25ec6f32ec3127b405825f1289 Mon Sep 17 00:00:00 2001 From: Tobie Morgan Hitchcock Date: Sat, 15 Jul 2023 08:08:26 +0100 Subject: [PATCH] Path processing improvements (#2250) --- lib/src/sql/idiom.rs | 14 +-- lib/src/sql/part.rs | 34 ++++--- lib/src/sql/value/cut.rs | 76 ++++----------- lib/src/sql/value/del.rs | 90 +++++++++++++++--- lib/src/sql/value/get.rs | 58 +++++++++--- lib/src/sql/value/pick.rs | 6 +- lib/src/sql/value/put.rs | 10 +- lib/src/sql/value/serde/ser/part/mod.rs | 8 ++ lib/src/sql/value/set.rs | 90 +++++++++++++++--- lib/tests/select.rs | 121 +++++++++++++++++++++++- 10 files changed, 386 insertions(+), 121 deletions(-) diff --git a/lib/src/sql/idiom.rs b/lib/src/sql/idiom.rs index d20e9e85..0749892b 100644 --- a/lib/src/sql/idiom.rs +++ b/lib/src/sql/idiom.rs @@ -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::>() .into() } @@ -137,7 +139,7 @@ impl Idiom { ) -> Result { 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, diff --git a/lib/src/sql/part.rs b/lib/src/sql/part.rs index 483ab152..5d3fcc44 100644 --- a/lib/src/sql/part.rs +++ b/lib/src/sql/part.rs @@ -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), } @@ -68,12 +69,6 @@ impl From for Part { } } -impl From for Part { - fn from(v: Value) -> Self { - Self::Where(v) - } -} - impl From 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))) diff --git a/lib/src/sql/value/cut.rs b/lib/src/sql/value/cut.rs index 705b62c7..3b982d2a 100644 --- a/lib/src/sql/value/cut.rs +++ b/lib/src/sql/value/cut.rs @@ -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); } } diff --git a/lib/src/sql/value/del.rs b/lib/src/sql/value/del.rs index a076ce19..4a0c75f9 100644 --- a/lib/src/sql/value/del.rs +++ b/lib/src/sql/value/del.rs @@ -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); + } } diff --git a/lib/src/sql/value/get.rs b/lib/src/sql/value/get.rs index 23ced18f..7cf0efd3 100644 --- a/lib/src/sql/value/get.rs +++ b/lib/src/sql/value/get.rs @@ -27,9 +27,9 @@ impl Value { path: &[Part], ) -> Result { 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; diff --git a/lib/src/sql/value/pick.rs b/lib/src/sql/value/pick.rs index b3a6dc01..e3379cf5 100644 --- a/lib/src/sql/value/pick.rs +++ b/lib/src/sql/value/pick.rs @@ -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::>().into(), Part::First => match v.first() { diff --git a/lib/src/sql/value/put.rs b/lib/src/sql/value/put.rs index 9dd7b967..224be006 100644 --- a/lib/src/sql/value/put.rs +++ b/lib/src/sql/value/put.rs @@ -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) diff --git a/lib/src/sql/value/serde/ser/part/mod.rs b/lib/src/sql/value/serde/ser/part/mod.rs index 5f3fb4a0..0825ea14 100644 --- a/lib/src/sql/value/serde/ser/part/mod.rs +++ b/lib/src/sql/value/serde/ser/part/mod.rs @@ -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()); diff --git a/lib/src/sql/value/set.rs b/lib/src/sql/value/set.rs index 1277ee45..cb404da2 100644 --- a/lib/src/sql/value/set.rs +++ b/lib/src/sql/value/set.rs @@ -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); + } } diff --git a/lib/tests/select.rs b/lib/tests/select.rs index 021de68a..c9adbc02 100644 --- a/lib/tests/select.rs +++ b/lib/tests/select.rs @@ -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;