This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-2488-9ae00e73b54ebbca2341c3de22312a8011e98757 in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
commit 667e81d7f5a682536725e9d9a64a6e62118cd21d Author: Artem Osipov <[email protected]> AuthorDate: Thu Sep 24 08:34:46 2026 +0000 Snowflake: Parse structured ARRAY types (#2488) Co-authored-by: Luca Cappelletti <[email protected]> --- src/ast/data_type.rs | 5 +++++ src/dialect/clickhouse.rs | 4 ++++ src/dialect/mod.rs | 16 ++++++++++++++++ src/dialect/snowflake.rs | 9 +++++++++ src/parser/mod.rs | 28 +++++++++++++++++++++++----- tests/sqlparser_clickhouse.rs | 10 ++++++++++ tests/sqlparser_snowflake.rs | 24 ++++++++++++++++++++++++ 7 files changed, 91 insertions(+), 5 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index be1acd1a7..d51ff2341 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<DataType>, Option<u64>), /// Parenthesis style, e.g. `Array(Int64)`. Parenthesis(Box<DataType>), + /// Parenthesis style with a non-null element constraint, e.g. `ARRAY(INT NOT NULL)`. + ParenthesisNotNull(Box<DataType>), /// Qualified by a data type and optional size, e.g. `INT ARRAY` or `INT ARRAY[4]`. Qualified(Box<DataType>, Option<u64>), } diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 880df16e3..adff426b0 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 eb5ec1dfa..80d5c5811 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1113,6 +1113,22 @@ 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 `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 41a8f8708..ce9ccfb02 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -240,6 +240,15 @@ 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 + } + + 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 cc8f71b17..d48f00e44 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13120,12 +13120,30 @@ 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() { + 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 if dialect_of!(self is ClickHouseDialect) { - Ok(self.parse_sub_type(|internal_type| { - DataType::Array(ArrayElemTypeDef::Parenthesis(internal_type)) - })?) } else { self.expect_token(&Token::Lt)?; let (inside_type, _trailing_bracket) = self.parse_data_type_helper()?; diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index e680aa0a0..2d55ae058 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<INT>)") + .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 37222341b..ab6e24254 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4913,6 +4913,30 @@ 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", + ); + 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] fn test_snowflake_stage_name_with_escaped_quotes() { snowflake().verified_stmt("REMOVE @````"); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
