Skip to content

Commit c3129ee

Browse files
goldmedalayman-sigma
authored andcommitted
Support Map literal syntax for DuckDB and Generic (apache#1344)
1 parent 5b8aa21 commit c3129ee

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)]
@@ -773,6 +804,14 @@ pub enum Expr {
773804
/// ```
774805
/// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs
775806
Dictionary(Vec<DictionaryField>),
807+
/// `DuckDB` specific `Map` literal expression [1]
808+
///
809+
/// Syntax:
810+
/// ```sql
811+
/// syntax: Map {key1: value1[, ... ]}
812+
/// ```
813+
/// [1]: https://duckdb.org/docs/sql/data_types/map#creating-maps
814+
Map(Map),
776815
/// An access of nested data using subscript syntax, for example `array[2]`.
777816
Subscript {
778817
expr: Box<Expr>,
@@ -1352,6 +1391,9 @@ impl fmt::Display for Expr {
13521391
Expr::Dictionary(fields) => {
13531392
write!(f, "{{{}}}", display_comma_separated(fields))
13541393
}
1394+
Expr::Map(map) => {
1395+
write!(f, "{map}")
1396+
}
13551397
Expr::Subscript {
13561398
expr,
13571399
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 {
@@ -2326,6 +2329,47 @@ impl<'a> Parser<'a> {
23262329
})
23272330
}
23282331

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

tests/sqlparser_common.rs

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

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