diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9d986ec80..7f6feba42 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -474,10 +474,23 @@ pub struct Array { /// `true` for `ARRAY[..]`, `false` for `[..]` pub named: bool, + + /// The declared element type of a typed array literal, e.g. `INT64` in + /// `ARRAY[1, 2, 3]` (BigQuery). `None` for an untyped `[..]` / + /// `ARRAY[..]`. + pub element_type: Option, } impl fmt::Display for Array { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(element_type) = &self.element_type { + return write!( + f, + "ARRAY<{}>[{}]", + element_type, + display_comma_separated(&self.elem) + ); + } write!( f, "{}[{}]", diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 41a5fe6d1..695513187 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1727,7 +1727,8 @@ impl Spanned for Array { fn span(&self) -> Span { let Array { elem, - named: _, // bool + named: _, // bool + element_type: _, // DataType, not spanned } = self; union_spans(elem.iter().map(|i| i.span())) diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index fe6e966f5..e1955f673 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -126,6 +126,11 @@ impl Dialect for BigQueryDialect { true } + // See https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#array_type + fn supports_array_typed_literal(&self) -> bool { + true + } + /// See fn supports_select_expr_star(&self) -> bool { true diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6ab6cb15e..7a370279c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -647,6 +647,13 @@ pub trait Dialect: Debug + Any { false } + /// Return true if the dialect supports a typed array literal, where the + /// element type is given in angle brackets before the elements, e.g. + /// `ARRAY[1, 2, 3]` (BigQuery). + fn supports_array_typed_literal(&self) -> bool { + false + } + /// Return true if the dialect supports empty projections in SELECT statements /// /// Example diff --git a/src/parser/mod.rs b/src/parser/mod.rs index efff501b7..84b8ff081 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1616,6 +1616,23 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LBracket)?; Ok(Some(self.parse_array_expr(true)?)) } + // Typed array literal, e.g. `ARRAY[1, 2, 3]` (BigQuery). + Keyword::ARRAY + if self.dialect.supports_array_typed_literal() + && self.peek_token_ref().token == Token::Lt => + { + self.expect_token(&Token::Lt)?; + let (element_type, trailing_bracket) = self.parse_data_type_helper()?; + self.expect_closing_angle_bracket(trailing_bracket)?; + self.expect_token(&Token::LBracket)?; + let elem = self.parse_comma_separated0(Parser::parse_expr, Token::RBracket)?; + self.expect_token(&Token::RBracket)?; + Ok(Some(Expr::Array(Array { + elem, + named: true, + element_type: Some(element_type), + }))) + } Keyword::ARRAY if self.peek_token_ref().token == Token::LParen && !dialect_of!(self is ClickHouseDialect | DatabricksDialect) => @@ -3132,7 +3149,11 @@ impl<'a> Parser<'a> { pub fn parse_array_expr(&mut self, named: bool) -> Result { let exprs = self.parse_comma_separated0(Parser::parse_expr, Token::RBracket)?; self.expect_token(&Token::RBracket)?; - Ok(Expr::Array(Array { elem: exprs, named })) + Ok(Expr::Array(Array { + elem: exprs, + named, + element_type: None, + })) } /// Parse the `ON OVERFLOW` clause for `LISTAGG`. diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 3cf1e6a2c..ba0bd1e5d 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -3127,3 +3127,42 @@ fn parse_bigquery_create_vector_index() { } bigquery().verified_stmt("CREATE VECTOR INDEX emb ON t(embedding)"); } + +#[test] +fn parse_typed_array_literal() { + // Typed array literal `ARRAY[...]`: the element type is carried on the + // `Array` node and round-trips. Scalar, empty, nested struct and nested + // array element types are all supported. + for sql in [ + "SELECT ARRAY[1, 2, 3]", + "SELECT ARRAY['a', 'b']", + "SELECT ARRAY[]", + "SELECT ARRAY>[(1, 'x'), (2, 'y')]", + "SELECT ARRAY>[ARRAY[1], ARRAY[2]]", + ] { + bigquery().verified_stmt(sql); + } + + // The element type is recorded on the AST. + let Statement::Query(query) = bigquery().verified_stmt("SELECT ARRAY[1, 2, 3]") else { + panic!("expected a query"); + }; + let SelectItem::UnnamedExpr(Expr::Array(array)) = + &query.body.as_select().unwrap().projection[0] + else { + panic!("expected an array expression"); + }; + assert!(array.named); + assert_eq!(array.element_type, Some(DataType::Int64)); + + // Untyped arrays keep `element_type` as `None`. + let Statement::Query(query) = bigquery().verified_stmt("SELECT [1, 2, 3]") else { + panic!("expected a query"); + }; + let SelectItem::UnnamedExpr(Expr::Array(array)) = + &query.body.as_select().unwrap().projection[0] + else { + panic!("expected an array expression"); + }; + assert_eq!(array.element_type, None); +} diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index f16a1f8e4..d6ddcf35d 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -121,6 +121,7 @@ fn parse_array_expr() { Expr::value(Value::SingleQuotedString("2".to_string())), ], named: false, + element_type: None, }), expr_from_projection(only(&select.projection)) ) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 45a73fc6f..3f2f17b2c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -13777,6 +13777,7 @@ fn test_map_syntax() { key: Box::new(Expr::Array(Array { elem: vec![number_expr("1"), number_expr("2"), number_expr("3")], named: false, + element_type: None, })), value: Box::new(Expr::value(number("10.0"))), }, @@ -13784,6 +13785,7 @@ fn test_map_syntax() { key: Box::new(Expr::Array(Array { elem: vec![number_expr("4"), number_expr("5"), number_expr("6")], named: false, + element_type: None, })), value: Box::new(Expr::value(number("20.0"))), }, @@ -13847,6 +13849,7 @@ fn test_map_syntax() { value: Box::new(Expr::Array(Array { elem: vec![number_expr("1"), null_expr(), number_expr("3")], named: false, + element_type: None, })), }, MapEntry { @@ -13854,6 +13857,7 @@ fn test_map_syntax() { value: Box::new(Expr::Array(Array { elem: vec![number_expr("4"), null_expr(), number_expr("6")], named: false, + element_type: None, })), }, MapEntry { @@ -13861,6 +13865,7 @@ fn test_map_syntax() { value: Box::new(Expr::Array(Array { elem: vec![number_expr("7"), number_expr("8"), number_expr("9")], named: false, + element_type: None, })), }, ], diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 0fce68a04..47b34fb05 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -435,7 +435,8 @@ fn test_duckdb_struct_literal() { (Value::SingleQuotedString("abc".to_string())).with_empty_span() )), },],)], - named: false + named: false, + element_type: None, }), expr_from_projection(&select.projection[1]) ); @@ -452,7 +453,8 @@ fn test_duckdb_struct_literal() { Ident::from("t"), Ident::from("str_col") ])], - named: false + named: false, + element_type: None, })), }, ],), @@ -691,7 +693,8 @@ fn test_array_index() { Expr::Value((Value::SingleQuotedString("b".to_owned())).with_empty_span()), Expr::Value((Value::SingleQuotedString("c".to_owned())).with_empty_span()) ], - named: false + named: false, + element_type: None, })), access_chain: vec![AccessExpr::Subscript(Subscript::Index { index: Expr::value(number("3")) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 0a6a8f71d..987cabcaa 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2524,6 +2524,7 @@ fn parse_pg_regex_match_ops() { Expr::Value(single_quoted_string("x").with_empty_span()), ], named: true, + element_type: None, })), is_some: false, }), @@ -2565,6 +2566,7 @@ fn parse_pg_like_match_ops() { right: Box::new(Expr::Array(Array { elem: vec![Expr::Value(single_quoted_string("a_c%").with_empty_span())], named: true, + element_type: None, })), }), select.projection[0] @@ -2645,8 +2647,10 @@ fn parse_array_index_expr() { elem: vec![Expr::Array(Array { elem: vec![num[2].clone(), num[3].clone(),], named: true, + element_type: None, })], named: true, + element_type: None, })), data_type: DataType::Array(ArrayElemTypeDef::SquareBracket( Box::new(DataType::Array(ArrayElemTypeDef::SquareBracket( @@ -2675,7 +2679,8 @@ fn parse_array_index_expr() { assert_eq!( &Expr::Array(sqlparser::ast::Array { elem: vec![], - named: true + named: true, + element_type: None, }), expr_from_projection(only(&select.projection)), ); @@ -3657,6 +3662,7 @@ fn test_json() { Expr::Value((Value::SingleQuotedString("b".to_string())).with_empty_span()), ], named: true, + element_type: None, })), }), select.projection[0], @@ -3712,7 +3718,8 @@ fn test_json() { Expr::Value((Value::SingleQuotedString("b".to_string())).with_empty_span()), Expr::Value((Value::SingleQuotedString("c".to_string())).with_empty_span()) ], - named: true + named: true, + element_type: None, })) }, select.selection.unwrap(), @@ -3729,7 +3736,8 @@ fn test_json() { Expr::Value((Value::SingleQuotedString("b".to_string())).with_empty_span()), Expr::Value((Value::SingleQuotedString("c".to_string())).with_empty_span()) ], - named: true + named: true, + element_type: None, })) }, select.selection.unwrap(), @@ -3909,7 +3917,8 @@ fn test_composite_value() { (Value::SingleQuotedString("i".to_string())).with_empty_span() ), ], - named: true + named: true, + element_type: None, } )))], clauses: vec![],