From 1340cda86a6372ca2c02bbd0f0498436672bcda6 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Wed, 9 Sep 2026 01:29:09 +0300 Subject: [PATCH 1/3] Snowflake: parse structured OBJECT types --- src/ast/data_type.rs | 7 +++++++ src/dialect/mod.rs | 10 ++++++++++ src/dialect/snowflake.rs | 5 +++++ src/parser/mod.rs | 29 +++++++++++++++++++++++++++++ tests/sqlparser_snowflake.rs | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+) 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/dialect/mod.rs b/src/dialect/mod.rs index ff83a4da61..12b996f4df 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1083,6 +1083,16 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if this dialect supports structured `OBJECT` types. + /// + /// Example: + /// ```sql + /// CREATE TABLE t (o OBJECT(city VARCHAR, zip NUMBER NOT NULL)); + /// ``` + fn supports_structured_object_type(&self) -> bool { + false + } + /// Returns true if this dialect supports extra parentheses around /// lone table names or derived tables in the `FROM` clause. /// diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 0bedb12a55..cfcee2e4a4 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -240,6 +240,11 @@ impl Dialect for SnowflakeDialect { true } + /// See [doc](https://docs.snowflake.com/en/sql-reference/data-types-structured#structured-object-types) + fn supports_structured_object_type(&self) -> bool { + true + } + /// See [doc](https://docs.snowflake.com/en/sql-reference/constructs/from) fn supports_parens_around_table_factor(&self) -> bool { true diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5edc437145..f77fd519a0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13116,6 +13116,12 @@ impl<'a> Parser<'a> { )))) } } + Keyword::OBJECT + if self.dialect.supports_structured_object_type() + && 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 +14339,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..a05709f6cd 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4912,3 +4912,38 @@ 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().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().verified_stmt("CREATE TABLE t (o OBJECT)"); +} + +#[test] +fn test_structured_object_type_errors() { + 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}"); + } +} From 02dddd110b882d8ec50b3ae109dce3c92d3038d1 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Thu, 10 Sep 2026 11:20:24 +0300 Subject: [PATCH 2/3] docs: link structured object type syntax --- src/dialect/snowflake.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index cfcee2e4a4..89dbe2fd7b 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -240,7 +240,7 @@ impl Dialect for SnowflakeDialect { true } - /// See [doc](https://docs.snowflake.com/en/sql-reference/data-types-structured#structured-object-types) + /// See [doc](https://docs.snowflake.com/en/sql-reference/data-types-structured#specifying-a-structured-object-type) fn supports_structured_object_type(&self) -> bool { true } From ef16bd9d1233673cb55c64750b9b3fa0b445f19e Mon Sep 17 00:00:00 2001 From: osipovartem Date: Fri, 11 Sep 2026 16:44:21 +0300 Subject: [PATCH 3/3] Apply structured OBJECT parsing across dialects --- src/dialect/mod.rs | 10 ---------- src/dialect/snowflake.rs | 5 ----- src/parser/mod.rs | 5 +---- tests/sqlparser_snowflake.rs | 7 ++----- 4 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 12b996f4df..ff83a4da61 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1083,16 +1083,6 @@ pub trait Dialect: Debug + Any { false } - /// Returns true if this dialect supports structured `OBJECT` types. - /// - /// Example: - /// ```sql - /// CREATE TABLE t (o OBJECT(city VARCHAR, zip NUMBER NOT NULL)); - /// ``` - fn supports_structured_object_type(&self) -> bool { - false - } - /// Returns true if this dialect supports extra parentheses around /// lone table names or derived tables in the `FROM` clause. /// diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 89dbe2fd7b..0bedb12a55 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -240,11 +240,6 @@ impl Dialect for SnowflakeDialect { true } - /// See [doc](https://docs.snowflake.com/en/sql-reference/data-types-structured#specifying-a-structured-object-type) - fn supports_structured_object_type(&self) -> bool { - true - } - /// See [doc](https://docs.snowflake.com/en/sql-reference/constructs/from) fn supports_parens_around_table_factor(&self) -> bool { true diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f77fd519a0..16fa51db91 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13116,10 +13116,7 @@ impl<'a> Parser<'a> { )))) } } - Keyword::OBJECT - if self.dialect.supports_structured_object_type() - && self.peek_token_ref().token == Token::LParen => - { + 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) => { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index a05709f6cd..7bebd42b0b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4914,7 +4914,7 @@ fn test_select_dollar_column_from_stage() { } #[test] fn test_structured_object_type() { - snowflake().verified_stmt( + snowflake_and_generic().verified_stmt( "SELECT payload::OBJECT(address OBJECT(city VARCHAR NOT NULL), zip NUMBER) FROM t", ); @@ -4934,11 +4934,8 @@ fn test_structured_object_type() { assert_eq!(fields[1].options.len(), 1); assert_eq!(fields[1].options[0].option, ColumnOption::NotNull); - snowflake().verified_stmt("CREATE TABLE t (o OBJECT)"); -} + snowflake_and_generic().verified_stmt("CREATE TABLE t (o OBJECT)"); -#[test] -fn test_structured_object_type_errors() { for sql in [ "CREATE TABLE t (o OBJECT(VARCHAR))", "CREATE TABLE t (o OBJECT(city VARCHAR NULL))",