diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index a074b6dc5..65700a79d 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -443,6 +443,8 @@ pub enum DataType { /// /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/map Map(Box, Box), + /// Snowflake structured MAP with optional non-null values. + SnowflakeMap(Box, Box, bool), /// Tuple, see [ClickHouse]. /// /// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple @@ -723,6 +725,13 @@ 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::SnowflakeParenthesis(t, not_null) => { + write!(f, "ARRAY({t}")?; + if *not_null { + write!(f, " NOT NULL")?; + } + write!(f, ")") + } }, DataType::Custom(ty, modifiers) => { if modifiers.is_empty() { @@ -792,6 +801,13 @@ impl fmt::Display for DataType { DataType::Map(key_data_type, value_data_type) => { write!(f, "Map({key_data_type}, {value_data_type})") } + DataType::SnowflakeMap(key_data_type, value_data_type, not_null) => { + write!(f, "MAP({key_data_type}, {value_data_type}")?; + if *not_null { + write!(f, " NOT NULL")?; + } + write!(f, ")") + } DataType::Tuple(fields) => { write!(f, "Tuple({})", display_comma_separated(fields)) } @@ -1153,6 +1169,8 @@ pub enum ArrayElemTypeDef { SquareBracket(Box, Option), /// Parenthesis style, e.g. `Array(Int64)`. Parenthesis(Box), + /// Snowflake parenthesis style, e.g. `ARRAY(NUMBER NOT NULL)`. + SnowflakeParenthesis(Box, bool), } /// Represents different types of geometric shapes which are commonly used in diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 32e0bf927..85a8bac7f 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1081,6 +1081,16 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if typed `ARRAY(element_type [NOT NULL])` is supported. + fn supports_parenthesized_array_type_with_element_nullability(&self) -> bool { + false + } + + /// Returns true if `MAP(key_type, value_type [NOT NULL])` is supported. + fn supports_parenthesized_map_type_with_value_nullability(&self) -> bool { + false + } + /// Returns true if this dialect supports structured `OBJECT` types. /// /// Example: diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 9dd51c483..754bbee64 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -248,6 +248,14 @@ impl Dialect for SnowflakeDialect { true } + fn supports_parenthesized_array_type_with_element_nullability(&self) -> bool { + true + } + + fn supports_parenthesized_map_type_with_value_nullability(&self) -> bool { + true + } + /// See [doc](https://docs.snowflake.com/en/sql-reference/data-types-structured#structured-object-types) fn supports_structured_object_type(&self) -> bool { true diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1d5261440..20216d886 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12675,7 +12675,20 @@ 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 dialect_of!(self is ClickHouseDialect | SnowflakeDialect) + if self + .dialect + .supports_parenthesized_array_type_with_element_nullability() + && self.peek_token_ref().token == Token::LParen + { + self.expect_token(&Token::LParen)?; + let element_type = self.parse_data_type()?; + let not_null = self.parse_keywords(&[Keyword::NOT, Keyword::NULL]); + self.expect_token(&Token::RParen)?; + Ok(DataType::Array(ArrayElemTypeDef::SnowflakeParenthesis( + Box::new(element_type), + not_null, + ))) + } else if dialect_of!(self is ClickHouseDialect) && self.peek_token_ref().token == Token::LParen { Ok(self.parse_sub_type(|internal_type| { @@ -12735,6 +12748,23 @@ impl<'a> Parser<'a> { Box::new(value_data_type), )) } + Keyword::MAP + if self + .dialect + .supports_parenthesized_map_type_with_value_nullability() => + { + self.expect_token(&Token::LParen)?; + let key_data_type = self.parse_data_type()?; + self.expect_token(&Token::Comma)?; + let value_data_type = self.parse_data_type()?; + let value_not_null = self.parse_keywords(&[Keyword::NOT, Keyword::NULL]); + self.expect_token(&Token::RParen)?; + Ok(DataType::SnowflakeMap( + Box::new(key_data_type), + Box::new(value_data_type), + value_not_null, + )) + } Keyword::MAP if dialect_is!(dialect is ClickHouseDialect | GenericDialect) => { self.prev_token(); let (key_data_type, value_data_type) = self.parse_click_house_map_def()?; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 12e5792a0..b3cc8b633 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1329,25 +1329,22 @@ fn parse_array() { ); let sql = "SELECT CAST(a AS ARRAY(VARCHAR)) FROM customer"; - let select = snowflake() - .verified_only_select_with_canonical(sql, "SELECT CAST(a AS Array(VARCHAR)) FROM customer"); + let select = snowflake().verified_only_select(sql); assert_eq!( &Expr::Cast { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("a"))), - data_type: DataType::Array(ArrayElemTypeDef::Parenthesis(Box::new(DataType::Varchar( - None - ),))), + data_type: DataType::Array(ArrayElemTypeDef::SnowflakeParenthesis( + Box::new(DataType::Varchar(None)), + false, + )), array: false, format: None, }, expr_from_projection(only(&select.projection)) ); - snowflake().one_statement_parses_to( - "CREATE TABLE t (a ARRAY(NUMBER))", - "CREATE TABLE t (a Array(NUMBER))", - ); + snowflake().verified_stmt("CREATE TABLE t (a ARRAY(NUMBER))"); } #[test] @@ -5533,9 +5530,8 @@ fn test_external_volume() { #[test] fn test_structured_object_type() { - snowflake().one_statement_parses_to( + snowflake().verified_stmt( "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( @@ -5756,3 +5752,41 @@ fn test_sequence_lifecycle() { ); } } + +#[test] +fn test_nested_structured_array_map_nullability() { + let sql = "SELECT payload::OBJECT(items ARRAY(NUMBER NOT NULL), meta MAP(VARCHAR, OBJECT(k NUMBER) NOT NULL)) FROM t"; + let select = snowflake().verified_only_select(sql); + let Expr::Cast { data_type, .. } = expr_from_projection(only(&select.projection)) else { + unreachable!(); + }; + let DataType::Object(fields) = data_type else { + unreachable!(); + }; + assert!(matches!( + &fields[0].data_type, + DataType::Array(ArrayElemTypeDef::SnowflakeParenthesis(_, true)) + )); + let DataType::SnowflakeMap(_, value, true) = &fields[1].data_type else { + unreachable!(); + }; + assert!(matches!(**value, DataType::Object(_))); + + snowflake().verified_stmt("SELECT payload::ARRAY(NUMBER NOT NULL) FROM t"); + snowflake().verified_stmt("SELECT payload::MAP(VARCHAR, NUMBER NOT NULL) FROM t"); + snowflake().verified_stmt("SELECT payload::MAP(VARCHAR, NUMBER) FROM t"); + snowflake().verified_stmt( + "SELECT payload::ARRAY(MAP(VARCHAR, ARRAY(NUMBER NOT NULL) NOT NULL) NOT NULL) FROM t", + ); + + for invalid in [ + "SELECT payload::ARRAY(NUMBER NULL) FROM t", + "SELECT payload::MAP(VARCHAR NOT NULL, NUMBER) FROM t", + "SELECT payload::MAP(VARCHAR, NUMBER NOT NULL NOT NULL) FROM t", + ] { + assert!( + snowflake().parse_sql_statements(invalid).is_err(), + "{invalid}" + ); + } +}