From f5b21eb3633fdbbaa64b6247fdfeb606afbd35d1 Mon Sep 17 00:00:00 2001 From: Tobie Morgan Hitchcock Date: Wed, 4 Jan 2023 09:24:14 +0000 Subject: [PATCH] Add support for `FLEXIBLE` fields on `SCHEMAFULL` tables Closes #1341 --- lib/src/doc/clean.rs | 19 +++-- lib/src/sql/idiom.rs | 6 ++ lib/src/sql/statements/define.rs | 20 +++++- lib/src/sql/value/every.rs | 15 ++-- lib/src/sql/value/merge.rs | 2 +- lib/tests/field.rs | 118 +++++++++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 12 deletions(-) diff --git a/lib/src/doc/clean.rs b/lib/src/doc/clean.rs index 823bf520..306071a1 100644 --- a/lib/src/doc/clean.rs +++ b/lib/src/doc/clean.rs @@ -22,13 +22,24 @@ impl<'a> Document<'a> { let mut keys: Vec = vec![]; // Loop through all field statements for fd in self.fd(opt, txn).await?.iter() { - // Loop over this field in the document - for k in self.current.each(&fd.name).into_iter() { - keys.push(k); + // Is this a schemaless field? + match fd.flex { + false => { + // Loop over this field in the document + for k in self.current.each(&fd.name).into_iter() { + keys.push(k); + } + } + true => { + // Loop over every field under this field in the document + for k in self.current.every(Some(&fd.name), true, true).into_iter() { + keys.push(k); + } + } } } // Loop over every field in the document - for fd in self.current.every(true, true).iter() { + for fd in self.current.every(None, true, true).iter() { if !keys.contains(fd) { match fd { fd if fd.is_id() => continue, diff --git a/lib/src/sql/idiom.rs b/lib/src/sql/idiom.rs index 1a95a322..8f91d6f5 100644 --- a/lib/src/sql/idiom.rs +++ b/lib/src/sql/idiom.rs @@ -62,6 +62,12 @@ impl From> for Idiom { } } +impl From<&[Part]> for Idiom { + fn from(v: &[Part]) -> Self { + Self(v.to_vec()) + } +} + impl Idiom { /// Appends a part to the end of this Idiom pub(crate) fn push(mut self, n: Part) -> Idiom { diff --git a/lib/src/sql/statements/define.rs b/lib/src/sql/statements/define.rs index ec9c7fcb..5fb3aa10 100644 --- a/lib/src/sql/statements/define.rs +++ b/lib/src/sql/statements/define.rs @@ -834,6 +834,7 @@ fn event(i: &str) -> IResult<&str, DefineEventStatement> { pub struct DefineFieldStatement { pub name: Idiom, pub what: Ident, + pub flex: bool, pub kind: Option, pub value: Option, pub assert: Option, @@ -873,6 +874,9 @@ impl DefineFieldStatement { impl fmt::Display for DefineFieldStatement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "DEFINE FIELD {} ON {}", self.name, self.what)?; + if self.flex { + write!(f, " FLEXIBLE")? + } if let Some(ref v) = self.kind { write!(f, " TYPE {}", v)? } @@ -906,6 +910,13 @@ fn field(i: &str) -> IResult<&str, DefineFieldStatement> { DefineFieldStatement { name, what, + flex: opts + .iter() + .find_map(|x| match x { + DefineFieldOption::Flex => Some(true), + _ => None, + }) + .unwrap_or_default(), kind: opts.iter().find_map(|x| match x { DefineFieldOption::Kind(ref v) => Some(v.to_owned()), _ => None, @@ -931,6 +942,7 @@ fn field(i: &str) -> IResult<&str, DefineFieldStatement> { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] pub enum DefineFieldOption { + Flex, Kind(Kind), Value(Value), Assert(Value), @@ -938,7 +950,13 @@ pub enum DefineFieldOption { } fn field_opts(i: &str) -> IResult<&str, DefineFieldOption> { - alt((field_kind, field_value, field_assert, field_permissions))(i) + alt((field_flex, field_kind, field_value, field_assert, field_permissions))(i) +} + +fn field_flex(i: &str) -> IResult<&str, DefineFieldOption> { + let (i, _) = shouldbespace(i)?; + let (i, _) = alt((tag_no_case("FLEXIBLE"), tag_no_case("FLEXI"), tag_no_case("FLEX")))(i)?; + Ok((i, DefineFieldOption::Flex)) } fn field_kind(i: &str) -> IResult<&str, DefineFieldOption> { diff --git a/lib/src/sql/value/every.rs b/lib/src/sql/value/every.rs index 75a1341a..cbf40236 100644 --- a/lib/src/sql/value/every.rs +++ b/lib/src/sql/value/every.rs @@ -3,8 +3,11 @@ use crate::sql::part::Part; use crate::sql::value::Value; impl Value { - pub fn every(&self, steps: bool, arrays: bool) -> Vec { - self._every(steps, arrays, Idiom::default()) + pub fn every(&self, path: Option<&[Part]>, steps: bool, arrays: bool) -> Vec { + match path { + Some(path) => self.pick(path)._every(steps, arrays, Idiom::from(path)), + None => self._every(steps, arrays, Idiom::default()), + } } fn _every(&self, steps: bool, arrays: bool, prev: Idiom) -> Vec { match self { @@ -56,7 +59,7 @@ mod tests { fn every_without_array_indexes() { let val = Value::parse("{ test: { something: [{ age: 34, tags: ['code', 'databases'] }, { age: 36, tags: ['design', 'operations'] }] } }"); let res = vec![Idiom::parse("test.something")]; - assert_eq!(res, val.every(false, false)); + assert_eq!(res, val.every(None, false, false)); } #[test] @@ -73,14 +76,14 @@ mod tests { Idiom::parse("test.something[0].tags[1]"), Idiom::parse("test.something[0].tags[0]"), ]; - assert_eq!(res, val.every(false, true)); + assert_eq!(res, val.every(None, false, true)); } #[test] fn every_including_intermediary_nodes_without_array_indexes() { let val = Value::parse("{ test: { something: [{ age: 34, tags: ['code', 'databases'] }, { age: 36, tags: ['design', 'operations'] }] } }"); let res = vec![Idiom::parse("test"), Idiom::parse("test.something")]; - assert_eq!(res, val.every(true, false)); + assert_eq!(res, val.every(None, true, false)); } #[test] @@ -100,6 +103,6 @@ mod tests { Idiom::parse("test.something[0].tags[1]"), Idiom::parse("test.something[0].tags[0]"), ]; - assert_eq!(res, val.every(true, true)); + assert_eq!(res, val.every(None, true, true)); } } diff --git a/lib/src/sql/value/merge.rs b/lib/src/sql/value/merge.rs index 4f86af6e..27299348 100644 --- a/lib/src/sql/value/merge.rs +++ b/lib/src/sql/value/merge.rs @@ -14,7 +14,7 @@ impl Value { ) -> Result<(), Error> { match val { v if v.is_object() => { - for k in v.every(false, false).iter() { + for k in v.every(None, false, false).iter() { match v.get(ctx, opt, txn, &k.0).await? { Value::None => self.del(ctx, opt, txn, &k.0).await?, v => self.set(ctx, opt, txn, &k.0, v).await?, diff --git a/lib/tests/field.rs b/lib/tests/field.rs index 5d90b297..d184c82d 100644 --- a/lib/tests/field.rs +++ b/lib/tests/field.rs @@ -147,3 +147,121 @@ async fn field_definition_empty_nested_objects() -> Result<(), Error> { // Ok(()) } + +#[tokio::test] +async fn field_definition_empty_nested_arrays() -> Result<(), Error> { + let sql = " + DEFINE TABLE person SCHEMAFULL; + DEFINE FIELD settings on person TYPE object; + UPDATE person:test CONTENT { + settings: { + nested: [ + 1, + 2, + 3, + 4, + 5 + ] + } + }; + SELECT * FROM person; + "; + 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, false).await?; + assert_eq!(res.len(), 4); + // + 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( + "[ + { + id: person:test, + settings: {}, + } + ]", + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + id: person:test, + settings: {}, + } + ]", + ); + assert_eq!(tmp, val); + // + Ok(()) +} + +#[tokio::test] +async fn field_definition_empty_nested_flexible() -> Result<(), Error> { + let sql = " + DEFINE TABLE person SCHEMAFULL; + DEFINE FIELD settings on person FLEXIBLE TYPE object; + UPDATE person:test CONTENT { + settings: { + nested: { + object: { + thing: 'test' + } + } + } + }; + SELECT * FROM person; + "; + 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, false).await?; + assert_eq!(res.len(), 4); + // + 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( + "[ + { + id: person:test, + settings: { + nested: { + object: { + thing: 'test' + } + } + }, + } + ]", + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + id: person:test, + settings: { + nested: { + object: { + thing: 'test' + } + } + }, + } + ]", + ); + assert_eq!(tmp, val); + // + Ok(()) +}