diff --git a/core/src/sql/kind.rs b/core/src/sql/kind.rs index dba8ff1e..b99cd7d1 100644 --- a/core/src/sql/kind.rs +++ b/core/src/sql/kind.rs @@ -1,8 +1,14 @@ +use super::escape::escape_key; +use super::{Duration, Number, Strand}; 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 serde::{Deserialize, Serialize}; -use std::fmt::{self, Display, Formatter}; +use std::collections::BTreeMap; +use std::fmt::{self, Display, Formatter, Write}; #[revisioned(revision = 1)] #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)] @@ -31,6 +37,7 @@ pub enum Kind { Array(Box, Option), Function(Option>, Option>), Range, + Literal(Literal), } impl Default for Kind { @@ -75,7 +82,8 @@ impl Kind { | Kind::Record(_) | Kind::Geometry(_) | Kind::Function(_, _) - | Kind::Range => return None, + | Kind::Range + | Kind::Literal(_) => return None, Kind::Option(x) => { this = x; } @@ -140,6 +148,7 @@ impl Display for Kind { }, Kind::Either(k) => write!(f, "{}", Fmt::verbar_separated(k)), Kind::Range => f.write_str("range"), + Kind::Literal(l) => write!(f, "{}", l), } } } @@ -149,3 +158,137 @@ impl InfoStructure for Kind { 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), + Object(BTreeMap), +} + +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(" }") + } + } + } + } +} diff --git a/core/src/sql/value/value.rs b/core/src/sql/value/value.rs index fa16857b..01b2ef6a 100644 --- a/core/src/sql/value/value.rs +++ b/core/src/sql/value/value.rs @@ -6,6 +6,7 @@ use crate::doc::CursorDoc; use crate::err::Error; use crate::fnc::util::string::fuzzy::Fuzzy; use crate::sql::id::range::IdRange; +use crate::sql::kind::Literal; use crate::sql::statements::info::InfoStructure; use crate::sql::Closure; use crate::sql::{ @@ -1337,6 +1338,7 @@ impl Value { into: kind.to_string(), }) } + Kind::Literal(lit) => self.coerce_to_literal(lit), }; // Check for any conversion errors 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 { + 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` pub(crate) fn coerce_to_null(self) -> Result { match self { @@ -1920,6 +1934,7 @@ impl Value { into: kind.to_string(), }) } + Kind::Literal(lit) => self.convert_to_literal(lit), }; // Check for any conversion errors 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 { + 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` pub(crate) fn convert_to_null(self) -> Result { match self { diff --git a/core/src/syn/parser/kind.rs b/core/src/syn/parser/kind.rs index fa8898ac..3535f667 100644 --- a/core/src/syn/parser/kind.rs +++ b/core/src/syn/parser/kind.rs @@ -1,7 +1,9 @@ +use std::collections::BTreeMap; + use reblessive::Stk; use crate::{ - sql::Kind, + sql::{kind::Literal, Kind, Strand}, syn::{ parser::mac::expected, token::{t, Keyword, Span, TokenKind}, @@ -68,6 +70,11 @@ impl Parser<'_> { /// Parse a single kind which is not any, option, or either. async fn parse_concrete_kind(&mut self, ctx: &mut Stk) -> ParseResult { + 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 { t!("BOOL") => Ok(Kind::Bool), t!("NULL") => Ok(Kind::Null), @@ -152,6 +159,60 @@ impl Parser<'_> { x => unexpected!(self, x, "a geometry kind name"), } } + + /// Parse a literal kind + async fn parse_literal_kind(&mut self, ctx: &mut Stk) -> ParseResult { + match self.peek_kind() { + t!("'") | t!("\"") | TokenKind::Strand => { + let s = self.next_token_value::()?; + 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)] diff --git a/lib/tests/cast.rs b/lib/tests/cast.rs index d56490b5..4749cb08 100644 --- a/lib/tests/cast.rs +++ b/lib/tests/cast.rs @@ -70,7 +70,7 @@ async fn cast_range_to_array() -> Result<(), Error> { 1>..5; 1..=5; 1>..=5; - "#; + "#; let dbs = new_ds().await?; let ses = Session::owner().with_ns("test").with_db("test"); let res = &mut dbs.execute(sql, &ses, None).await?; @@ -94,3 +94,40 @@ async fn cast_range_to_array() -> Result<(), Error> { // 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(()) +}