Support Map literal syntax for DuckDB and Generic (#1344)

This commit is contained in:
Jax Liu 2024-07-21 20:18:50 +08:00 committed by GitHub
parent 71dc966586
commit 48ea5640a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 219 additions and 0 deletions

View file

@ -329,6 +329,37 @@ impl fmt::Display for DictionaryField {
}
}
/// Represents a Map expression.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct Map {
pub entries: Vec<MapEntry>,
}
impl Display for Map {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "MAP {{{}}}", display_comma_separated(&self.entries))
}
}
/// A map field within a map.
///
/// [duckdb]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct MapEntry {
pub key: Box<Expr>,
pub value: Box<Expr>,
}
impl fmt::Display for MapEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.key, self.value)
}
}
/// Options for `CAST` / `TRY_CAST`
/// BigQuery: <https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax>
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@ -764,6 +795,14 @@ pub enum Expr {
/// ```
/// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs
Dictionary(Vec<DictionaryField>),
/// `DuckDB` specific `Map` literal expression [1]
///
/// Syntax:
/// ```sql
/// syntax: Map {key1: value1[, ... ]}
/// ```
/// [1]: https://duckdb.org/docs/sql/data_types/map#creating-maps
Map(Map),
/// An access of nested data using subscript syntax, for example `array[2]`.
Subscript {
expr: Box<Expr>,
@ -1331,6 +1370,9 @@ impl fmt::Display for Expr {
Expr::Dictionary(fields) => {
write!(f, "{{{}}}", display_comma_separated(fields))
}
Expr::Map(map) => {
write!(f, "{map}")
}
Expr::Subscript {
expr,
subscript: key,

View file

@ -48,4 +48,11 @@ impl Dialect for DuckDbDialect {
fn supports_dictionary_syntax(&self) -> bool {
true
}
// DuckDB uses this syntax for `MAP`s.
//
// https://duckdb.org/docs/sql/data_types/map.html#creating-maps
fn support_map_literal_syntax(&self) -> bool {
true
}
}

View file

@ -70,4 +70,8 @@ impl Dialect for GenericDialect {
fn supports_select_wildcard_except(&self) -> bool {
true
}
fn support_map_literal_syntax(&self) -> bool {
true
}
}

View file

@ -215,6 +215,11 @@ pub trait Dialect: Debug + Any {
fn supports_dictionary_syntax(&self) -> bool {
false
}
/// Returns true if the dialect supports defining object using the
/// syntax like `Map {1: 10, 2: 20}`.
fn support_map_literal_syntax(&self) -> bool {
false
}
/// Returns true if the dialect supports lambda functions, for example:
///
/// ```sql

View file

@ -1078,6 +1078,9 @@ impl<'a> Parser<'a> {
let expr = self.parse_subexpr(Self::PLUS_MINUS_PREC)?;
Ok(Expr::Prior(Box::new(expr)))
}
Keyword::MAP if self.peek_token() == Token::LBrace && self.dialect.support_map_literal_syntax() => {
self.parse_duckdb_map_literal()
}
// Here `w` is a word, check if it's a part of a multipart
// identifier, a function call, or a simple identifier:
_ => match self.peek_token().token {
@ -2322,6 +2325,47 @@ impl<'a> Parser<'a> {
})
}
/// DuckDB specific: Parse a duckdb [map]
///
/// Syntax:
///
/// ```sql
/// Map {key1: value1[, ... ]}
/// ```
///
/// [map]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps
fn parse_duckdb_map_literal(&mut self) -> Result<Expr, ParserError> {
self.expect_token(&Token::LBrace)?;
let fields = self.parse_comma_separated(Self::parse_duckdb_map_field)?;
self.expect_token(&Token::RBrace)?;
Ok(Expr::Map(Map { entries: fields }))
}
/// Parse a field for a duckdb [map]
///
/// Syntax
///
/// ```sql
/// key: value
/// ```
///
/// [map]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps
fn parse_duckdb_map_field(&mut self) -> Result<MapEntry, ParserError> {
let key = self.parse_expr()?;
self.expect_token(&Token::Colon)?;
let value = self.parse_expr()?;
Ok(MapEntry {
key: Box::new(key),
value: Box::new(value),
})
}
/// Parse clickhouse [map]
///
/// Syntax

View file

@ -10077,6 +10077,101 @@ fn test_dictionary_syntax() {
)
}
#[test]
fn test_map_syntax() {
fn check(sql: &str, expect: Expr) {
assert_eq!(
all_dialects_where(|d| d.support_map_literal_syntax()).verified_expr(sql),
expect
);
}
check(
"MAP {'Alberta': 'Edmonton', 'Manitoba': 'Winnipeg'}",
Expr::Map(Map {
entries: vec![
MapEntry {
key: Box::new(Expr::Value(Value::SingleQuotedString("Alberta".to_owned()))),
value: Box::new(Expr::Value(Value::SingleQuotedString(
"Edmonton".to_owned(),
))),
},
MapEntry {
key: Box::new(Expr::Value(Value::SingleQuotedString(
"Manitoba".to_owned(),
))),
value: Box::new(Expr::Value(Value::SingleQuotedString(
"Winnipeg".to_owned(),
))),
},
],
}),
);
fn number_expr(s: &str) -> Expr {
Expr::Value(number(s))
}
check(
"MAP {1: 10.0, 2: 20.0}",
Expr::Map(Map {
entries: vec![
MapEntry {
key: Box::new(number_expr("1")),
value: Box::new(number_expr("10.0")),
},
MapEntry {
key: Box::new(number_expr("2")),
value: Box::new(number_expr("20.0")),
},
],
}),
);
check(
"MAP {[1, 2, 3]: 10.0, [4, 5, 6]: 20.0}",
Expr::Map(Map {
entries: vec![
MapEntry {
key: Box::new(Expr::Array(Array {
elem: vec![number_expr("1"), number_expr("2"), number_expr("3")],
named: false,
})),
value: Box::new(Expr::Value(number("10.0"))),
},
MapEntry {
key: Box::new(Expr::Array(Array {
elem: vec![number_expr("4"), number_expr("5"), number_expr("6")],
named: false,
})),
value: Box::new(Expr::Value(number("20.0"))),
},
],
}),
);
check(
"MAP {'a': 10, 'b': 20}['a']",
Expr::Subscript {
expr: Box::new(Expr::Map(Map {
entries: vec![
MapEntry {
key: Box::new(Expr::Value(Value::SingleQuotedString("a".to_owned()))),
value: Box::new(number_expr("10")),
},
MapEntry {
key: Box::new(Expr::Value(Value::SingleQuotedString("b".to_owned()))),
value: Box::new(number_expr("20")),
},
],
})),
subscript: Box::new(Subscript::Index {
index: Expr::Value(Value::SingleQuotedString("a".to_owned())),
}),
},
);
}
#[test]
fn parse_within_group() {
verified_expr("PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY sales_amount)");

View file

@ -125,6 +125,28 @@ fn custom_statement_parser() -> Result<(), ParserError> {
Ok(())
}
#[test]
fn test_map_syntax_not_support_default() -> Result<(), ParserError> {
#[derive(Debug)]
struct MyDialect {}
impl Dialect for MyDialect {
fn is_identifier_start(&self, ch: char) -> bool {
is_identifier_start(ch)
}
fn is_identifier_part(&self, ch: char) -> bool {
is_identifier_part(ch)
}
}
let dialect = MyDialect {};
let sql = "SELECT MAP {1: 2}";
let ast = Parser::parse_sql(&dialect, sql);
assert!(ast.is_err());
Ok(())
}
fn is_identifier_start(ch: char) -> bool {
ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_'
}