diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index be1acd1a7..81e2b74a9 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -465,6 +465,13 @@ pub enum DataType { /// /// [DuckDB]: https://duckdb.org/docs/sql/data_types/union.html Union(Vec), + /// Object type, see [Snowflake]. + /// + /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/data-types-semistructured#object + Object { + /// `None` for bare `OBJECT`, `Some` when parentheses are present (possibly empty). + fields: Option>, + }, /// Nullable - special marker NULL represents in ClickHouse as a data type. /// /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nullable @@ -778,6 +785,10 @@ impl fmt::Display for DataType { DataType::Union(fields) => { write!(f, "UNION({})", display_comma_separated(fields)) } + DataType::Object { fields } => match fields { + None => write!(f, "OBJECT"), + Some(fields) => write!(f, "OBJECT({})", display_comma_separated(fields)), + }, // ClickHouse DataType::Nullable(data_type) => { write!(f, "Nullable({data_type})") diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b2b3f42bb..be324a95a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3657,6 +3657,32 @@ impl<'a> Parser<'a> { /// ``` /// /// [1]: https://duckdb.org/docs/sql/data_types/union.html + fn parse_object_data_type(&mut self) -> Result { + self.expect_keyword_is(Keyword::OBJECT)?; + // Object type may have no fields: OBJECT or OBJECT() + if !self.peek_token_ref().token.eq(&Token::LParen) { + return Ok(DataType::Object { fields: None }); + } + self.expect_token(&Token::LParen)?; + let fields = if self.peek_token_ref().token == Token::RParen { + vec![] + } else { + self.parse_comma_separated(|parser| { + let field_name = parser.parse_identifier()?; + let field_type = parser.parse_data_type()?; + Ok(StructField { + field_name: Some(field_name), + field_type, + options: None, + }) + })? + }; + self.expect_token(&Token::RParen)?; + Ok(DataType::Object { + fields: Some(fields), + }) + } + fn parse_union_type_def(&mut self) -> Result, ParserError> { self.expect_keyword_is(Keyword::UNION)?; @@ -13115,6 +13141,10 @@ impl<'a> Parser<'a> { let fields = self.parse_union_type_def()?; Ok(DataType::Union(fields)) } + Keyword::OBJECT if dialect_is!(dialect is SnowflakeDialect | GenericDialect) => { + self.prev_token(); + self.parse_object_data_type() + } Keyword::NULLABLE if dialect_is!(dialect is ClickHouseDialect | GenericDialect) => { Ok(self.parse_sub_type(DataType::Nullable)?) } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 059560dcc..24c653d9f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4912,3 +4912,21 @@ 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 parse_nested_object() { + // nested OBJECT with a single field + snowflake().verified_stmt("SELECT TRY_CAST(PARSE_JSON('{\"obj_field\":{\"field\":\"value\",}}') AS OBJECT(obj_field OBJECT(field VARCHAR)))"); + + // OBJECT with multiple fields + snowflake().verified_stmt("SELECT CAST(v AS OBJECT(a VARCHAR, b INT, c BOOLEAN))"); + + // nested OBJECT with multiple fields at both levels + snowflake().verified_stmt("SELECT CAST(v AS OBJECT(x OBJECT(a INT, b VARCHAR), y NUMBER))"); + + // OBJECT with zero fields (empty parentheses) + snowflake().verified_stmt("SELECT CAST(v AS OBJECT())"); + + // bare OBJECT without parentheses round-trips as OBJECT + snowflake().verified_stmt("SELECT CAST(v AS OBJECT)"); +}