Fix: Ensure path idioms are correct when looping over (#3363)

This commit is contained in:
Tobie Morgan Hitchcock 2024-01-22 20:48:35 +00:00 committed by GitHub
parent cf978bc4eb
commit 7f39754ec2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 135 additions and 30 deletions

View file

@ -130,6 +130,12 @@ impl Idiom {
pub(crate) fn split_multi_yield(v: &Part) -> bool { pub(crate) fn split_multi_yield(v: &Part) -> bool {
matches!(v, Part::Graph(g) if g.alias.is_some()) matches!(v, Part::Graph(g) if g.alias.is_some())
} }
/// Check if the path part is a yield in a multi-yield expression
pub(crate) fn remove_trailing_all(&mut self) {
if self.ends_with(&[Part::All]) {
self.0.truncate(self.len() - 1);
}
}
} }
impl Idiom { impl Idiom {

View file

@ -9,39 +9,49 @@ impl Value {
None => self._every(steps, arrays, Idiom::default()), None => self._every(steps, arrays, Idiom::default()),
} }
} }
fn _every(&self, steps: bool, arrays: bool, prev: Idiom) -> Vec<Idiom> { fn _every(&self, steps: bool, arrays: bool, mut prev: Idiom) -> Vec<Idiom> {
match self { match self {
// Current path part is an object and is not empty // Current path part is an object and is not empty
Value::Object(v) if !v.is_empty() => match steps { Value::Object(v) if !v.is_empty() => {
// Let's log all intermediary nodes // Remove any trailing * path parts
true if !prev.is_empty() => Some(prev.clone()) prev.remove_trailing_all();
.into_iter() // Check if we should log intermediary nodes
.chain(v.iter().flat_map(|(k, v)| { match steps {
let p = Part::from(k.to_owned()); // Let's log all intermediary nodes
v._every(steps, arrays, prev.clone().push(p)) true if !prev.is_empty() => Some(prev.clone())
})) .into_iter()
.collect::<Vec<_>>(), .chain(v.iter().flat_map(|(k, v)| {
// Let's not log intermediary nodes let p = Part::from(k.to_owned());
_ => v v._every(steps, arrays, prev.clone().push(p))
.iter() }))
.flat_map(|(k, v)| { .collect::<Vec<_>>(),
let p = Part::from(k.to_owned()); // Let's not log intermediary nodes
v._every(steps, arrays, prev.clone().push(p)) _ => v
}) .iter()
.collect::<Vec<_>>(), .flat_map(|(k, v)| {
}, let p = Part::from(k.to_owned());
v._every(steps, arrays, prev.clone().push(p))
})
.collect::<Vec<_>>(),
}
}
// Current path part is an array and is not empty // Current path part is an array and is not empty
Value::Array(v) if !v.is_empty() => match arrays { Value::Array(v) if !v.is_empty() => {
// Let's log all individual array items // Remove any trailing * path parts
true => std::iter::once(prev.clone()) prev.remove_trailing_all();
.chain(v.iter().enumerate().rev().flat_map(|(i, v)| { // Check if we should log individual array items
let p = Part::from(i.to_owned()); match arrays {
v._every(steps, arrays, prev.clone().push(p)) // Let's log all individual array items
})) true => std::iter::once(prev.clone())
.collect::<Vec<_>>(), .chain(v.iter().enumerate().rev().flat_map(|(i, v)| {
// Let's not log individual array items let p = Part::from(i.to_owned());
false => vec![prev], v._every(steps, arrays, prev.clone().push(p))
}, }))
.collect::<Vec<_>>(),
// Let's not log individual array items
false => vec![prev],
}
}
// Process everything else // Process everything else
_ => vec![prev], _ => vec![prev],
} }
@ -117,4 +127,23 @@ mod tests {
]; ];
assert_eq!(res, val.every(None, true, true)); assert_eq!(res, val.every(None, true, true));
} }
#[test]
fn every_including_intermediary_nodes_including_array_indexes_ending_all() {
let val = Value::parse("{ test: { something: [{ age: 34, tags: ['code', 'databases'] }, { age: 36, tags: ['design', 'operations'] }] } }");
let res = vec![
Idiom::parse("test.something"),
Idiom::parse("test.something[1]"),
Idiom::parse("test.something[1].age"),
Idiom::parse("test.something[1].tags"),
Idiom::parse("test.something[1].tags[1]"),
Idiom::parse("test.something[1].tags[0]"),
Idiom::parse("test.something[0]"),
Idiom::parse("test.something[0].age"),
Idiom::parse("test.something[0].tags"),
Idiom::parse("test.something[0].tags[1]"),
Idiom::parse("test.something[0].tags[0]"),
];
assert_eq!(res, val.every(Some(&Idiom::parse("test.something.*")), true, true));
}
} }

View file

@ -920,3 +920,73 @@ async fn field_definition_readonly() -> Result<(), Error> {
// //
Ok(()) Ok(())
} }
#[tokio::test]
async fn field_definition_flexible_array_any() -> Result<(), Error> {
let sql = "
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD custom ON user TYPE option<array>;
DEFINE FIELD custom.* ON user FLEXIBLE TYPE any;
CREATE user:one CONTENT { custom: ['sometext'] };
CREATE user:two CONTENT { custom: [ ['sometext'] ] };
CREATE user:three CONTENT { custom: [ { key: 'sometext' } ] };
";
let dbs = new_ds().await?.with_auth_enabled(true);
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 6);
//
let tmp = res.remove(0).result;
assert!(tmp.is_ok());
//
let tmp = res.remove(0).result;
assert!(tmp.is_ok());
//
let tmp = res.remove(0).result;
assert!(tmp.is_ok());
//
let tmp = res.remove(0).result?;
let val = Value::parse(
"[
{
custom: [
'sometext'
],
id: user:one
},
]",
);
assert_eq!(tmp, val);
//
let tmp = res.remove(0).result?;
let val = Value::parse(
"[
{
custom: [
[
'sometext'
]
],
id: user:two
},
]",
);
assert_eq!(tmp, val);
//
let tmp = res.remove(0).result?;
let val = Value::parse(
"[
{
custom: [
{
key: 'sometext'
}
],
id: user:three
}
]",
);
assert_eq!(tmp, val);
//
Ok(())
}