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]

Reply via email to