From cb33ca17136ff25fb3fddbed075112b140735d5f Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Sun, 31 May 2026 13:34:06 +0300 Subject: [PATCH 1/3] added support for parsing nested object in query for snowflake --- src/ast/data_type.rs | 7 +++++++ src/parser/mod.rs | 25 +++++++++++++++++++++++++ tests/sqlparser_snowflake.rs | 9 +++++++++ 3 files changed, 41 insertions(+) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index f129a557c..861d84795 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -465,6 +465,10 @@ 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(Vec), /// Nullable - special marker NULL represents in ClickHouse as a data type. /// /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nullable @@ -776,6 +780,9 @@ impl fmt::Display for DataType { DataType::Union(fields) => { write!(f, "UNION({})", display_comma_separated(fields)) } + DataType::Object(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 3c6185193..8c2781460 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12733,6 +12733,31 @@ 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.expect_keyword_is(Keyword::OBJECT)?; + // Object type may have no fields: OBJECT or OBJECT() + if !self.peek_token_ref().token.eq(&Token::LParen) { + Ok(DataType::Object(vec![])) + } else { + 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)) + } + } 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 0000b0a3d..c2d86fcb5 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -5254,3 +5254,12 @@ 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() { + let sql = r#"SELECT TRY_CAST(PARSE_JSON('{"obj_field":{"field":"value",}}') AS OBJECT(obj_field OBJECT( + field VARCHAR +)));"#; + + snowflake().parse_sql_statements(sql).unwrap(); +} \ No newline at end of file From 141df5805d12981d9afcd7c8e2b5a8c65aadc7d9 Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Thu, 13 Aug 2026 13:38:21 +0300 Subject: [PATCH 2/3] PR fixes, including: added tests, type expansion. --- src/ast/data_type.rs | 12 ++++++--- src/parser/mod.rs | 47 +++++++++++++++++++----------------- tests/sqlparser_snowflake.rs | 17 ++++++++++--- 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 861d84795..843990d68 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -468,7 +468,10 @@ pub enum DataType { /// Object type, see [Snowflake]. /// /// [Snowflake]: https://docs.snowflake.com/en/sql-reference/data-types-semistructured#object - Object(Vec), + 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 @@ -780,9 +783,10 @@ impl fmt::Display for DataType { DataType::Union(fields) => { write!(f, "UNION({})", display_comma_separated(fields)) } - DataType::Object(fields) => { - write!(f, "OBJECT({})", 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 8c2781460..353f5b9a0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3561,6 +3561,30 @@ 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)?; @@ -12735,28 +12759,7 @@ impl<'a> Parser<'a> { } Keyword::OBJECT if dialect_is!(dialect is SnowflakeDialect | GenericDialect) => { self.prev_token(); - self.expect_keyword_is(Keyword::OBJECT)?; - // Object type may have no fields: OBJECT or OBJECT() - if !self.peek_token_ref().token.eq(&Token::LParen) { - Ok(DataType::Object(vec![])) - } else { - 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)) - } + 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 c2d86fcb5..97ed265f5 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -5257,9 +5257,18 @@ fn test_select_dollar_column_from_stage() { #[test] fn parse_nested_object() { - let sql = r#"SELECT TRY_CAST(PARSE_JSON('{"obj_field":{"field":"value",}}') AS OBJECT(obj_field OBJECT( - field VARCHAR -)));"#; + // 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)))"); - snowflake().parse_sql_statements(sql).unwrap(); + // 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)"); } \ No newline at end of file From e8e783abd2767871b743412333dc317221efd777 Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Thu, 13 Aug 2026 16:47:06 +0300 Subject: [PATCH 3/3] fix cargo fmt check --- src/parser/mod.rs | 4 +++- tests/sqlparser_snowflake.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5535b7c4a..be324a95a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3678,7 +3678,9 @@ impl<'a> Parser<'a> { })? }; self.expect_token(&Token::RParen)?; - Ok(DataType::Object { fields: Some(fields) }) + Ok(DataType::Object { + fields: Some(fields), + }) } fn parse_union_type_def(&mut self) -> Result, ParserError> { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 6a77a4434..24c653d9f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4929,4 +4929,4 @@ fn parse_nested_object() { // bare OBJECT without parentheses round-trips as OBJECT snowflake().verified_stmt("SELECT CAST(v AS OBJECT)"); -} \ No newline at end of file +}