diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index be1acd1a7c..d30ecc0d9c 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -452,6 +452,10 @@ pub enum DataType { /// /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nested-data-structures/nested Nested(Vec), + /// Structured object type, see [Snowflake]. + /// + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/data-types-structured#structured-object-types + Object(Vec), /// Enum type. Enum(Vec, Option), /// Set type. @@ -802,6 +806,9 @@ impl fmt::Display for DataType { DataType::Nested(fields) => { write!(f, "Nested({})", display_comma_separated(fields)) } + DataType::Object(fields) => { + write!(f, "OBJECT({})", display_comma_separated(fields)) + } DataType::Unspecified => Ok(()), DataType::Trigger => write!(f, "TRIGGER"), DataType::AnyType => write!(f, "ANY TYPE"), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5edc437145..16fa51db91 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13116,6 +13116,9 @@ impl<'a> Parser<'a> { )))) } } + Keyword::OBJECT if self.peek_token_ref().token == Token::LParen => { + Ok(DataType::Object(self.parse_structured_object_type_def()?)) + } Keyword::STRUCT if dialect_is!(dialect is DuckDbDialect) => { self.prev_token(); let field_defs = self.parse_duckdb_struct_type_def()?; @@ -14333,6 +14336,29 @@ impl<'a> Parser<'a> { } } + fn parse_structured_object_type_def(&mut self) -> Result, ParserError> { + self.expect_token(&Token::LParen)?; + let fields = self.parse_comma_separated(|parser| { + let name = parser.parse_identifier()?; + let data_type = parser.parse_data_type()?; + let options = if parser.parse_keywords(&[Keyword::NOT, Keyword::NULL]) { + vec![ColumnOptionDef { + name: None, + option: ColumnOption::NotNull, + }] + } else { + vec![] + }; + Ok(ColumnDef { + name, + data_type, + options, + }) + })?; + self.expect_token(&Token::RParen)?; + Ok(fields) + } + /// Parse a parenthesized sub data type fn parse_sub_type(&mut self, parent_type: F) -> Result where diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 059560dcc9..7bebd42b0b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4912,3 +4912,35 @@ fn test_select_dollar_column_from_stage() { // With table function args, without alias snowflake().verified_stmt("SELECT $1, $2 FROM @mystage1(file_format => 'myformat')"); } +#[test] +fn test_structured_object_type() { + snowflake_and_generic().verified_stmt( + "SELECT payload::OBJECT(address OBJECT(city VARCHAR NOT NULL), zip NUMBER) FROM t", + ); + + let select = snowflake().verified_only_select( + "SELECT CAST(payload AS OBJECT(city VARCHAR, zip NUMBER NOT NULL)) FROM t", + ); + let Expr::Cast { data_type, .. } = expr_from_projection(only(&select.projection)) else { + unreachable!(); + }; + let DataType::Object(fields) = data_type else { + unreachable!(); + }; + assert_eq!(fields.len(), 2); + assert_eq!(fields[0].name, Ident::new("city")); + assert!(fields[0].options.is_empty()); + assert_eq!(fields[1].name, Ident::new("zip")); + assert_eq!(fields[1].options.len(), 1); + assert_eq!(fields[1].options[0].option, ColumnOption::NotNull); + + snowflake_and_generic().verified_stmt("CREATE TABLE t (o OBJECT)"); + + for sql in [ + "CREATE TABLE t (o OBJECT(VARCHAR))", + "CREATE TABLE t (o OBJECT(city VARCHAR NULL))", + "CREATE TABLE t (o OBJECT(city VARCHAR)", + ] { + assert!(snowflake().parse_sql_statements(sql).is_err(), "{sql}"); + } +}