From 7708a40d9dfc0f41efd27a16ab92936f7b0ecb5e Mon Sep 17 00:00:00 2001 From: funcpp Date: Tue, 31 Mar 2026 13:04:08 +0900 Subject: [PATCH] Support multi-column aliases in SELECT items for Databricks Spark SQL grammar allows parenthesized identifier lists as SELECT item aliases: namedExpression: expression (AS? (identifier | identifierList))? identifierList: '(' identifier (',' identifier)* ')' This enables syntax like: SELECT stack(2, 'a', 'b', 'c', 'd') AS (col1, col2) --- src/ast/query.rs | 15 +++++++++++++++ src/ast/spans.rs | 3 +++ src/dialect/databricks.rs | 4 ++++ src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 11 +++++++++++ src/parser/mod.rs | 13 +++++++++++++ tests/sqlparser_common.rs | 15 +++++++++++++++ 7 files changed, 65 insertions(+) diff --git a/src/ast/query.rs b/src/ast/query.rs index a52d518b1..49ba86f1f 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -872,6 +872,15 @@ pub enum SelectItem { /// 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, + }, /// An expression, followed by a wildcard expansion. /// e.g. `alias.*`, `STRUCT('foo').*` QualifiedWildcard(SelectItemQualifiedWildcardKind, WildcardAdditionalOptions), @@ -1175,6 +1184,12 @@ impl fmt::Display for SelectItem { 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 d80a3f4d5..90fa2b8b5 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1821,6 +1821,9 @@ impl Spanned for SelectItem { 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 09cac96fa..8df7444ee 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -99,4 +99,8 @@ impl Dialect for DatabricksDialect { fn supports_bang_not_operator(&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 1d5461fec..08f10d543 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -288,4 +288,8 @@ impl Dialect for GenericDialect { fn supports_comma_separated_trim(&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 bbf7d5804..67e9c9a0e 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1670,6 +1670,17 @@ pub trait Dialect: Debug + Any { fn supports_comma_separated_trim(&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 70e8ce28f..0a214f1f2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -18074,6 +18074,19 @@ impl<'a> Parser<'a> { 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 ad1e521f1..cd83ba90a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -18754,3 +18754,18 @@ fn test_wildcard_func_arg() { 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() + ); +}