Skip to content

Commit e98a9f1

Browse files
gstvgalamb
authored andcommitted
Add support for DuckDB struct literal syntax (apache#1194)
Co-authored-by: Andrew Lamb <[email protected]>
1 parent 0ba09b6 commit e98a9f1

File tree

3 files changed

+159
-1
lines changed

3 files changed

+159
-1
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: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,4 +314,91 @@ fn test_drop_secret_simple() {
314314
,
315315
stmt
316316
);
317-
}
317+
}
318+
319+
#[test]
320+
fn test_duckdb_struct_literal() {
321+
//struct literal syntax https://duckdb.org/docs/sql/data_types/struct#creating-structs
322+
//syntax: {'field_name': expr1[, ... ]}
323+
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}}";
324+
let select = duckdb_and_generic().verified_only_select(sql);
325+
assert_eq!(6, select.projection.len());
326+
assert_eq!(
327+
&Expr::Dictionary(vec![
328+
DictionaryField {
329+
key: Ident::with_quote('\'', "a"),
330+
value: Box::new(Expr::Value(number("1"))),
331+
},
332+
DictionaryField {
333+
key: Ident::with_quote('\'', "b"),
334+
value: Box::new(Expr::Value(number("2"))),
335+
},
336+
DictionaryField {
337+
key: Ident::with_quote('\'', "c"),
338+
value: Box::new(Expr::Value(number("3"))),
339+
},
340+
],),
341+
expr_from_projection(&select.projection[0])
342+
);
343+
344+
assert_eq!(
345+
&Expr::Array(Array {
346+
elem: vec![Expr::Dictionary(vec![DictionaryField {
347+
key: Ident::with_quote('\'', "a"),
348+
value: Box::new(Expr::Value(Value::SingleQuotedString("abc".to_string()))),
349+
},],)],
350+
named: false
351+
}),
352+
expr_from_projection(&select.projection[1])
353+
);
354+
assert_eq!(
355+
&Expr::Dictionary(vec![
356+
DictionaryField {
357+
key: Ident::with_quote('\'', "a"),
358+
value: Box::new(Expr::Value(number("1"))),
359+
},
360+
DictionaryField {
361+
key: Ident::with_quote('\'', "b"),
362+
value: Box::new(Expr::Array(Array {
363+
elem: vec![Expr::CompoundIdentifier(vec![
364+
Ident::from("t"),
365+
Ident::from("str_col")
366+
])],
367+
named: false
368+
})),
369+
},
370+
],),
371+
expr_from_projection(&select.projection[2])
372+
);
373+
assert_eq!(
374+
&Expr::Dictionary(vec![
375+
DictionaryField {
376+
key: Ident::with_quote('\'', "a"),
377+
value: Expr::Value(number("1")).into(),
378+
},
379+
DictionaryField {
380+
key: Ident::with_quote('\'', "b"),
381+
value: Expr::Value(Value::SingleQuotedString("abc".to_string())).into(),
382+
},
383+
],),
384+
expr_from_projection(&select.projection[3])
385+
);
386+
assert_eq!(
387+
&Expr::Dictionary(vec![DictionaryField {
388+
key: Ident::with_quote('\'', "abc"),
389+
value: Expr::Identifier(Ident::from("str_col")).into(),
390+
}],),
391+
expr_from_projection(&select.projection[4])
392+
);
393+
assert_eq!(
394+
&Expr::Dictionary(vec![DictionaryField {
395+
key: Ident::with_quote('\'', "a"),
396+
value: Expr::Dictionary(vec![DictionaryField {
397+
key: Ident::with_quote('\'', "aa"),
398+
value: Expr::Value(number("1")).into(),
399+
}],)
400+
.into(),
401+
}],),
402+
expr_from_projection(&select.projection[5])
403+
);
404+
}

0 commit comments

Comments
 (0)