From fad46e3b01edcee1648e2c472ed9315692f35e4f Mon Sep 17 00:00:00 2001 From: Chongchen Chen Date: Wed, 23 Jul 2025 19:42:35 +0800 Subject: [PATCH 1/5] feat: support multi value column unpivot --- src/ast/mod.rs | 12 +++---- src/ast/query.rs | 56 +++++++++++++++++++++++++++--- src/ast/spans.rs | 10 ++++-- src/parser/mod.rs | 36 +++++++++++++++++-- tests/sqlparser_common.rs | 73 +++++++++++++++++++++++++++++++++++---- 5 files changed, 167 insertions(+), 20 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1798223f3..f056d15af 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -76,12 +76,12 @@ pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, ExprWithAliasAndOrderBy, Fetch, ForClause, ForJson, ForXml, FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, - IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint, - JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, - JsonTableNestedColumn, LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, - MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, - OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, - PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, + IdentsWithAlias, IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, + JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, + JsonTableNamedColumn, JsonTableNestedColumn, LateralView, LimitClause, LockClause, LockType, + MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, + NonBlock, Offset, OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, + OrderByOptions, PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor, diff --git a/src/ast/query.rs b/src/ast/query.rs index 7ffb64d9b..5b1b3cd28 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -745,6 +745,47 @@ impl fmt::Display for IdentWithAlias { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct IdentsWithAlias { + pub idents: Vec, + pub alias: Option, +} + +impl IdentsWithAlias { + pub fn new(idents: Vec, alias: Option) -> Self { + Self { idents, alias } + } +} + +impl fmt::Display for IdentsWithAlias { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.idents.len() { + 0 => Ok(()), + 1 => { + if let Some(alias) = &self.alias { + write!(f, "{} AS {}", self.idents[0], alias) + } else { + write!(f, "{}", self.idents[0]) + } + } + _ => { + if let Some(alias) = &self.alias { + write!( + f, + "({}) AS {}", + display_comma_separated(&self.idents), + alias + ) + } else { + write!(f, "({})", display_comma_separated(&self.idents)) + } + } + } + } +} + /// Additional options for wildcards, e.g. Snowflake `EXCLUDE`/`RENAME` and Bigquery `EXCEPT`. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1351,9 +1392,9 @@ pub enum TableFactor { /// See . Unpivot { table: Box, - value: Ident, + value: Vec, name: Ident, - columns: Vec, + columns: Vec, null_inclusion: Option, alias: Option, }, @@ -2035,10 +2076,17 @@ impl fmt::Display for TableFactor { if let Some(null_inclusion) = null_inclusion { write!(f, " {null_inclusion} ")?; } + write!(f, "(")?; + if value.len() == 1 { + // single value column unpivot + write!(f, "{}", value[0])?; + } else { + // multi value column unpivot + write!(f, "({})", display_comma_separated(value))?; + } write!( f, - "({} FOR {} IN ({}))", - value, + " FOR {} IN ({}))", name, display_comma_separated(columns) )?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 3e82905e1..ed0de0646 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1985,9 +1985,15 @@ impl Spanned for TableFactor { alias, } => union_spans( core::iter::once(table.span()) - .chain(core::iter::once(value.span)) + .chain(value.iter().map(|i| i.span)) .chain(core::iter::once(name.span)) - .chain(columns.iter().map(|i| i.span)) + .chain(columns.iter().flat_map(|ilist| { + ilist + .idents + .iter() + .map(|i| i.span) + .chain(ilist.alias.as_ref().map(|a| a.span)) + })) .chain(alias.as_ref().map(|alias| alias.span())), ), TableFactor::MatchRecognize { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8d5a55da0..d763b6e12 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10810,6 +10810,29 @@ impl<'a> Parser<'a> { } } + pub fn parse_identifiers_with_alias(&mut self) -> Result { + let idents = match self.peek_token_ref().token { + Token::LParen => self.parse_parenthesized_column_list(Mandatory, false)?, + _ => vec![self.parse_identifier()?], + }; + let alias = if self.parse_keyword(Keyword::AS) { + Some(self.parse_identifier()?) + } else { + None + }; + Ok(IdentsWithAlias { idents, alias }) + } + + pub fn parse_parenthesized_columns_with_alias_list( + &mut self, + optional: IsOptional, + allow_empty: bool, + ) -> Result, ParserError> { + self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| { + p.parse_identifiers_with_alias() + }) + } + /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers. /// For example: `(col1, "col 2", ...)` pub fn parse_parenthesized_column_list( @@ -13882,11 +13905,20 @@ impl<'a> Parser<'a> { None }; self.expect_token(&Token::LParen)?; - let value = self.parse_identifier()?; + let value = match self.peek_token_ref().token { + Token::LParen => { + // multi value column unpivot + self.parse_parenthesized_column_list(Mandatory, false)? + } + _ => { + // single value column unpivot + vec![self.parse_identifier()?] + } + }; self.expect_keyword_is(Keyword::FOR)?; let name = self.parse_identifier()?; self.expect_keyword_is(Keyword::IN)?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_columns_with_alias_list(Mandatory, false)?; self.expect_token(&Token::RParen)?; let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Unpivot { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5d8284a46..d7bbd3ac7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10947,11 +10947,11 @@ fn parse_unpivot_table() { index_hints: vec![], }), null_inclusion: None, - value: Ident { + value: vec![Ident { value: "quantity".to_string(), quote_style: None, span: Span::empty(), - }, + }], name: Ident { value: "quarter".to_string(), @@ -10960,7 +10960,7 @@ fn parse_unpivot_table() { }, columns: ["Q1", "Q2", "Q3", "Q4"] .into_iter() - .map(Ident::new) + .map(|col| IdentsWithAlias::new(vec![Ident::new(col)], None)) .collect(), alias: Some(TableAlias { name: Ident::new("u"), @@ -11022,6 +11022,67 @@ fn parse_unpivot_table() { verified_stmt(sql_unpivot_include_nulls).to_string(), sql_unpivot_include_nulls ); + + let sql_unpivot_with_alias = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT INCLUDE NULLS (quantity FOR quarter IN (Q1 AS Quater1, Q2 AS Quater2, Q3 AS Quater3, Q4 AS Quater4)) AS u (product, quarter, quantity)" + ); + + if let Unpivot { value, columns, .. } = + &verified_only_select(sql_unpivot_with_alias).from[0].relation + { + assert_eq!( + *columns, + vec![ + IdentsWithAlias::new(vec![Ident::new("Q1")], Some(Ident::new("Quater1"))), + IdentsWithAlias::new(vec![Ident::new("Q2")], Some(Ident::new("Quater2"))), + IdentsWithAlias::new(vec![Ident::new("Q3")], Some(Ident::new("Quater3"))), + IdentsWithAlias::new(vec![Ident::new("Q4")], Some(Ident::new("Quater4"))), + ] + ); + assert_eq!(*value, vec![Ident::new("quantity")]); + } + + assert_eq!( + verified_stmt(sql_unpivot_with_alias).to_string(), + sql_unpivot_with_alias + ); + + let sql_unpivot_with_alias = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT INCLUDE NULLS ((first_quarter, second_quarter) ", + "FOR half_of_the_year IN (", + "(Q1, Q2) AS H1, ", + "(Q3, Q4) AS H2", + "))" + ); + + if let Unpivot { value, columns, .. } = + &verified_only_select(sql_unpivot_with_alias).from[0].relation + { + assert_eq!( + *columns, + vec![ + IdentsWithAlias::new( + vec![Ident::new("Q1"), Ident::new("Q2")], + Some(Ident::new("H1")) + ), + IdentsWithAlias::new( + vec![Ident::new("Q3"), Ident::new("Q4")], + Some(Ident::new("H2")) + ), + ] + ); + assert_eq!( + *value, + vec![Ident::new("first_quarter"), Ident::new("second_quarter")] + ); + } + + assert_eq!( + verified_stmt(sql_unpivot_with_alias).to_string(), + sql_unpivot_with_alias + ); } #[test] @@ -11119,11 +11180,11 @@ fn parse_pivot_unpivot_table() { index_hints: vec![], }), null_inclusion: None, - value: Ident { + value: vec![Ident { value: "population".to_string(), quote_style: None, span: Span::empty() - }, + }], name: Ident { value: "year".to_string(), @@ -11132,7 +11193,7 @@ fn parse_pivot_unpivot_table() { }, columns: ["population_2000", "population_2010"] .into_iter() - .map(Ident::new) + .map(|col| IdentsWithAlias::new(vec![Ident::new(col)], None)) .collect(), alias: Some(TableAlias { name: Ident::new("u"), From 68d5cbd6d5740cc69ed40fc077d85dd1e7171579 Mon Sep 17 00:00:00 2001 From: Chongchen Chen Date: Tue, 29 Jul 2025 21:42:20 +0800 Subject: [PATCH 2/5] update --- src/ast/mod.rs | 12 ++--- src/ast/query.rs | 57 ++------------------- src/ast/spans.rs | 10 +--- src/parser/mod.rs | 26 ++++------ tests/sqlparser_common.rs | 102 +++++++++++++++++++++----------------- 5 files changed, 79 insertions(+), 128 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f056d15af..1798223f3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -76,12 +76,12 @@ pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, ExprWithAliasAndOrderBy, Fetch, ForClause, ForJson, ForXml, FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, - IdentsWithAlias, IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, - JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, - JsonTableNamedColumn, JsonTableNestedColumn, LateralView, LimitClause, LockClause, LockType, - MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, - NonBlock, Offset, OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, - OrderByOptions, PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, + IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint, + JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, + JsonTableNestedColumn, LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, + MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, + OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, + PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor, diff --git a/src/ast/query.rs b/src/ast/query.rs index 5b1b3cd28..ea641deba 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -745,47 +745,6 @@ impl fmt::Display for IdentWithAlias { } } -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct IdentsWithAlias { - pub idents: Vec, - pub alias: Option, -} - -impl IdentsWithAlias { - pub fn new(idents: Vec, alias: Option) -> Self { - Self { idents, alias } - } -} - -impl fmt::Display for IdentsWithAlias { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.idents.len() { - 0 => Ok(()), - 1 => { - if let Some(alias) = &self.alias { - write!(f, "{} AS {}", self.idents[0], alias) - } else { - write!(f, "{}", self.idents[0]) - } - } - _ => { - if let Some(alias) = &self.alias { - write!( - f, - "({}) AS {}", - display_comma_separated(&self.idents), - alias - ) - } else { - write!(f, "({})", display_comma_separated(&self.idents)) - } - } - } - } -} - /// Additional options for wildcards, e.g. Snowflake `EXCLUDE`/`RENAME` and Bigquery `EXCEPT`. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1390,11 +1349,12 @@ pub enum TableFactor { /// ``` /// /// See . + /// See . Unpivot { table: Box, - value: Vec, + value: Expr, name: Ident, - columns: Vec, + columns: Vec, null_inclusion: Option, alias: Option, }, @@ -2076,17 +2036,10 @@ impl fmt::Display for TableFactor { if let Some(null_inclusion) = null_inclusion { write!(f, " {null_inclusion} ")?; } - write!(f, "(")?; - if value.len() == 1 { - // single value column unpivot - write!(f, "{}", value[0])?; - } else { - // multi value column unpivot - write!(f, "({})", display_comma_separated(value))?; - } write!( f, - " FOR {} IN ({}))", + "({} FOR {} IN ({}))", + value, name, display_comma_separated(columns) )?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index ed0de0646..414d2abc7 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1985,15 +1985,9 @@ impl Spanned for TableFactor { alias, } => union_spans( core::iter::once(table.span()) - .chain(value.iter().map(|i| i.span)) + .chain(core::iter::once(value.span())) .chain(core::iter::once(name.span)) - .chain(columns.iter().flat_map(|ilist| { - ilist - .idents - .iter() - .map(|i| i.span) - .chain(ilist.alias.as_ref().map(|a| a.span)) - })) + .chain(columns.iter().map(|ilist| ilist.span())) .chain(alias.as_ref().map(|alias| alias.span())), ), TableFactor::MatchRecognize { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d763b6e12..bc614a61e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10810,26 +10810,13 @@ impl<'a> Parser<'a> { } } - pub fn parse_identifiers_with_alias(&mut self) -> Result { - let idents = match self.peek_token_ref().token { - Token::LParen => self.parse_parenthesized_column_list(Mandatory, false)?, - _ => vec![self.parse_identifier()?], - }; - let alias = if self.parse_keyword(Keyword::AS) { - Some(self.parse_identifier()?) - } else { - None - }; - Ok(IdentsWithAlias { idents, alias }) - } - pub fn parse_parenthesized_columns_with_alias_list( &mut self, optional: IsOptional, allow_empty: bool, - ) -> Result, ParserError> { + ) -> Result, ParserError> { self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| { - p.parse_identifiers_with_alias() + p.parse_expr_with_alias() }) } @@ -13908,11 +13895,16 @@ impl<'a> Parser<'a> { let value = match self.peek_token_ref().token { Token::LParen => { // multi value column unpivot - self.parse_parenthesized_column_list(Mandatory, false)? + Expr::Tuple( + self.parse_parenthesized_column_list(Mandatory, false)? + .into_iter() + .map(|col| Expr::Identifier(col)) + .collect(), + ) } _ => { // single value column unpivot - vec![self.parse_identifier()?] + Expr::Identifier(self.parse_identifier()?) } }; self.expect_keyword_is(Keyword::FOR)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d7bbd3ac7..71b3d88e8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10947,20 +10947,14 @@ fn parse_unpivot_table() { index_hints: vec![], }), null_inclusion: None, - value: vec![Ident { - value: "quantity".to_string(), - quote_style: None, - span: Span::empty(), - }], - - name: Ident { - value: "quarter".to_string(), - quote_style: None, - span: Span::empty(), - }, + value: Expr::Identifier(Ident::new("quantity")), + name: Ident::new("quarter"), columns: ["Q1", "Q2", "Q3", "Q4"] .into_iter() - .map(|col| IdentsWithAlias::new(vec![Ident::new(col)], None)) + .map(|col| ExprWithAlias { + expr: Expr::Identifier(Ident::new(col)), + alias: None, + }) .collect(), alias: Some(TableAlias { name: Ident::new("u"), @@ -11024,9 +11018,12 @@ fn parse_unpivot_table() { ); let sql_unpivot_with_alias = concat!( - "SELECT * FROM sales AS s ", - "UNPIVOT INCLUDE NULLS (quantity FOR quarter IN (Q1 AS Quater1, Q2 AS Quater2, Q3 AS Quater3, Q4 AS Quater4)) AS u (product, quarter, quantity)" - ); + "SELECT * FROM sales AS s ", + "UNPIVOT INCLUDE NULLS ", + "(quantity FOR quarter IN ", + "(Q1 AS Quater1, Q2 AS Quater2, Q3 AS Quater3, Q4 AS Quater4)) ", + "AS u (product, quarter, quantity)" + ); if let Unpivot { value, columns, .. } = &verified_only_select(sql_unpivot_with_alias).from[0].relation @@ -11034,13 +11031,25 @@ fn parse_unpivot_table() { assert_eq!( *columns, vec![ - IdentsWithAlias::new(vec![Ident::new("Q1")], Some(Ident::new("Quater1"))), - IdentsWithAlias::new(vec![Ident::new("Q2")], Some(Ident::new("Quater2"))), - IdentsWithAlias::new(vec![Ident::new("Q3")], Some(Ident::new("Quater3"))), - IdentsWithAlias::new(vec![Ident::new("Q4")], Some(Ident::new("Quater4"))), + ExprWithAlias { + expr: Expr::Identifier(Ident::new("Q1")), + alias: Some(Ident::new("Quater1")), + }, + ExprWithAlias { + expr: Expr::Identifier(Ident::new("Q2")), + alias: Some(Ident::new("Quater2")), + }, + ExprWithAlias { + expr: Expr::Identifier(Ident::new("Q3")), + alias: Some(Ident::new("Quater3")), + }, + ExprWithAlias { + expr: Expr::Identifier(Ident::new("Q4")), + alias: Some(Ident::new("Quater4")), + }, ] ); - assert_eq!(*value, vec![Ident::new("quantity")]); + assert_eq!(*value, Expr::Identifier(Ident::new("quantity"))); } assert_eq!( @@ -11048,7 +11057,7 @@ fn parse_unpivot_table() { sql_unpivot_with_alias ); - let sql_unpivot_with_alias = concat!( + let sql_unpivot_with_alias_and_multi_value = concat!( "SELECT * FROM sales AS s ", "UNPIVOT INCLUDE NULLS ((first_quarter, second_quarter) ", "FOR half_of_the_year IN (", @@ -11058,30 +11067,39 @@ fn parse_unpivot_table() { ); if let Unpivot { value, columns, .. } = - &verified_only_select(sql_unpivot_with_alias).from[0].relation + &verified_only_select(sql_unpivot_with_alias_and_multi_value).from[0].relation { assert_eq!( *columns, vec![ - IdentsWithAlias::new( - vec![Ident::new("Q1"), Ident::new("Q2")], - Some(Ident::new("H1")) - ), - IdentsWithAlias::new( - vec![Ident::new("Q3"), Ident::new("Q4")], - Some(Ident::new("H2")) - ), + ExprWithAlias { + expr: Expr::Tuple(vec![ + Expr::Identifier(Ident::new("Q1")), + Expr::Identifier(Ident::new("Q2")), + ]), + alias: Some(Ident::new("H1")), + }, + ExprWithAlias { + expr: Expr::Tuple(vec![ + Expr::Identifier(Ident::new("Q3")), + Expr::Identifier(Ident::new("Q4")), + ]), + alias: Some(Ident::new("H2")), + }, ] ); assert_eq!( *value, - vec![Ident::new("first_quarter"), Ident::new("second_quarter")] + Expr::Tuple(vec![ + Expr::Identifier(Ident::new("first_quarter")), + Expr::Identifier(Ident::new("second_quarter")), + ]) ); } assert_eq!( - verified_stmt(sql_unpivot_with_alias).to_string(), - sql_unpivot_with_alias + verified_stmt(sql_unpivot_with_alias_and_multi_value).to_string(), + sql_unpivot_with_alias_and_multi_value ); } @@ -11180,20 +11198,14 @@ fn parse_pivot_unpivot_table() { index_hints: vec![], }), null_inclusion: None, - value: vec![Ident { - value: "population".to_string(), - quote_style: None, - span: Span::empty() - }], - - name: Ident { - value: "year".to_string(), - quote_style: None, - span: Span::empty() - }, + value: Expr::Identifier(Ident::new("population")), + name: Ident::new("year"), columns: ["population_2000", "population_2010"] .into_iter() - .map(|col| IdentsWithAlias::new(vec![Ident::new(col)], None)) + .map(|col| ExprWithAlias { + expr: Expr::Identifier(Ident::new(col)), + alias: None, + }) .collect(), alias: Some(TableAlias { name: Ident::new("u"), From 9ccbd73706e8e22c1c33ec057a528aa8e926ce5f Mon Sep 17 00:00:00 2001 From: Chongchen Chen Date: Tue, 29 Jul 2025 21:44:21 +0800 Subject: [PATCH 3/5] fmt --- src/parser/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bc614a61e..3ac22e3f7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13898,7 +13898,7 @@ impl<'a> Parser<'a> { Expr::Tuple( self.parse_parenthesized_column_list(Mandatory, false)? .into_iter() - .map(|col| Expr::Identifier(col)) + .map(Expr::Identifier) .collect(), ) } From 7ef9db9f1543669afea6a86b7f369c9e9485889e Mon Sep 17 00:00:00 2001 From: Chongchen Chen Date: Thu, 31 Jul 2025 19:18:44 +0800 Subject: [PATCH 4/5] add test & use parse_expr --- src/parser/mod.rs | 16 +--------------- tests/sqlparser_common.rs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3ac22e3f7..886e08d9c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13892,21 +13892,7 @@ impl<'a> Parser<'a> { None }; self.expect_token(&Token::LParen)?; - let value = match self.peek_token_ref().token { - Token::LParen => { - // multi value column unpivot - Expr::Tuple( - self.parse_parenthesized_column_list(Mandatory, false)? - .into_iter() - .map(Expr::Identifier) - .collect(), - ) - } - _ => { - // single value column unpivot - Expr::Identifier(self.parse_identifier()?) - } - }; + let value = self.parse_expr()?; self.expect_keyword_is(Keyword::FOR)?; let name = self.parse_identifier()?; self.expect_keyword_is(Keyword::IN)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 71b3d88e8..475fdf0f1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11101,6 +11101,44 @@ fn parse_unpivot_table() { verified_stmt(sql_unpivot_with_alias_and_multi_value).to_string(), sql_unpivot_with_alias_and_multi_value ); + + let sql_unpivot_with_alias_and_multi_value_and_qualifier = concat!( + "SELECT * FROM sales AS s ", + "UNPIVOT INCLUDE NULLS ((first_quarter, second_quarter) ", + "FOR half_of_the_year IN (", + "(sales.Q1, sales.Q2) AS H1, ", + "(sales.Q3, sales.Q4) AS H2", + "))" + ); + + if let Unpivot { columns, .. } = + &verified_only_select(sql_unpivot_with_alias_and_multi_value_and_qualifier).from[0].relation + { + assert_eq!( + *columns, + vec![ + ExprWithAlias { + expr: Expr::Tuple(vec![ + Expr::CompoundIdentifier(vec![Ident::new("sales"), Ident::new("Q1"),]), + Expr::CompoundIdentifier(vec![Ident::new("sales"), Ident::new("Q2"),]), + ]), + alias: Some(Ident::new("H1")), + }, + ExprWithAlias { + expr: Expr::Tuple(vec![ + Expr::CompoundIdentifier(vec![Ident::new("sales"), Ident::new("Q3"),]), + Expr::CompoundIdentifier(vec![Ident::new("sales"), Ident::new("Q4"),]), + ]), + alias: Some(Ident::new("H2")), + }, + ] + ); + } + + assert_eq!( + verified_stmt(sql_unpivot_with_alias_and_multi_value_and_qualifier).to_string(), + sql_unpivot_with_alias_and_multi_value_and_qualifier + ); } #[test] From 7de5248458c2f9e38babdf2f21f095fa7d8b1582 Mon Sep 17 00:00:00 2001 From: Chongchen Chen Date: Fri, 1 Aug 2025 20:50:46 +0800 Subject: [PATCH 5/5] update --- src/parser/mod.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 886e08d9c..47e53744e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10810,16 +10810,6 @@ impl<'a> Parser<'a> { } } - pub fn parse_parenthesized_columns_with_alias_list( - &mut self, - optional: IsOptional, - allow_empty: bool, - ) -> Result, ParserError> { - self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| { - p.parse_expr_with_alias() - }) - } - /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers. /// For example: `(col1, "col 2", ...)` pub fn parse_parenthesized_column_list( @@ -13896,7 +13886,9 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::FOR)?; let name = self.parse_identifier()?; self.expect_keyword_is(Keyword::IN)?; - let columns = self.parse_parenthesized_columns_with_alias_list(Mandatory, false)?; + let columns = self.parse_parenthesized_column_list_inner(Mandatory, false, |p| { + p.parse_expr_with_alias() + })?; self.expect_token(&Token::RParen)?; let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Unpivot {