Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/ast/data_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,8 @@ pub enum DataType {
///
/// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/map
Map(Box<DataType>, Box<DataType>),
/// Snowflake structured MAP with optional non-null values.
SnowflakeMap(Box<DataType>, Box<DataType>, bool),
/// Tuple, see [ClickHouse].
///
/// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/tuple
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -1153,6 +1169,8 @@ pub enum ArrayElemTypeDef {
SquareBracket(Box<DataType>, Option<u64>),
/// Parenthesis style, e.g. `Array(Int64)`.
Parenthesis(Box<DataType>),
/// Snowflake parenthesis style, e.g. `ARRAY(NUMBER NOT NULL)`.
SnowflakeParenthesis(Box<DataType>, bool),
}

/// Represents different types of geometric shapes which are commonly used in
Expand Down
10 changes: 10 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down Expand Up @@ -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()?;
Expand Down
56 changes: 45 additions & 11 deletions tests/sqlparser_snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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}"
);
}
}
Loading