From 9d339c904995a12869dc05f2fed1e8b19a2a572a Mon Sep 17 00:00:00 2001 From: osipovartem Date: Wed, 9 Sep 2026 01:26:18 +0300 Subject: [PATCH 1/2] Snowflake: parse structured ARRAY types --- src/dialect/clickhouse.rs | 4 ++++ src/dialect/mod.rs | 10 ++++++++++ src/dialect/snowflake.rs | 5 +++++ src/parser/mod.rs | 8 +++++--- tests/sqlparser_snowflake.rs | 12 ++++++++++++ 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index c81d953d1b..1a1f168915 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -79,6 +79,10 @@ impl Dialect for ClickHouseDialect { true } + fn supports_array_typedef_with_parentheses(&self) -> bool { + true + } + // ClickHouse uses this for some FORMAT expressions in `INSERT` context, e.g. when inserting // with FORMAT JSONEachRow a raw JSON key-value expression is valid and expected. // diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 0f3f5642e1..9ffa62a851 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1113,6 +1113,16 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if this dialect supports the `ARRAY(element_type)` syntax. + /// + /// Example: + /// ```sql + /// CREATE TABLE t (a ARRAY(VARCHAR)); + /// ``` + fn supports_array_typedef_with_parentheses(&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 41a8f8708e..64846acd87 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#label-structured-types-array) + fn supports_array_typedef_with_parentheses(&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 eb435f153a..a1f4b13a3f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13120,12 +13120,14 @@ impl<'a> Parser<'a> { Keyword::ENUM16 => Ok(DataType::Enum(self.parse_enum_values()?, Some(16))), Keyword::SET => Ok(DataType::Set(self.parse_string_values()?)), Keyword::ARRAY => { - if self.dialect.supports_array_typedef_without_element_type() { - Ok(DataType::Array(ArrayElemTypeDef::None)) - } else if dialect_of!(self is ClickHouseDialect) { + if self.dialect.supports_array_typedef_with_parentheses() + && self.peek_token_ref().token == Token::LParen + { Ok(self.parse_sub_type(|internal_type| { DataType::Array(ArrayElemTypeDef::Parenthesis(internal_type)) })?) + } else if self.dialect.supports_array_typedef_without_element_type() { + Ok(DataType::Array(ArrayElemTypeDef::None)) } else { self.expect_token(&Token::Lt)?; let (inside_type, _trailing_bracket) = self.parse_data_type_helper()?; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 37222341b1..44452aa45b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4913,6 +4913,18 @@ fn test_select_dollar_column_from_stage() { snowflake().verified_stmt("SELECT $1, $2 FROM @mystage1(file_format => 'myformat')"); } +#[test] +fn test_structured_array_type() { + snowflake().one_statement_parses_to( + "CREATE TABLE t (a ARRAY(VARCHAR))", + "CREATE TABLE t (a Array(VARCHAR))", + ); + snowflake().one_statement_parses_to( + "SELECT CAST(a AS ARRAY(NUMBER(10, 2))) FROM t", + "SELECT CAST(a AS Array(NUMBER(10, 2))) FROM t", + ); +} + #[test] fn test_snowflake_stage_name_with_escaped_quotes() { snowflake().verified_stmt("REMOVE @````"); From 029d3048fab8cb3c1f729e82f876d64a4c9aa66b Mon Sep 17 00:00:00 2001 From: osipovartem Date: Wed, 23 Sep 2026 01:31:55 +0300 Subject: [PATCH 2/2] fix: preserve Snowflake ARRAY element nullability --- src/ast/data_type.rs | 5 +++++ src/dialect/mod.rs | 6 ++++++ src/dialect/snowflake.rs | 4 ++++ src/parser/mod.rs | 28 ++++++++++++++++++++++------ tests/sqlparser_clickhouse.rs | 10 ++++++++++ tests/sqlparser_snowflake.rs | 12 ++++++++++++ 6 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index be1acd1a7c..d51ff2341c 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -720,6 +720,9 @@ impl fmt::Display for DataType { ArrayElemTypeDef::SquareBracket(t, Some(size)) => write!(f, "{t}[{size}]"), ArrayElemTypeDef::AngleBracket(t) => write!(f, "ARRAY<{t}>"), ArrayElemTypeDef::Parenthesis(t) => write!(f, "Array({t})"), + ArrayElemTypeDef::ParenthesisNotNull(t) => { + write!(f, "ARRAY({t} NOT NULL)") + } ArrayElemTypeDef::Qualified(t, None) => write!(f, "{t} ARRAY"), ArrayElemTypeDef::Qualified(t, Some(size)) => write!(f, "{t} ARRAY[{size}]"), }, @@ -1165,6 +1168,8 @@ pub enum ArrayElemTypeDef { SquareBracket(Box, Option), /// Parenthesis style, e.g. `Array(Int64)`. Parenthesis(Box), + /// Parenthesis style with a non-null element constraint, e.g. `ARRAY(INT NOT NULL)`. + ParenthesisNotNull(Box), /// Qualified by a data type and optional size, e.g. `INT ARRAY` or `INT ARRAY[4]`. Qualified(Box, Option), } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 9ffa62a851..3ff9ca193e 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1123,6 +1123,12 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if this dialect supports `NOT NULL` on an element type in + /// an `ARRAY(element_type)` definition. + fn supports_array_element_not_null(&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 64846acd87..ce9ccfb028 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -245,6 +245,10 @@ impl Dialect for SnowflakeDialect { true } + fn supports_array_element_not_null(&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 a1f4b13a3f..528f31ca44 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13120,12 +13120,28 @@ impl<'a> Parser<'a> { Keyword::ENUM16 => Ok(DataType::Enum(self.parse_enum_values()?, Some(16))), Keyword::SET => Ok(DataType::Set(self.parse_string_values()?)), Keyword::ARRAY => { - if self.dialect.supports_array_typedef_with_parentheses() - && self.peek_token_ref().token == Token::LParen - { - Ok(self.parse_sub_type(|internal_type| { - DataType::Array(ArrayElemTypeDef::Parenthesis(internal_type)) - })?) + if self.dialect.supports_array_typedef_with_parentheses() { + if self.peek_token_ref().token == Token::LParen { + self.expect_token(&Token::LParen)?; + let internal_type = self.parse_data_type()?; + let not_null = self.dialect.supports_array_element_not_null() + && self.parse_keywords(&[Keyword::NOT, Keyword::NULL]); + self.expect_token(&Token::RParen)?; + + if not_null { + Ok(DataType::Array(ArrayElemTypeDef::ParenthesisNotNull( + Box::new(internal_type), + ))) + } else { + Ok(DataType::Array(ArrayElemTypeDef::Parenthesis(Box::new( + internal_type, + )))) + } + } else if self.dialect.supports_array_typedef_without_element_type() { + Ok(DataType::Array(ArrayElemTypeDef::None)) + } else { + self.expected("(", self.peek_token()) + } } else if self.dialect.supports_array_typedef_without_element_type() { Ok(DataType::Array(ArrayElemTypeDef::None)) } else { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 258f443679..9cb242e545 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -779,6 +779,16 @@ fn parse_create_table_with_nested_data_types() { } } +#[test] +fn reject_angle_bracket_array_type() { + assert_eq!( + clickhouse() + .parse_sql_statements("CREATE TABLE t (a ARRAY)") + .unwrap_err(), + ParserError("Expected: (, found: <".to_string()) + ); +} + #[test] fn parse_create_table_with_primary_key() { match clickhouse_and_generic().verified_stmt(concat!( diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 44452aa45b..ab6e24254d 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4923,6 +4923,18 @@ fn test_structured_array_type() { "SELECT CAST(a AS ARRAY(NUMBER(10, 2))) FROM t", "SELECT CAST(a AS Array(NUMBER(10, 2))) FROM t", ); + snowflake().verified_stmt("CREATE TABLE t (a ARRAY(VARCHAR NOT NULL))"); + let select = + snowflake().verified_only_select("SELECT CAST(a AS ARRAY(VARCHAR NOT NULL)) FROM t"); + let Expr::Cast { data_type, .. } = expr_from_projection(only(&select.projection)) else { + unreachable!(); + }; + assert_eq!( + data_type, + &DataType::Array(ArrayElemTypeDef::ParenthesisNotNull(Box::new( + DataType::Varchar(None) + ))) + ); } #[test]