Support multi-column aliases in SELECT items (#2289)
diff --git a/src/ast/query.rs b/src/ast/query.rs
index a52d518..49ba86f 100644
--- a/src/ast/query.rs
+++ b/src/ast/query.rs
@@ -872,6 +872,15 @@
         /// The alias for the expression.
         alias: Ident,
     },
+    /// An expression, followed by `[ AS ] (alias1, alias2, ...)`
+    ///
+    /// [Spark SQL](https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select.html)
+    ExprWithAliases {
+        /// The expression being projected.
+        expr: Expr,
+        /// The list of aliases for the expression.
+        aliases: Vec<Ident>,
+    },
     /// An expression, followed by a wildcard expansion.
     /// e.g. `alias.*`, `STRUCT<STRING>('foo').*`
     QualifiedWildcard(SelectItemQualifiedWildcardKind, WildcardAdditionalOptions),
@@ -1175,6 +1184,12 @@
                 f.write_str(" AS ")?;
                 alias.fmt(f)
             }
+            SelectItem::ExprWithAliases { expr, aliases } => {
+                expr.fmt(f)?;
+                f.write_str(" AS (")?;
+                display_comma_separated(aliases).fmt(f)?;
+                f.write_str(")")
+            }
             SelectItem::QualifiedWildcard(kind, additional_options) => {
                 kind.fmt(f)?;
                 additional_options.fmt(f)
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index 70c12de..95d2e88 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -1823,6 +1823,9 @@
         match self {
             SelectItem::UnnamedExpr(expr) => expr.span(),
             SelectItem::ExprWithAlias { expr, alias } => expr.span().union(&alias.span),
+            SelectItem::ExprWithAliases { expr, aliases } => {
+                union_spans(iter::once(expr.span()).chain(aliases.iter().map(|i| i.span)))
+            }
             SelectItem::QualifiedWildcard(kind, wildcard_additional_options) => union_spans(
                 [kind.span()]
                     .into_iter()
diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs
index c76b464..679d335 100644
--- a/src/dialect/databricks.rs
+++ b/src/dialect/databricks.rs
@@ -104,4 +104,8 @@
     fn supports_cte_without_as(&self) -> bool {
         true
     }
+
+    fn supports_select_item_multi_column_alias(&self) -> bool {
+        true
+    }
 }
diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs
index 8809bb9..c8f5ce6 100644
--- a/src/dialect/generic.rs
+++ b/src/dialect/generic.rs
@@ -296,4 +296,8 @@
     fn supports_cte_without_as(&self) -> bool {
         true
     }
+
+    fn supports_select_item_multi_column_alias(&self) -> bool {
+        true
+    }
 }
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index bb11f3e..1a2b9b1 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -1691,6 +1691,17 @@
     fn supports_cte_without_as(&self) -> bool {
         false
     }
+
+    /// Returns true if the dialect supports parenthesized multi-column
+    /// aliases in SELECT items. For example:
+    /// ```sql
+    /// SELECT stack(2, 'a', 'b') AS (col1, col2)
+    /// ```
+    ///
+    /// [Spark SQL](https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select.html)
+    fn supports_select_item_multi_column_alias(&self) -> bool {
+        false
+    }
 }
 
 /// Operators for which precedence must be defined.
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index a63352e..60dd3e6 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -18092,6 +18092,19 @@
                     self.parse_wildcard_additional_options(wildcard_token)?,
                 ))
             }
+            expr if self.dialect.supports_select_item_multi_column_alias()
+                && self.peek_keyword(Keyword::AS)
+                && self.peek_nth_token(1).token == Token::LParen =>
+            {
+                self.expect_keyword(Keyword::AS)?;
+                self.expect_token(&Token::LParen)?;
+                let aliases = self.parse_comma_separated(|p| p.parse_identifier())?;
+                self.expect_token(&Token::RParen)?;
+                Ok(SelectItem::ExprWithAliases {
+                    expr: maybe_prefixed_expr(expr, prefix),
+                    aliases,
+                })
+            }
             expr => self
                 .maybe_parse_select_item_alias()
                 .map(|alias| match alias {
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index c21202f..f5add3a 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -18756,3 +18756,18 @@
     dialects.verified_expr("HASH(* EXCLUDE (col1))");
     dialects.verified_expr("HASH(* EXCLUDE (col1, col2))");
 }
+
+#[test]
+fn parse_select_item_multi_column_alias() {
+    all_dialects_where(|d| d.supports_select_item_multi_column_alias())
+        .verified_stmt("SELECT stack(2, 'a', 'b', 'c', 'd') AS (col1, col2)");
+
+    all_dialects_where(|d| d.supports_select_item_multi_column_alias())
+        .verified_stmt("SELECT stack(2, 'a', 'b', 'c', 'd') AS (col1, col2) FROM t");
+
+    assert!(
+        all_dialects_where(|d| !d.supports_select_item_multi_column_alias())
+            .parse_sql_statements("SELECT stack(2, 'a', 'b') AS (col1, col2)")
+            .is_err()
+    );
+}