This is an automated email from the ASF dual-hosted git repository. github-bot pushed a commit to branch gh-readonly-queue/main/pr-2151-6060a11d1f728d1639f761d3bb90e136911934f9 in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
commit aab3b45e931858c8a44a7aaa1b902415965c2d9b Author: Michael Victor Zink <[email protected]> AuthorDate: Thu Jan 22 02:24:10 2026 -0800 MySQL: Support `CAST(... AS ... ARRAY)` syntax (#2151) --- src/ast/mod.rs | 16 +++++++++++++--- src/ast/spans.rs | 3 ++- src/parser/mod.rs | 4 ++++ tests/sqlparser_common.rs | 18 ++++++++++++++++++ tests/sqlparser_databricks.rs | 1 + tests/sqlparser_duckdb.rs | 1 + tests/sqlparser_mysql.rs | 27 +++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 5 +++++ tests/sqlparser_snowflake.rs | 12 +++++++----- 9 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d77186bc..0470d6a8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1033,6 +1033,12 @@ pub enum Expr { expr: Box<Expr>, /// Target data type. data_type: DataType, + /// [MySQL] allows CAST(... AS type ARRAY) in functional index definitions for InnoDB + /// multi-valued indices. It's not really a datatype, and is only allowed in `CAST` in key + /// specifications, so it's a flag here. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/cast-functions.html#function_cast + array: bool, /// Optional CAST(string_expression AS type FORMAT format_string_expression) as used by [BigQuery] /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#formatting_syntax @@ -1879,14 +1885,18 @@ impl fmt::Display for Expr { kind, expr, data_type, + array, format, } => match kind { CastKind::Cast => { + write!(f, "CAST({expr} AS {data_type}")?; + if *array { + write!(f, " ARRAY")?; + } if let Some(format) = format { - write!(f, "CAST({expr} AS {data_type} FORMAT {format})") - } else { - write!(f, "CAST({expr} AS {data_type})") + write!(f, " FORMAT {format}")?; } + write!(f, ")") } CastKind::TryCast => { if let Some(format) = format { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 488c8862..1c5cc473 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1540,6 +1540,7 @@ impl Spanned for Expr { kind: _, expr, data_type: _, + array: _, format: _, } => expr.span(), Expr::AtTimeZone { @@ -2801,7 +2802,7 @@ WHERE id = 1 UPDATE SET target_table.description = source_table.description WHEN MATCHED AND target_table.x != 'X' THEN DELETE - WHEN NOT MATCHED AND 1 THEN INSERT (product, quantity) ROW + WHEN NOT MATCHED AND 1 THEN INSERT (product, quantity) ROW "#; let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap(); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 149365c4..e56beefc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2626,12 +2626,14 @@ impl<'a> Parser<'a> { let expr = self.parse_expr()?; self.expect_keyword_is(Keyword::AS)?; let data_type = self.parse_data_type()?; + let array = self.parse_keyword(Keyword::ARRAY); let format = self.parse_optional_cast_format()?; self.expect_token(&Token::RParen)?; Ok(Expr::Cast { kind, expr: Box::new(expr), data_type, + array, format, }) } @@ -3910,6 +3912,7 @@ impl<'a> Parser<'a> { kind: CastKind::DoubleColon, expr: Box::new(expr), data_type: self.parse_data_type()?, + array: false, format: None, }) } else if Token::ExclamationMark == *tok && self.dialect.supports_factorial_operator() { @@ -4150,6 +4153,7 @@ impl<'a> Parser<'a> { kind: CastKind::DoubleColon, expr: Box::new(expr), data_type: self.parse_data_type()?, + array: false, format: None, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index bbbf0d83..4728d156 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3027,6 +3027,7 @@ fn parse_cast() { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::BigInt(None), + array: false, format: None, }, expr_from_projection(only(&select.projection)) @@ -3039,6 +3040,7 @@ fn parse_cast() { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::TinyInt(None), + array: false, format: None, }, expr_from_projection(only(&select.projection)) @@ -3070,6 +3072,7 @@ fn parse_cast() { length: 50, unit: None, })), + array: false, format: None, }, expr_from_projection(only(&select.projection)) @@ -3082,6 +3085,7 @@ fn parse_cast() { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Clob(None), + array: false, format: None, }, expr_from_projection(only(&select.projection)) @@ -3094,6 +3098,7 @@ fn parse_cast() { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Clob(Some(50)), + array: false, format: None, }, expr_from_projection(only(&select.projection)) @@ -3106,6 +3111,7 @@ fn parse_cast() { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Binary(Some(50)), + array: false, format: None, }, expr_from_projection(only(&select.projection)) @@ -3118,6 +3124,7 @@ fn parse_cast() { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Varbinary(Some(BinaryLength::IntegerLength { length: 50 })), + array: false, format: None, }, expr_from_projection(only(&select.projection)) @@ -3130,6 +3137,7 @@ fn parse_cast() { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Blob(None), + array: false, format: None, }, expr_from_projection(only(&select.projection)) @@ -3142,6 +3150,7 @@ fn parse_cast() { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::Blob(Some(50)), + array: false, format: None, }, expr_from_projection(only(&select.projection)) @@ -3154,6 +3163,7 @@ fn parse_cast() { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("details"))), data_type: DataType::JSONB, + array: false, format: None, }, expr_from_projection(only(&select.projection)) @@ -3169,6 +3179,7 @@ fn parse_try_cast() { kind: CastKind::TryCast, expr: Box::new(Expr::Identifier(Ident::new("id"))), data_type: DataType::BigInt(None), + array: false, format: None, }, expr_from_projection(only(&select.projection)) @@ -6505,6 +6516,7 @@ fn interval_disallow_interval_expr_double_colon() { fractional_seconds_precision: None, })), data_type: DataType::Text, + array: false, format: None, } ) @@ -9220,6 +9232,7 @@ fn parse_double_colon_cast_at_timezone() { .with_empty_span() )), data_type: DataType::Timestamp(None, TimezoneInfo::None), + array: false, format: None }), time_zone: Box::new(Expr::Value( @@ -13352,6 +13365,7 @@ fn test_dictionary_syntax() { (Value::SingleQuotedString("2023-04-01".to_owned())).with_empty_span(), )), data_type: DataType::Timestamp(None, TimezoneInfo::None), + array: false, format: None, }), }, @@ -13363,6 +13377,7 @@ fn test_dictionary_syntax() { (Value::SingleQuotedString("2023-04-05".to_owned())).with_empty_span(), )), data_type: DataType::Timestamp(None, TimezoneInfo::None), + array: false, format: None, }), }, @@ -13606,6 +13621,7 @@ fn test_extract_seconds_ok() { fields: None, precision: None }, + array: false, format: None, }), } @@ -13634,6 +13650,7 @@ fn test_extract_seconds_ok() { fields: None, precision: None, }, + array: false, format: None, }), })], @@ -13691,6 +13708,7 @@ fn test_extract_seconds_single_quote_ok() { fields: None, precision: None }, + array: false, format: None, }), } diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 7f5ec6c3..b088afd7 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -349,6 +349,7 @@ fn data_type_timestamp_ntz() { "created_at".into() )))), data_type: DataType::TimestampNtz(None), + array: false, format: None } ); diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 80a15eb1..bdfe4f50 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -380,6 +380,7 @@ fn test_duckdb_specific_int_types() { Value::Number("123".parse().unwrap(), false).with_empty_span() )), data_type: data_type.clone(), + array: false, format: None, }, expr_from_projection(&select.projection[0]) diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e847d3ed..4a620538 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -874,6 +874,25 @@ fn test_functional_key_part() { )), }), data_type: DataType::Unsigned, + array: false, + format: None, + })), + ); + assert_eq!( + index_column(mysql_and_generic().verified_stmt( + r#"CREATE TABLE t (jsoncol JSON, PRIMARY KEY ((CAST(col ->> '$.fields' AS UNSIGNED ARRAY)) ASC))"# + )), + Expr::Nested(Box::new(Expr::Cast { + kind: CastKind::Cast, + expr: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("col"))), + op: BinaryOperator::LongArrow, + right: Box::new(Expr::Value( + Value::SingleQuotedString("$.fields".to_string()).with_empty_span() + )), + }), + data_type: DataType::Unsigned, + array: true, format: None, })), ); @@ -4096,6 +4115,14 @@ fn parse_cast_integers() { .expect_err("CAST doesn't allow display width"); } +#[test] +fn parse_cast_array() { + mysql().verified_expr("CAST(foo AS SIGNED ARRAY)"); + mysql() + .run_parser_method("CAST(foo AS ARRAY)", |p| p.parse_expr()) + .expect_err("ARRAY alone is not a type"); +} + #[test] fn parse_match_against_with_alias() { let sql = "SELECT tbl.ProjectID FROM surveys.tbl1 AS tbl WHERE MATCH (tbl.ReferenceID) AGAINST ('AAA' IN BOOLEAN MODE)"; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 325e3939..a68ebaa7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1706,6 +1706,7 @@ fn parse_execute() { (Value::Number("1337".parse().unwrap(), false)).with_empty_span() )), data_type: DataType::SmallInt(None), + array: false, format: None }, alias: None @@ -1717,6 +1718,7 @@ fn parse_execute() { (Value::Number("7331".parse().unwrap(), false)).with_empty_span() )), data_type: DataType::SmallInt(None), + array: false, format: None }, alias: None @@ -2343,6 +2345,7 @@ fn parse_array_index_expr() { ))), None )), + array: false, format: None, }))), access_chain: vec![ @@ -5570,6 +5573,7 @@ fn parse_at_time_zone() { Value::SingleQuotedString("America/Los_Angeles".to_owned()).with_empty_span(), )), data_type: DataType::Text, + array: false, format: None, }), }), @@ -6386,6 +6390,7 @@ fn arrow_cast_precedence() { (Value::SingleQuotedString("bar".to_string())).with_empty_span() )), data_type: DataType::Text, + array: false, format: None, }), } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 5889b2bd..aff02a37 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1101,8 +1101,8 @@ fn parse_create_dynamic_table() { " EXTERNAL_VOLUME='my_external_volume'", " CATALOG='SNOWFLAKE'", " BASE_LOCATION='my_iceberg_table'", - " TARGET_LAG='20 minutes'", - " WAREHOUSE=mywh", + " TARGET_LAG='20 minutes'", + " WAREHOUSE=mywh", " AS SELECT product_id, product_name FROM staging_table" )); @@ -1250,6 +1250,7 @@ fn parse_array() { kind: CastKind::Cast, expr: Box::new(Expr::Identifier(Ident::new("a"))), data_type: DataType::Array(ArrayElemTypeDef::None), + array: false, format: None, }, expr_from_projection(only(&select.projection)) @@ -1349,8 +1350,6 @@ fn parse_semi_structured_data_traversal() { Expr::JsonAccess { value: Box::new(Expr::Cast { kind: CastKind::DoubleColon, - data_type: DataType::Array(ArrayElemTypeDef::None), - format: None, expr: Box::new(Expr::JsonAccess { value: Box::new(Expr::Identifier(Ident::new("a"))), path: JsonPath { @@ -1359,7 +1358,10 @@ fn parse_semi_structured_data_traversal() { quoted: false }] } - }) + }), + data_type: DataType::Array(ArrayElemTypeDef::None), + array: false, + format: None, }), path: JsonPath { path: vec![JsonPathElem::Bracket { --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
