Skip to content

Commit e747c9c

Browse files
gstvgalamb
andauthored
Add support for DuckDB struct literal syntax (#1194)
Co-authored-by: Andrew Lamb <[email protected]>
1 parent 4472789 commit e747c9c

File tree

3 files changed

+158
-0
lines changed

3 files changed

+158
-0
lines changed

src/ast/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,23 @@ impl fmt::Display for StructField {
347347
}
348348
}
349349

350+
/// A dictionary field within a dictionary.
351+
///
352+
/// [duckdb]: https://duckdb.org/docs/sql/data_types/struct#creating-structs
353+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
354+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
355+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
356+
pub struct DictionaryField {
357+
pub key: Ident,
358+
pub value: Box<Expr>,
359+
}
360+
361+
impl fmt::Display for DictionaryField {
362+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363+
write!(f, "{}: {}", self.key, self.value)
364+
}
365+
}
366+
350367
/// Options for `CAST` / `TRY_CAST`
351368
/// BigQuery: <https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax>
352369
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@@ -687,6 +704,14 @@ pub enum Expr {
687704
expr: Box<Expr>,
688705
name: Ident,
689706
},
707+
/// `DuckDB` specific `Struct` literal expression [1]
708+
///
709+
/// Syntax:
710+
/// ```sql
711+
/// syntax: {'field_name': expr1[, ... ]}
712+
/// ```
713+
/// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs
714+
Dictionary(Vec<DictionaryField>),
690715
/// An array index expression e.g. `(ARRAY[1, 2])[1]` or `(current_schemas(FALSE))[1]`
691716
ArrayIndex {
692717
obj: Box<Expr>,
@@ -1146,6 +1171,9 @@ impl fmt::Display for Expr {
11461171
Expr::Named { expr, name } => {
11471172
write!(f, "{} AS {}", expr, name)
11481173
}
1174+
Expr::Dictionary(fields) => {
1175+
write!(f, "{{{}}}", display_comma_separated(fields))
1176+
}
11491177
Expr::ArrayIndex { obj, indexes } => {
11501178
write!(f, "{obj}")?;
11511179
for i in indexes {

src/parser/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,10 @@ impl<'a> Parser<'a> {
11171117
self.prev_token();
11181118
Ok(Expr::Value(self.parse_value()?))
11191119
}
1120+
Token::LBrace if dialect_of!(self is DuckDbDialect | GenericDialect) => {
1121+
self.prev_token();
1122+
self.parse_duckdb_struct_literal()
1123+
}
11201124
_ => self.expected("an expression:", next_token),
11211125
}?;
11221126

@@ -2127,6 +2131,45 @@ impl<'a> Parser<'a> {
21272131
))
21282132
}
21292133

2134+
/// DuckDB specific: Parse a duckdb dictionary [1]
2135+
///
2136+
/// Syntax:
2137+
///
2138+
/// ```sql
2139+
/// {'field_name': expr1[, ... ]}
2140+
/// ```
2141+
///
2142+
/// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs
2143+
fn parse_duckdb_struct_literal(&mut self) -> Result<Expr, ParserError> {
2144+
self.expect_token(&Token::LBrace)?;
2145+
2146+
let fields = self.parse_comma_separated(Self::parse_duckdb_dictionary_field)?;
2147+
2148+
self.expect_token(&Token::RBrace)?;
2149+
2150+
Ok(Expr::Dictionary(fields))
2151+
}
2152+
2153+
/// Parse a field for a duckdb dictionary [1]
2154+
/// Syntax
2155+
/// ```sql
2156+
/// 'name': expr
2157+
/// ```
2158+
///
2159+
/// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs
2160+
fn parse_duckdb_dictionary_field(&mut self) -> Result<DictionaryField, ParserError> {
2161+
let key = self.parse_identifier(false)?;
2162+
2163+
self.expect_token(&Token::Colon)?;
2164+
2165+
let expr = self.parse_expr()?;
2166+
2167+
Ok(DictionaryField {
2168+
key,
2169+
value: Box::new(expr),
2170+
})
2171+
}
2172+
21302173
/// For nested types that use the angle bracket syntax, this matches either
21312174
/// `>`, `>>` or nothing depending on which variant is expected (specified by the previously
21322175
/// matched `trailing_bracket` argument). It returns whether there is a trailing

tests/sqlparser_duckdb.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,90 @@ fn test_duckdb_load_extension() {
246246
stmt
247247
);
248248
}
249+
250+
#[test]
251+
fn test_duckdb_struct_literal() {
252+
//struct literal syntax https://duckdb.org/docs/sql/data_types/struct#creating-structs
253+
//syntax: {'field_name': expr1[, ... ]}
254+
let sql = "SELECT {'a': 1, 'b': 2, 'c': 3}, [{'a': 'abc'}], {'a': 1, 'b': [t.str_col]}, {'a': 1, 'b': 'abc'}, {'abc': str_col}, {'a': {'aa': 1}}";
255+
let select = duckdb_and_generic().verified_only_select(sql);
256+
assert_eq!(6, select.projection.len());
257+
assert_eq!(
258+
&Expr::Dictionary(vec![
259+
DictionaryField {
260+
key: Ident::with_quote('\'', "a"),
261+
value: Box::new(Expr::Value(number("1"))),
262+
},
263+
DictionaryField {
264+
key: Ident::with_quote('\'', "b"),
265+
value: Box::new(Expr::Value(number("2"))),
266+
},
267+
DictionaryField {
268+
key: Ident::with_quote('\'', "c"),
269+
value: Box::new(Expr::Value(number("3"))),
270+
},
271+
],),
272+
expr_from_projection(&select.projection[0])
273+
);
274+
275+
assert_eq!(
276+
&Expr::Array(Array {
277+
elem: vec![Expr::Dictionary(vec![DictionaryField {
278+
key: Ident::with_quote('\'', "a"),
279+
value: Box::new(Expr::Value(Value::SingleQuotedString("abc".to_string()))),
280+
},],)],
281+
named: false
282+
}),
283+
expr_from_projection(&select.projection[1])
284+
);
285+
assert_eq!(
286+
&Expr::Dictionary(vec![
287+
DictionaryField {
288+
key: Ident::with_quote('\'', "a"),
289+
value: Box::new(Expr::Value(number("1"))),
290+
},
291+
DictionaryField {
292+
key: Ident::with_quote('\'', "b"),
293+
value: Box::new(Expr::Array(Array {
294+
elem: vec![Expr::CompoundIdentifier(vec![
295+
Ident::from("t"),
296+
Ident::from("str_col")
297+
])],
298+
named: false
299+
})),
300+
},
301+
],),
302+
expr_from_projection(&select.projection[2])
303+
);
304+
assert_eq!(
305+
&Expr::Dictionary(vec![
306+
DictionaryField {
307+
key: Ident::with_quote('\'', "a"),
308+
value: Expr::Value(number("1")).into(),
309+
},
310+
DictionaryField {
311+
key: Ident::with_quote('\'', "b"),
312+
value: Expr::Value(Value::SingleQuotedString("abc".to_string())).into(),
313+
},
314+
],),
315+
expr_from_projection(&select.projection[3])
316+
);
317+
assert_eq!(
318+
&Expr::Dictionary(vec![DictionaryField {
319+
key: Ident::with_quote('\'', "abc"),
320+
value: Expr::Identifier(Ident::from("str_col")).into(),
321+
}],),
322+
expr_from_projection(&select.projection[4])
323+
);
324+
assert_eq!(
325+
&Expr::Dictionary(vec![DictionaryField {
326+
key: Ident::with_quote('\'', "a"),
327+
value: Expr::Dictionary(vec![DictionaryField {
328+
key: Ident::with_quote('\'', "aa"),
329+
value: Expr::Value(number("1")).into(),
330+
}],)
331+
.into(),
332+
}],),
333+
expr_from_projection(&select.projection[5])
334+
);
335+
}

0 commit comments

Comments
 (0)