Skip to content

Commit 48ea564

Browse files
authored
Support Map literal syntax for DuckDB and Generic (#1344)
1 parent 71dc966 commit 48ea564

File tree

7 files changed

+219
-0
lines changed

7 files changed

+219
-0
lines changed

src/ast/mod.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,37 @@ impl fmt::Display for DictionaryField {
329329
}
330330
}
331331

332+
/// Represents a Map expression.
333+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
334+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
335+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
336+
pub struct Map {
337+
pub entries: Vec<MapEntry>,
338+
}
339+
340+
impl Display for Map {
341+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
342+
write!(f, "MAP {{{}}}", display_comma_separated(&self.entries))
343+
}
344+
}
345+
346+
/// A map field within a map.
347+
///
348+
/// [duckdb]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps
349+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
350+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
351+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
352+
pub struct MapEntry {
353+
pub key: Box<Expr>,
354+
pub value: Box<Expr>,
355+
}
356+
357+
impl fmt::Display for MapEntry {
358+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359+
write!(f, "{}: {}", self.key, self.value)
360+
}
361+
}
362+
332363
/// Options for `CAST` / `TRY_CAST`
333364
/// BigQuery: <https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax>
334365
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@@ -764,6 +795,14 @@ pub enum Expr {
764795
/// ```
765796
/// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs
766797
Dictionary(Vec<DictionaryField>),
798+
/// `DuckDB` specific `Map` literal expression [1]
799+
///
800+
/// Syntax:
801+
/// ```sql
802+
/// syntax: Map {key1: value1[, ... ]}
803+
/// ```
804+
/// [1]: https://duckdb.org/docs/sql/data_types/map#creating-maps
805+
Map(Map),
767806
/// An access of nested data using subscript syntax, for example `array[2]`.
768807
Subscript {
769808
expr: Box<Expr>,
@@ -1331,6 +1370,9 @@ impl fmt::Display for Expr {
13311370
Expr::Dictionary(fields) => {
13321371
write!(f, "{{{}}}", display_comma_separated(fields))
13331372
}
1373+
Expr::Map(map) => {
1374+
write!(f, "{map}")
1375+
}
13341376
Expr::Subscript {
13351377
expr,
13361378
subscript: key,

src/dialect/duckdb.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,11 @@ impl Dialect for DuckDbDialect {
4848
fn supports_dictionary_syntax(&self) -> bool {
4949
true
5050
}
51+
52+
// DuckDB uses this syntax for `MAP`s.
53+
//
54+
// https://duckdb.org/docs/sql/data_types/map.html#creating-maps
55+
fn support_map_literal_syntax(&self) -> bool {
56+
true
57+
}
5158
}

src/dialect/generic.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,8 @@ impl Dialect for GenericDialect {
7070
fn supports_select_wildcard_except(&self) -> bool {
7171
true
7272
}
73+
74+
fn support_map_literal_syntax(&self) -> bool {
75+
true
76+
}
7377
}

src/dialect/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ pub trait Dialect: Debug + Any {
215215
fn supports_dictionary_syntax(&self) -> bool {
216216
false
217217
}
218+
/// Returns true if the dialect supports defining object using the
219+
/// syntax like `Map {1: 10, 2: 20}`.
220+
fn support_map_literal_syntax(&self) -> bool {
221+
false
222+
}
218223
/// Returns true if the dialect supports lambda functions, for example:
219224
///
220225
/// ```sql

src/parser/mod.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,9 @@ impl<'a> Parser<'a> {
10781078
let expr = self.parse_subexpr(Self::PLUS_MINUS_PREC)?;
10791079
Ok(Expr::Prior(Box::new(expr)))
10801080
}
1081+
Keyword::MAP if self.peek_token() == Token::LBrace && self.dialect.support_map_literal_syntax() => {
1082+
self.parse_duckdb_map_literal()
1083+
}
10811084
// Here `w` is a word, check if it's a part of a multipart
10821085
// identifier, a function call, or a simple identifier:
10831086
_ => match self.peek_token().token {
@@ -2322,6 +2325,47 @@ impl<'a> Parser<'a> {
23222325
})
23232326
}
23242327

2328+
/// DuckDB specific: Parse a duckdb [map]
2329+
///
2330+
/// Syntax:
2331+
///
2332+
/// ```sql
2333+
/// Map {key1: value1[, ... ]}
2334+
/// ```
2335+
///
2336+
/// [map]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps
2337+
fn parse_duckdb_map_literal(&mut self) -> Result<Expr, ParserError> {
2338+
self.expect_token(&Token::LBrace)?;
2339+
2340+
let fields = self.parse_comma_separated(Self::parse_duckdb_map_field)?;
2341+
2342+
self.expect_token(&Token::RBrace)?;
2343+
2344+
Ok(Expr::Map(Map { entries: fields }))
2345+
}
2346+
2347+
/// Parse a field for a duckdb [map]
2348+
///
2349+
/// Syntax
2350+
///
2351+
/// ```sql
2352+
/// key: value
2353+
/// ```
2354+
///
2355+
/// [map]: https://duckdb.org/docs/sql/data_types/map.html#creating-maps
2356+
fn parse_duckdb_map_field(&mut self) -> Result<MapEntry, ParserError> {
2357+
let key = self.parse_expr()?;
2358+
2359+
self.expect_token(&Token::Colon)?;
2360+
2361+
let value = self.parse_expr()?;
2362+
2363+
Ok(MapEntry {
2364+
key: Box::new(key),
2365+
value: Box::new(value),
2366+
})
2367+
}
2368+
23252369
/// Parse clickhouse [map]
23262370
///
23272371
/// Syntax

tests/sqlparser_common.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10077,6 +10077,101 @@ fn test_dictionary_syntax() {
1007710077
)
1007810078
}
1007910079

10080+
#[test]
10081+
fn test_map_syntax() {
10082+
fn check(sql: &str, expect: Expr) {
10083+
assert_eq!(
10084+
all_dialects_where(|d| d.support_map_literal_syntax()).verified_expr(sql),
10085+
expect
10086+
);
10087+
}
10088+
10089+
check(
10090+
"MAP {'Alberta': 'Edmonton', 'Manitoba': 'Winnipeg'}",
10091+
Expr::Map(Map {
10092+
entries: vec![
10093+
MapEntry {
10094+
key: Box::new(Expr::Value(Value::SingleQuotedString("Alberta".to_owned()))),
10095+
value: Box::new(Expr::Value(Value::SingleQuotedString(
10096+
"Edmonton".to_owned(),
10097+
))),
10098+
},
10099+
MapEntry {
10100+
key: Box::new(Expr::Value(Value::SingleQuotedString(
10101+
"Manitoba".to_owned(),
10102+
))),
10103+
value: Box::new(Expr::Value(Value::SingleQuotedString(
10104+
"Winnipeg".to_owned(),
10105+
))),
10106+
},
10107+
],
10108+
}),
10109+
);
10110+
10111+
fn number_expr(s: &str) -> Expr {
10112+
Expr::Value(number(s))
10113+
}
10114+
10115+
check(
10116+
"MAP {1: 10.0, 2: 20.0}",
10117+
Expr::Map(Map {
10118+
entries: vec![
10119+
MapEntry {
10120+
key: Box::new(number_expr("1")),
10121+
value: Box::new(number_expr("10.0")),
10122+
},
10123+
MapEntry {
10124+
key: Box::new(number_expr("2")),
10125+
value: Box::new(number_expr("20.0")),
10126+
},
10127+
],
10128+
}),
10129+
);
10130+
10131+
check(
10132+
"MAP {[1, 2, 3]: 10.0, [4, 5, 6]: 20.0}",
10133+
Expr::Map(Map {
10134+
entries: vec![
10135+
MapEntry {
10136+
key: Box::new(Expr::Array(Array {
10137+
elem: vec![number_expr("1"), number_expr("2"), number_expr("3")],
10138+
named: false,
10139+
})),
10140+
value: Box::new(Expr::Value(number("10.0"))),
10141+
},
10142+
MapEntry {
10143+
key: Box::new(Expr::Array(Array {
10144+
elem: vec![number_expr("4"), number_expr("5"), number_expr("6")],
10145+
named: false,
10146+
})),
10147+
value: Box::new(Expr::Value(number("20.0"))),
10148+
},
10149+
],
10150+
}),
10151+
);
10152+
10153+
check(
10154+
"MAP {'a': 10, 'b': 20}['a']",
10155+
Expr::Subscript {
10156+
expr: Box::new(Expr::Map(Map {
10157+
entries: vec![
10158+
MapEntry {
10159+
key: Box::new(Expr::Value(Value::SingleQuotedString("a".to_owned()))),
10160+
value: Box::new(number_expr("10")),
10161+
},
10162+
MapEntry {
10163+
key: Box::new(Expr::Value(Value::SingleQuotedString("b".to_owned()))),
10164+
value: Box::new(number_expr("20")),
10165+
},
10166+
],
10167+
})),
10168+
subscript: Box::new(Subscript::Index {
10169+
index: Expr::Value(Value::SingleQuotedString("a".to_owned())),
10170+
}),
10171+
},
10172+
);
10173+
}
10174+
1008010175
#[test]
1008110176
fn parse_within_group() {
1008210177
verified_expr("PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY sales_amount)");

tests/sqlparser_custom_dialect.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,28 @@ fn custom_statement_parser() -> Result<(), ParserError> {
125125
Ok(())
126126
}
127127

128+
#[test]
129+
fn test_map_syntax_not_support_default() -> Result<(), ParserError> {
130+
#[derive(Debug)]
131+
struct MyDialect {}
132+
133+
impl Dialect for MyDialect {
134+
fn is_identifier_start(&self, ch: char) -> bool {
135+
is_identifier_start(ch)
136+
}
137+
138+
fn is_identifier_part(&self, ch: char) -> bool {
139+
is_identifier_part(ch)
140+
}
141+
}
142+
143+
let dialect = MyDialect {};
144+
let sql = "SELECT MAP {1: 2}";
145+
let ast = Parser::parse_sql(&dialect, sql);
146+
assert!(ast.is_err());
147+
Ok(())
148+
}
149+
128150
fn is_identifier_start(ch: char) -> bool {
129151
ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_'
130152
}

0 commit comments

Comments
 (0)