Literal types (#4557)
This commit is contained in:
parent
d002fa3958
commit
f2ccedb8c1
4 changed files with 273 additions and 5 deletions
|
@ -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<Kind>, Option<u64>),
|
||||
Function(Option<Vec<Kind>>, Option<Box<Kind>>),
|
||||
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<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(" }")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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`
|
||||
pub(crate) fn coerce_to_null(self) -> Result<Value, Error> {
|
||||
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<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`
|
||||
pub(crate) fn convert_to_null(self) -> Result<Value, Error> {
|
||||
match self {
|
||||
|
|
|
@ -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<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 {
|
||||
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<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)]
|
||||
|
|
|
@ -70,7 +70,7 @@ async fn cast_range_to_array() -> Result<(), Error> {
|
|||
<array> 1>..5;
|
||||
<array> 1..=5;
|
||||
<array> 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(())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue