From 5c9be32322d55493797722f70f86dfd81baa1de4 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Wed, 9 Sep 2026 01:29:09 +0300 Subject: [PATCH] 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 | 37 ++++++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 285eec5054..a074b6dc5b 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -451,6 +451,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. @@ -794,6 +798,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 ba2d37bc90..5599a7779c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1076,6 +1076,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 b20bf4fd19..764c528299 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -242,6 +242,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 80b33f9d92..bfc9c2cf3e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12646,6 +12646,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()?; @@ -13847,6 +13853,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 519a28ef26..920d168a42 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -5202,3 +5202,40 @@ fn test_external_volume() { "Unexpected error: {err}" ); } + +#[test] +fn test_structured_object_type() { + snowflake().one_statement_parses_to( + "SELECT payload::OBJECT(tags ARRAY(VARCHAR), address OBJECT(city VARCHAR NOT NULL)) FROM t", + "SELECT payload::OBJECT(tags Array(VARCHAR), address OBJECT(city VARCHAR NOT NULL)) 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}"); + } +}