Literal types (#4557)

This commit is contained in:
Micha de Vries 2024-08-21 15:50:37 +01:00 committed by GitHub
parent d002fa3958
commit f2ccedb8c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 273 additions and 5 deletions

View file

@ -1,8 +1,14 @@
use super::escape::escape_key;
use super::{Duration, Number, Strand};
use crate::sql::statements::info::InfoStructure; use crate::sql::statements::info::InfoStructure;
use crate::sql::{fmt::Fmt, Table, Value}; use crate::sql::{
fmt::{is_pretty, pretty_indent, Fmt, Pretty},
Table, Value,
};
use revision::revisioned; use revision::revisioned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter}; use std::collections::BTreeMap;
use std::fmt::{self, Display, Formatter, Write};
#[revisioned(revision = 1)] #[revisioned(revision = 1)]
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)] #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
@ -31,6 +37,7 @@ pub enum Kind {
Array(Box<Kind>, Option<u64>), Array(Box<Kind>, Option<u64>),
Function(Option<Vec<Kind>>, Option<Box<Kind>>), Function(Option<Vec<Kind>>, Option<Box<Kind>>),
Range, Range,
Literal(Literal),
} }
impl Default for Kind { impl Default for Kind {
@ -75,7 +82,8 @@ impl Kind {
| Kind::Record(_) | Kind::Record(_)
| Kind::Geometry(_) | Kind::Geometry(_)
| Kind::Function(_, _) | Kind::Function(_, _)
| Kind::Range => return None, | Kind::Range
| Kind::Literal(_) => return None,
Kind::Option(x) => { Kind::Option(x) => {
this = x; this = x;
} }
@ -140,6 +148,7 @@ impl Display for Kind {
}, },
Kind::Either(k) => write!(f, "{}", Fmt::verbar_separated(k)), Kind::Either(k) => write!(f, "{}", Fmt::verbar_separated(k)),
Kind::Range => f.write_str("range"), Kind::Range => f.write_str("range"),
Kind::Literal(l) => write!(f, "{}", l),
} }
} }
} }
@ -149,3 +158,137 @@ impl InfoStructure for Kind {
self.to_string().into() self.to_string().into()
} }
} }
#[revisioned(revision = 1)]
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub enum Literal {
String(Strand),
Number(Number),
Duration(Duration),
Array(Vec<Kind>),
Object(BTreeMap<String, Kind>),
}
impl Literal {
pub fn to_kind(&self) -> Kind {
match self {
Self::String(_) => Kind::String,
Self::Number(_) => Kind::Number,
Self::Duration(_) => Kind::Duration,
Self::Array(a) => {
if let Some(inner) = a.first() {
if a.iter().all(|x| x == inner) {
return Kind::Array(Box::new(inner.to_owned()), Some(a.len() as u64));
}
}
Kind::Array(Box::new(Kind::Any), None)
}
Self::Object(_) => Kind::Object,
}
}
pub fn validate_value(&self, value: &Value) -> bool {
match self {
Self::String(v) => match value {
Value::Strand(s) => s == v,
_ => false,
},
Self::Number(v) => match value {
Value::Number(n) => n == v,
_ => false,
},
Self::Duration(v) => match value {
Value::Duration(n) => n == v,
_ => false,
},
Self::Array(a) => match value {
Value::Array(x) => {
if a.len() != x.len() {
return false;
}
for (i, inner) in a.iter().enumerate() {
if let Some(value) = x.get(i) {
if value.to_owned().coerce_to(inner).is_err() {
return false;
}
} else {
return false;
}
}
true
}
_ => false,
},
Self::Object(o) => match value {
Value::Object(x) => {
if o.len() != x.len() {
return false;
}
for (k, v) in o.iter() {
if let Some(value) = x.get(k) {
if value.to_owned().coerce_to(v).is_err() {
return false;
}
} else {
return false;
}
}
true
}
_ => false,
},
}
}
}
impl Display for Literal {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Literal::String(s) => write!(f, "{}", s),
Literal::Number(n) => write!(f, "{}", n),
Literal::Duration(n) => write!(f, "{}", n),
Literal::Array(a) => {
let mut f = Pretty::from(f);
f.write_char('[')?;
if !a.is_empty() {
let indent = pretty_indent();
write!(f, "{}", Fmt::pretty_comma_separated(a.as_slice()))?;
drop(indent);
}
f.write_char(']')
}
Literal::Object(o) => {
let mut f = Pretty::from(f);
if is_pretty() {
f.write_char('{')?;
} else {
f.write_str("{ ")?;
}
if !o.is_empty() {
let indent = pretty_indent();
write!(
f,
"{}",
Fmt::pretty_comma_separated(o.iter().map(|args| Fmt::new(
args,
|(k, v), f| write!(f, "{}: {}", escape_key(k), v)
)),)
)?;
drop(indent);
}
if is_pretty() {
f.write_char('}')
} else {
f.write_str(" }")
}
}
}
}
}

View file

@ -6,6 +6,7 @@ use crate::doc::CursorDoc;
use crate::err::Error; use crate::err::Error;
use crate::fnc::util::string::fuzzy::Fuzzy; use crate::fnc::util::string::fuzzy::Fuzzy;
use crate::sql::id::range::IdRange; use crate::sql::id::range::IdRange;
use crate::sql::kind::Literal;
use crate::sql::statements::info::InfoStructure; use crate::sql::statements::info::InfoStructure;
use crate::sql::Closure; use crate::sql::Closure;
use crate::sql::{ use crate::sql::{
@ -1337,6 +1338,7 @@ impl Value {
into: kind.to_string(), into: kind.to_string(),
}) })
} }
Kind::Literal(lit) => self.coerce_to_literal(lit),
}; };
// Check for any conversion errors // Check for any conversion errors
match res { match res {
@ -1431,6 +1433,18 @@ impl Value {
} }
} }
/// Try to coerce this value to a Literal, returns a `Value` with the coerced value
pub(crate) fn coerce_to_literal(self, literal: &Literal) -> Result<Value, Error> {
if literal.validate_value(&self) {
Ok(self)
} else {
Err(Error::CoerceTo {
from: self,
into: literal.to_string(),
})
}
}
/// Try to coerce this value to a `null` /// Try to coerce this value to a `null`
pub(crate) fn coerce_to_null(self) -> Result<Value, Error> { pub(crate) fn coerce_to_null(self) -> Result<Value, Error> {
match self { match self {
@ -1920,6 +1934,7 @@ impl Value {
into: kind.to_string(), into: kind.to_string(),
}) })
} }
Kind::Literal(lit) => self.convert_to_literal(lit),
}; };
// Check for any conversion errors // Check for any conversion errors
match res { match res {
@ -1938,6 +1953,18 @@ impl Value {
} }
} }
/// Try to convert this value to a Literal, returns a `Value` with the coerced value
pub(crate) fn convert_to_literal(self, literal: &Literal) -> Result<Value, Error> {
if literal.validate_value(&self) {
Ok(self)
} else {
Err(Error::ConvertTo {
from: self,
into: literal.to_string(),
})
}
}
/// Try to convert this value to a `null` /// Try to convert this value to a `null`
pub(crate) fn convert_to_null(self) -> Result<Value, Error> { pub(crate) fn convert_to_null(self) -> Result<Value, Error> {
match self { match self {

View file

@ -1,7 +1,9 @@
use std::collections::BTreeMap;
use reblessive::Stk; use reblessive::Stk;
use crate::{ use crate::{
sql::Kind, sql::{kind::Literal, Kind, Strand},
syn::{ syn::{
parser::mac::expected, parser::mac::expected,
token::{t, Keyword, Span, TokenKind}, token::{t, Keyword, Span, TokenKind},
@ -68,6 +70,11 @@ impl Parser<'_> {
/// Parse a single kind which is not any, option, or either. /// Parse a single kind which is not any, option, or either.
async fn parse_concrete_kind(&mut self, ctx: &mut Stk) -> ParseResult<Kind> { async fn parse_concrete_kind(&mut self, ctx: &mut Stk) -> ParseResult<Kind> {
if Self::token_can_be_literal_kind(self.peek_kind()) {
let literal = self.parse_literal_kind(ctx).await?;
return Ok(Kind::Literal(literal));
}
match self.next().kind { match self.next().kind {
t!("BOOL") => Ok(Kind::Bool), t!("BOOL") => Ok(Kind::Bool),
t!("NULL") => Ok(Kind::Null), t!("NULL") => Ok(Kind::Null),
@ -152,6 +159,60 @@ impl Parser<'_> {
x => unexpected!(self, x, "a geometry kind name"), x => unexpected!(self, x, "a geometry kind name"),
} }
} }
/// Parse a literal kind
async fn parse_literal_kind(&mut self, ctx: &mut Stk) -> ParseResult<Literal> {
match self.peek_kind() {
t!("'") | t!("\"") | TokenKind::Strand => {
let s = self.next_token_value::<Strand>()?;
Ok(Literal::String(s))
}
t!("+") | t!("-") | TokenKind::Number(_) | TokenKind::Digits | TokenKind::Duration => {
let token = self.glue_numeric()?;
match token.kind {
TokenKind::Number(_) => self.next_token_value().map(Literal::Number),
TokenKind::Duration => self.next_token_value().map(Literal::Duration),
x => unexpected!(self, x, "a value"),
}
}
t!("{") => {
self.pop_peek();
let mut obj = BTreeMap::new();
while !self.eat(t!("}")) {
let key = self.parse_object_key()?;
expected!(self, t!(":"));
let kind = ctx.run(|ctx| self.parse_inner_kind(ctx)).await?;
obj.insert(key, kind);
self.eat(t!(","));
}
Ok(Literal::Object(obj))
}
t!("[") => {
self.pop_peek();
let mut arr = Vec::new();
while !self.eat(t!("]")) {
let kind = ctx.run(|ctx| self.parse_inner_kind(ctx)).await?;
arr.push(kind);
self.eat(t!(","));
}
Ok(Literal::Array(arr))
}
_ => unexpected!(self, self.peek().kind, "a literal kind"),
}
}
fn token_can_be_literal_kind(t: TokenKind) -> bool {
matches!(
t,
t!("'")
| t!("\"") | TokenKind::Strand
| t!("+") | t!("-")
| TokenKind::Number(_)
| TokenKind::Digits
| TokenKind::Duration
| t!("{") | t!("[")
)
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -94,3 +94,40 @@ async fn cast_range_to_array() -> Result<(), Error> {
// //
Ok(()) Ok(())
} }
#[tokio::test]
async fn cast_with_literal_kind() -> Result<(), Error> {
let sql = r#"
<"a" | "b"> "a";
<123 | 456> 123;
<123 | "b"> 123;
<[number, "abc"]> [123, "abc"];
<{ a: 1d | 2d }> { a: 1d };
"#;
let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 5);
//
let tmp = res.remove(0).result?;
let val = Value::parse("'a'");
assert_eq!(tmp, val);
//
let tmp = res.remove(0).result?;
let val = Value::parse("123");
assert_eq!(tmp, val);
//
let tmp = res.remove(0).result?;
let val = Value::parse("123");
assert_eq!(tmp, val);
//
let tmp = res.remove(0).result?;
let val = Value::parse("[123, 'abc']");
assert_eq!(tmp, val);
//
let tmp = res.remove(0).result?;
let val = Value::parse("{ a: 1d }");
assert_eq!(tmp, val);
//
Ok(())
}