From b6359c10ccfaccfdf2f7de8000eb621c482f9396 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Sun, 8 Feb 2026 10:03:52 +0100 Subject: [PATCH 1/2] [PIVOT] Optional AS keyword for value aliases --- src/ast/query.rs | 7 +++++-- src/parser/mod.rs | 13 ++++++++++++- tests/sqlparser_common.rs | 6 ++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index b8f605be5..6d95216df 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1589,6 +1589,7 @@ pub enum TableFactor { /// /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#pivot_operator) /// [Snowflake](https://docs.snowflake.com/en/sql-reference/constructs/pivot) + /// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/SELECT.html#GUID-CFA006CA-6FF1-4972-821E-6996142A51C6__GUID-68257B27-1C4C-4C47-8140-5C60E0E65D35) Pivot { /// The input table to pivot. table: Box, @@ -1610,8 +1611,10 @@ pub enum TableFactor { /// table UNPIVOT [ { INCLUDE | EXCLUDE } NULLS ] (value FOR name IN (column1, [ column2, ... ])) [ alias ] /// ``` /// - /// See . - /// See . + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/constructs/unpivot) + /// [Databricks](https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-syntax-qry-select-unpivot) + /// [BigQuery](https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#unpivot_operator) + /// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/SELECT.html#GUID-CFA006CA-6FF1-4972-821E-6996142A51C6__GUID-9B4E0389-413C-4014-94A1-0A0571BDF7E1) Unpivot { /// The input table to unpivot. table: Box, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0b152f2be..2097548f3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -16172,6 +16172,15 @@ impl<'a> Parser<'a> { Ok(ExprWithAlias { expr, alias }) } + /// Parse an expression followed by an optional alias; Unlike + /// [Self::parse_expr_with_alias] the "AS" keyword between the expression + /// and the alias is optional. + fn parse_expr_with_alias_optional_as_keyword(&mut self) -> Result { + let expr = self.parse_expr()?; + let alias = self.parse_identifier_optional_alias()?; + Ok(ExprWithAlias { expr, alias }) + } + /// Parse a PIVOT table factor (ClickHouse/Oracle style pivot), returning a TableFactor. pub fn parse_pivot_table_factor( &mut self, @@ -16200,7 +16209,9 @@ impl<'a> Parser<'a> { } else if self.peek_sub_query() { PivotValueSource::Subquery(self.parse_query()?) } else { - PivotValueSource::List(self.parse_comma_separated(Self::parse_expr_with_alias)?) + PivotValueSource::List( + self.parse_comma_separated(Self::parse_expr_with_alias_optional_as_keyword)?, + ) }; self.expect_token(&Token::RParen)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3c32e627c..c19fbc6f6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11357,6 +11357,12 @@ fn parse_pivot_table() { verified_stmt(multiple_value_columns_sql).to_string(), multiple_value_columns_sql ); + + // assert optional "AS" keyword for aliases for pivot values + one_statement_parses_to( + "SELECT * FROM t PIVOT(SUM(1) FOR a.abc IN (1 x, 'two' y, three z))", + "SELECT * FROM t PIVOT(SUM(1) FOR a.abc IN (1 AS x, 'two' AS y, three AS z))", + ); } #[test] From e2c569d716299bd2d9961494599eadc61784874c Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Sun, 8 Feb 2026 17:57:03 +0100 Subject: [PATCH 2/2] [PIVOT] Optional AS keyword for aggregation function aliases --- src/parser/mod.rs | 36 ++++++++++++++++++++---------------- tests/sqlparser_common.rs | 6 ++++++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2097548f3..cd4c16df6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13543,7 +13543,7 @@ impl<'a> Parser<'a> { Keyword::PIVOT => { self.expect_token(&Token::LParen)?; let aggregate_functions = - self.parse_comma_separated(Self::parse_aliased_function_call)?; + self.parse_comma_separated(Self::parse_pivot_aggregate_function)?; self.expect_keyword_is(Keyword::FOR)?; let value_column = self.parse_period_separated(|p| p.parse_identifier())?; self.expect_keyword_is(Keyword::IN)?; @@ -16125,20 +16125,6 @@ impl<'a> Parser<'a> { }) } - fn parse_aliased_function_call(&mut self) -> Result { - let function_name = match self.next_token().token { - Token::Word(w) => Ok(w.value), - _ => self.expected("a function identifier", self.peek_token()), - }?; - let expr = self.parse_function(ObjectName::from(vec![Ident::new(function_name)]))?; - let alias = if self.parse_keyword(Keyword::AS) { - Some(self.parse_identifier()?) - } else { - None - }; - - Ok(ExprWithAlias { expr, alias }) - } /// Parses an expression with an optional alias /// /// Examples: @@ -16181,13 +16167,31 @@ impl<'a> Parser<'a> { Ok(ExprWithAlias { expr, alias }) } + /// Parses a plain function call with an optional alias for the `PIVOT` clause + fn parse_pivot_aggregate_function(&mut self) -> Result { + let function_name = match self.next_token().token { + Token::Word(w) => Ok(w.value), + _ => self.expected("a function identifier", self.peek_token()), + }?; + let expr = self.parse_function(ObjectName::from(vec![Ident::new(function_name)]))?; + let alias = { + fn validator(explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { + // ~ for a PIVOT aggregate function the alias must not be a "FOR"; in any dialect + kw != &Keyword::FOR && parser.dialect.is_select_item_alias(explicit, kw, parser) + } + self.parse_optional_alias_inner(None, validator)? + }; + Ok(ExprWithAlias { expr, alias }) + } + /// Parse a PIVOT table factor (ClickHouse/Oracle style pivot), returning a TableFactor. pub fn parse_pivot_table_factor( &mut self, table: TableFactor, ) -> Result { self.expect_token(&Token::LParen)?; - let aggregate_functions = self.parse_comma_separated(Self::parse_aliased_function_call)?; + let aggregate_functions = + self.parse_comma_separated(Self::parse_pivot_aggregate_function)?; self.expect_keyword_is(Keyword::FOR)?; let value_column = if self.peek_token_ref().token == Token::LParen { self.parse_parenthesized_column_list_inner(Mandatory, false, |p| { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c19fbc6f6..e715e3f04 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11363,6 +11363,12 @@ fn parse_pivot_table() { "SELECT * FROM t PIVOT(SUM(1) FOR a.abc IN (1 x, 'two' y, three z))", "SELECT * FROM t PIVOT(SUM(1) FOR a.abc IN (1 AS x, 'two' AS y, three AS z))", ); + + // assert optional "AS" keyword for aliases for pivot aggregate function + one_statement_parses_to( + "SELECT * FROM t PIVOT(SUM(1) x, COUNT(42) y FOR a.abc IN (1))", + "SELECT * FROM t PIVOT(SUM(1) AS x, COUNT(42) AS y FOR a.abc IN (1))", + ); } #[test]