Path processing improvements (#2250)
This commit is contained in:
parent
3a34821dc0
commit
a12348db8e
10 changed files with 386 additions and 121 deletions
|
@ -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,
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue