diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3e8354e15..7818dacd3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -347,6 +347,23 @@ impl fmt::Display for StructField { } } +/// A dictionary field within a dictionary. +/// +/// [duckdb]: https://duckdb.org/docs/sql/data_types/struct#creating-structs +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DictionaryField { + pub key: Ident, + pub value: Box, +} + +impl fmt::Display for DictionaryField { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.key, self.value) + } +} + /// Options for `CAST` / `TRY_CAST` /// BigQuery: #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -687,6 +704,14 @@ pub enum Expr { expr: Box, name: Ident, }, + /// `DuckDB` specific `Struct` literal expression [1] + /// + /// Syntax: + /// ```sql + /// syntax: {'field_name': expr1[, ... ]} + /// ``` + /// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs + Dictionary(Vec), /// An array index expression e.g. `(ARRAY[1, 2])[1]` or `(current_schemas(FALSE))[1]` ArrayIndex { obj: Box, @@ -1146,6 +1171,9 @@ impl fmt::Display for Expr { Expr::Named { expr, name } => { write!(f, "{} AS {}", expr, name) } + Expr::Dictionary(fields) => { + write!(f, "{{{}}}", display_comma_separated(fields)) + } Expr::ArrayIndex { obj, indexes } => { write!(f, "{obj}")?; for i in indexes { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 674d0692b..2a5e9567a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1117,6 +1117,10 @@ impl<'a> Parser<'a> { self.prev_token(); Ok(Expr::Value(self.parse_value()?)) } + Token::LBrace if dialect_of!(self is DuckDbDialect | GenericDialect) => { + self.prev_token(); + self.parse_duckdb_struct_literal() + } _ => self.expected("an expression:", next_token), }?; @@ -2127,6 +2131,45 @@ impl<'a> Parser<'a> { )) } + /// DuckDB specific: Parse a duckdb dictionary [1] + /// + /// Syntax: + /// + /// ```sql + /// {'field_name': expr1[, ... ]} + /// ``` + /// + /// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs + fn parse_duckdb_struct_literal(&mut self) -> Result { + self.expect_token(&Token::LBrace)?; + + let fields = self.parse_comma_separated(Self::parse_duckdb_dictionary_field)?; + + self.expect_token(&Token::RBrace)?; + + Ok(Expr::Dictionary(fields)) + } + + /// Parse a field for a duckdb dictionary [1] + /// Syntax + /// ```sql + /// 'name': expr + /// ``` + /// + /// [1]: https://duckdb.org/docs/sql/data_types/struct#creating-structs + fn parse_duckdb_dictionary_field(&mut self) -> Result { + let key = self.parse_identifier(false)?; + + self.expect_token(&Token::Colon)?; + + let expr = self.parse_expr()?; + + Ok(DictionaryField { + key, + value: Box::new(expr), + }) + } + /// For nested types that use the angle bracket syntax, this matches either /// `>`, `>>` or nothing depending on which variant is expected (specified by the previously /// matched `trailing_bracket` argument). It returns whether there is a trailing diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 45ae01bf0..a29d40084 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -246,3 +246,90 @@ fn test_duckdb_load_extension() { stmt ); } + +#[test] +fn test_duckdb_struct_literal() { + //struct literal syntax https://duckdb.org/docs/sql/data_types/struct#creating-structs + //syntax: {'field_name': expr1[, ... ]} + 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}}"; + let select = duckdb_and_generic().verified_only_select(sql); + assert_eq!(6, select.projection.len()); + assert_eq!( + &Expr::Dictionary(vec![ + DictionaryField { + key: Ident::with_quote('\'', "a"), + value: Box::new(Expr::Value(number("1"))), + }, + DictionaryField { + key: Ident::with_quote('\'', "b"), + value: Box::new(Expr::Value(number("2"))), + }, + DictionaryField { + key: Ident::with_quote('\'', "c"), + value: Box::new(Expr::Value(number("3"))), + }, + ],), + expr_from_projection(&select.projection[0]) + ); + + assert_eq!( + &Expr::Array(Array { + elem: vec![Expr::Dictionary(vec![DictionaryField { + key: Ident::with_quote('\'', "a"), + value: Box::new(Expr::Value(Value::SingleQuotedString("abc".to_string()))), + },],)], + named: false + }), + expr_from_projection(&select.projection[1]) + ); + assert_eq!( + &Expr::Dictionary(vec![ + DictionaryField { + key: Ident::with_quote('\'', "a"), + value: Box::new(Expr::Value(number("1"))), + }, + DictionaryField { + key: Ident::with_quote('\'', "b"), + value: Box::new(Expr::Array(Array { + elem: vec![Expr::CompoundIdentifier(vec![ + Ident::from("t"), + Ident::from("str_col") + ])], + named: false + })), + }, + ],), + expr_from_projection(&select.projection[2]) + ); + assert_eq!( + &Expr::Dictionary(vec![ + DictionaryField { + key: Ident::with_quote('\'', "a"), + value: Expr::Value(number("1")).into(), + }, + DictionaryField { + key: Ident::with_quote('\'', "b"), + value: Expr::Value(Value::SingleQuotedString("abc".to_string())).into(), + }, + ],), + expr_from_projection(&select.projection[3]) + ); + assert_eq!( + &Expr::Dictionary(vec![DictionaryField { + key: Ident::with_quote('\'', "abc"), + value: Expr::Identifier(Ident::from("str_col")).into(), + }],), + expr_from_projection(&select.projection[4]) + ); + assert_eq!( + &Expr::Dictionary(vec![DictionaryField { + key: Ident::with_quote('\'', "a"), + value: Expr::Dictionary(vec![DictionaryField { + key: Ident::with_quote('\'', "aa"), + value: Expr::Value(number("1")).into(), + }],) + .into(), + }],), + expr_from_projection(&select.projection[5]) + ); +}