diff --git a/lib/src/sql/kind.rs b/lib/src/sql/kind.rs index 03c2720d..3444712a 100644 --- a/lib/src/sql/kind.rs +++ b/lib/src/sql/kind.rs @@ -19,6 +19,7 @@ use std::fmt::{self, Display, Formatter}; #[revisioned(revision = 1)] pub enum Kind { Any, + Null, Bool, Bytes, Datetime, @@ -62,6 +63,7 @@ impl Display for Kind { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Kind::Any => f.write_str("any"), + Kind::Null => f.write_str("null"), Kind::Bool => f.write_str("bool"), Kind::Bytes => f.write_str("bytes"), Kind::Datetime => f.write_str("datetime"), @@ -109,6 +111,7 @@ pub fn any(i: &str) -> IResult<&str, Kind> { pub fn simple(i: &str) -> IResult<&str, Kind> { alt(( map(tag("bool"), |_| Kind::Bool), + map(tag("null"), |_| Kind::Null), map(tag("bytes"), |_| Kind::Bytes), map(tag("datetime"), |_| Kind::Datetime), map(tag("decimal"), |_| Kind::Decimal), @@ -266,6 +269,16 @@ mod tests { assert_eq!(out, Kind::Any); } + #[test] + fn kind_null() { + let sql = "null"; + let res = kind(sql); + assert!(res.is_ok()); + let out = res.unwrap().1; + assert_eq!("null", format!("{}", out)); + assert_eq!(out, Kind::Null); + } + #[test] fn kind_bool() { let sql = "bool"; diff --git a/lib/src/sql/value/value.rs b/lib/src/sql/value/value.rs index 7ff2f638..c98ac51b 100644 --- a/lib/src/sql/value/value.rs +++ b/lib/src/sql/value/value.rs @@ -1149,6 +1149,7 @@ impl Value { // Attempt to convert to the desired type let res = match kind { Kind::Any => Ok(self), + Kind::Null => self.coerce_to_null(), Kind::Bool => self.coerce_to_bool().map(Value::from), Kind::Int => self.coerce_to_int().map(Value::from), Kind::Float => self.coerce_to_float().map(Value::from), @@ -1179,7 +1180,6 @@ impl Value { }, Kind::Option(k) => match self { Self::None => Ok(Self::None), - Self::Null => Ok(Self::None), v => v.coerce_to(k), }, Kind::Either(k) => { @@ -1292,6 +1292,19 @@ impl Value { } } + /// Try to coerce this value to a `null` + pub(crate) fn coerce_to_null(self) -> Result { + match self { + // Allow any null value + Value::Null => Ok(Value::Null), + // Anything else raises an error + _ => Err(Error::CoerceTo { + from: self, + into: "null".into(), + }), + } + } + /// Try to coerce this value to a `bool` pub(crate) fn coerce_to_bool(self) -> Result { match self { @@ -1675,6 +1688,7 @@ impl Value { // Attempt to convert to the desired type let res = match kind { Kind::Any => Ok(self), + Kind::Null => self.convert_to_null(), Kind::Bool => self.convert_to_bool().map(Value::from), Kind::Int => self.convert_to_int().map(Value::from), Kind::Float => self.convert_to_float().map(Value::from), @@ -1705,7 +1719,6 @@ impl Value { }, Kind::Option(k) => match self { Self::None => Ok(Self::None), - Self::Null => Ok(Self::None), v => v.convert_to(k), }, Kind::Either(k) => { @@ -1743,6 +1756,19 @@ impl Value { } } + /// Try to convert this value to a `null` + pub(crate) fn convert_to_null(self) -> Result { + match self { + // Allow any boolean value + Value::Null => Ok(Value::Null), + // Anything else raises an error + _ => Err(Error::ConvertTo { + from: self, + into: "null".into(), + }), + } + } + /// Try to convert this value to a `bool` pub(crate) fn convert_to_bool(self) -> Result { match self { diff --git a/lib/tests/typing.rs b/lib/tests/typing.rs index b5af2b8f..a8679aa2 100644 --- a/lib/tests/typing.rs +++ b/lib/tests/typing.rs @@ -185,3 +185,137 @@ async fn strict_typing_defined() -> Result<(), Error> { // Ok(()) } + +#[tokio::test] +async fn strict_typing_none_null() -> Result<(), Error> { + let sql = " + DEFINE TABLE person SCHEMAFULL; + DEFINE FIELD name ON TABLE person TYPE option; + UPDATE person:test SET name = 'Tobie'; + UPDATE person:test SET name = NULL; + UPDATE person:test SET name = NONE; + -- + DEFINE TABLE person SCHEMAFULL; + DEFINE FIELD name ON TABLE person TYPE option; + UPDATE person:test SET name = 'Tobie'; + UPDATE person:test SET name = NULL; + UPDATE person:test SET name = NONE; + -- + DEFINE TABLE person SCHEMAFULL; + DEFINE FIELD name ON TABLE person TYPE string | null; + UPDATE person:test SET name = 'Tobie'; + UPDATE person:test SET name = NULL; + UPDATE person:test SET name = NONE; + "; + let dbs = Datastore::new("memory").await?; + let ses = Session::owner().with_ns("test").with_db("test"); + let res = &mut dbs.execute(sql, &ses, None).await?; + assert_eq!(res.len(), 15); + // + 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, + name: 'Tobie', + } + ]", + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result; + assert!(matches!( + tmp.err(), + Some(e) if e.to_string() == "Found NULL for field `name`, with record `person:test`, but expected a option" + )); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + id: person:test, + } + ]", + ); + assert_eq!(tmp, val); + // + 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, + name: 'Tobie', + } + ]", + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + id: person:test, + name: NULL, + } + ]", + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + id: person:test, + } + ]", + ); + assert_eq!(tmp, val); + // + 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, + name: 'Tobie', + } + ]", + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + id: person:test, + name: NULL, + } + ]", + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result; + assert!(matches!( + tmp.err(), + Some(e) if e.to_string() == "Found NONE for field `name`, with record `person:test`, but expected a string | null" + )); + // + Ok(()) +}