Add null as a specific field type (#2524)

This commit is contained in:
Tobie Morgan Hitchcock 2023-08-27 11:22:27 +01:00 committed by GitHub
parent a867eae362
commit e491789e20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 175 additions and 2 deletions

View file

@ -19,6 +19,7 @@ use std::fmt::{self, Display, Formatter};
#[revisioned(revision = 1)] #[revisioned(revision = 1)]
pub enum Kind { pub enum Kind {
Any, Any,
Null,
Bool, Bool,
Bytes, Bytes,
Datetime, Datetime,
@ -62,6 +63,7 @@ impl Display for Kind {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self { match self {
Kind::Any => f.write_str("any"), Kind::Any => f.write_str("any"),
Kind::Null => f.write_str("null"),
Kind::Bool => f.write_str("bool"), Kind::Bool => f.write_str("bool"),
Kind::Bytes => f.write_str("bytes"), Kind::Bytes => f.write_str("bytes"),
Kind::Datetime => f.write_str("datetime"), 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> { pub fn simple(i: &str) -> IResult<&str, Kind> {
alt(( alt((
map(tag("bool"), |_| Kind::Bool), map(tag("bool"), |_| Kind::Bool),
map(tag("null"), |_| Kind::Null),
map(tag("bytes"), |_| Kind::Bytes), map(tag("bytes"), |_| Kind::Bytes),
map(tag("datetime"), |_| Kind::Datetime), map(tag("datetime"), |_| Kind::Datetime),
map(tag("decimal"), |_| Kind::Decimal), map(tag("decimal"), |_| Kind::Decimal),
@ -266,6 +269,16 @@ mod tests {
assert_eq!(out, Kind::Any); 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] #[test]
fn kind_bool() { fn kind_bool() {
let sql = "bool"; let sql = "bool";

View file

@ -1149,6 +1149,7 @@ impl Value {
// Attempt to convert to the desired type // Attempt to convert to the desired type
let res = match kind { let res = match kind {
Kind::Any => Ok(self), Kind::Any => Ok(self),
Kind::Null => self.coerce_to_null(),
Kind::Bool => self.coerce_to_bool().map(Value::from), Kind::Bool => self.coerce_to_bool().map(Value::from),
Kind::Int => self.coerce_to_int().map(Value::from), Kind::Int => self.coerce_to_int().map(Value::from),
Kind::Float => self.coerce_to_float().map(Value::from), Kind::Float => self.coerce_to_float().map(Value::from),
@ -1179,7 +1180,6 @@ impl Value {
}, },
Kind::Option(k) => match self { Kind::Option(k) => match self {
Self::None => Ok(Self::None), Self::None => Ok(Self::None),
Self::Null => Ok(Self::None),
v => v.coerce_to(k), v => v.coerce_to(k),
}, },
Kind::Either(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<Value, Error> {
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` /// Try to coerce this value to a `bool`
pub(crate) fn coerce_to_bool(self) -> Result<bool, Error> { pub(crate) fn coerce_to_bool(self) -> Result<bool, Error> {
match self { match self {
@ -1675,6 +1688,7 @@ impl Value {
// Attempt to convert to the desired type // Attempt to convert to the desired type
let res = match kind { let res = match kind {
Kind::Any => Ok(self), Kind::Any => Ok(self),
Kind::Null => self.convert_to_null(),
Kind::Bool => self.convert_to_bool().map(Value::from), Kind::Bool => self.convert_to_bool().map(Value::from),
Kind::Int => self.convert_to_int().map(Value::from), Kind::Int => self.convert_to_int().map(Value::from),
Kind::Float => self.convert_to_float().map(Value::from), Kind::Float => self.convert_to_float().map(Value::from),
@ -1705,7 +1719,6 @@ impl Value {
}, },
Kind::Option(k) => match self { Kind::Option(k) => match self {
Self::None => Ok(Self::None), Self::None => Ok(Self::None),
Self::Null => Ok(Self::None),
v => v.convert_to(k), v => v.convert_to(k),
}, },
Kind::Either(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<Value, Error> {
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` /// Try to convert this value to a `bool`
pub(crate) fn convert_to_bool(self) -> Result<bool, Error> { pub(crate) fn convert_to_bool(self) -> Result<bool, Error> {
match self { match self {

View file

@ -185,3 +185,137 @@ async fn strict_typing_defined() -> Result<(), Error> {
// //
Ok(()) 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<string>;
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<string | null>;
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<string>"
));
//
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(())
}