From 3fade60c9673c03e21dc280a53a84edd0c62e741 Mon Sep 17 00:00:00 2001 From: Chongchen Chen Date: Wed, 23 Jul 2025 21:52:16 +0800 Subject: [PATCH 1/7] feat: support multiple value for pivot --- src/ast/query.rs | 11 ++++++++--- src/ast/spans.rs | 2 +- src/parser/mod.rs | 16 +++++++++++++++- tests/sqlparser_common.rs | 10 ++++++++-- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 7ffb64d9bb..c4eb410e3b 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1336,7 +1336,7 @@ pub enum TableFactor { Pivot { table: Box, aggregate_functions: Vec, // Function expression - value_column: Vec, + value_column: Vec, // Expr is a identifier or a compound identifier value_source: PivotValueSource, default_on_null: Option, alias: Option, @@ -2010,10 +2010,15 @@ impl fmt::Display for TableFactor { } => { write!( f, - "{table} PIVOT({} FOR {} IN ({value_source})", + "{table} PIVOT({} FOR ", display_comma_separated(aggregate_functions), - Expr::CompoundIdentifier(value_column.to_vec()), )?; + if value_column.len() == 1 { + write!(f, "{}", value_column[0])?; + } else { + write!(f, "({})", display_comma_separated(value_column))?; + } + write!(f, " IN ({value_source})")?; if let Some(expr) = default_on_null { write!(f, " DEFAULT ON NULL ({expr})")?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 3e82905e14..f38d9a64fb 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1971,7 +1971,7 @@ impl Spanned for TableFactor { } => union_spans( core::iter::once(table.span()) .chain(aggregate_functions.iter().map(|i| i.span())) - .chain(value_column.iter().map(|i| i.span)) + .chain(value_column.iter().map(|i| i.span())) .chain(core::iter::once(value_source.span())) .chain(default_on_null.as_ref().map(|i| i.span())) .chain(alias.as_ref().map(|i| i.span())), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8d5a55da00..448e439765 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10820,6 +10820,16 @@ impl<'a> Parser<'a> { self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| p.parse_identifier()) } + pub fn parse_parenthesized_compound_identifier_list( + &mut self, + optional: IsOptional, + allow_empty: bool, + ) -> Result, ParserError> { + self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| { + Ok(Expr::CompoundIdentifier(p.parse_period_separated(|p| p.parse_identifier())?)) + }) + } + /// Parses a parenthesized comma-separated list of index columns, which can be arbitrary /// expressions with ordering information (and an opclass in some dialects). fn parse_parenthesized_index_column_list(&mut self) -> Result, ParserError> { @@ -13828,7 +13838,11 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let aggregate_functions = self.parse_comma_separated(Self::parse_aliased_function_call)?; self.expect_keyword_is(Keyword::FOR)?; - let value_column = self.parse_period_separated(|p| p.parse_identifier())?; + let value_column = if self.peek_token().token == Token::LParen { + self.parse_parenthesized_compound_identifier_list(Mandatory, false)? + } else { + vec![Expr::CompoundIdentifier(self.parse_period_separated(|p| p.parse_identifier())?)] + }; self.expect_keyword_is(Keyword::IN)?; self.expect_token(&Token::LParen)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5d8284a464..939fb9b48a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10875,7 +10875,7 @@ fn parse_pivot_table() { expected_function("b", Some("t")), expected_function("c", Some("u")), ], - value_column: vec![Ident::new("a"), Ident::new("MONTH")], + value_column: vec![Expr::CompoundIdentifier(vec![Ident::new("a"), Ident::new("MONTH")])], value_source: PivotValueSource::List(vec![ ExprWithAlias { expr: Expr::value(number("1")), @@ -10922,6 +10922,12 @@ fn parse_pivot_table() { verified_stmt(sql_without_table_alias).to_string(), sql_without_table_alias ); + + let sql_with_multiple_value_column = concat!( + "SELECT * FROM person ", + "PIVOT(SUM(age) AS a, AVG(class) AS c FOR (name, age) IN (('John', 30) AS c1, ('Mike', 40) AS c2))" + ); + assert_eq!(verified_stmt(sql_with_multiple_value_column).to_string(), sql_with_multiple_value_column); } #[test] @@ -11143,7 +11149,7 @@ fn parse_pivot_unpivot_table() { expr: call("sum", [Expr::Identifier(Ident::new("population"))]), alias: None }], - value_column: vec![Ident::new("year")], + value_column: vec![Expr::CompoundIdentifier(vec![Ident::new("year")])], value_source: PivotValueSource::List(vec![ ExprWithAlias { expr: Expr::Value( From 9b2e8d05a23601978b7bf139cdaef27d02ac69c3 Mon Sep 17 00:00:00 2001 From: Chongchen Chen Date: Wed, 23 Jul 2025 21:55:33 +0800 Subject: [PATCH 2/7] update --- 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 448e439765..0e7d3ce5ab 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13838,7 +13838,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::LParen)?; let aggregate_functions = self.parse_comma_separated(Self::parse_aliased_function_call)?; self.expect_keyword_is(Keyword::FOR)?; - let value_column = if self.peek_token().token == Token::LParen { + let value_column = if self.peek_token_ref().token == Token::LParen { self.parse_parenthesized_compound_identifier_list(Mandatory, false)? } else { vec![Expr::CompoundIdentifier(self.parse_period_separated(|p| p.parse_identifier())?)] From 132502e1efd6a368b9bbc2e89138eacaaeed34a9 Mon Sep 17 00:00:00 2001 From: Chongchen Chen Date: Wed, 23 Jul 2025 21:56:39 +0800 Subject: [PATCH 3/7] format --- src/ast/query.rs | 2 +- src/parser/mod.rs | 8 ++++++-- tests/sqlparser_common.rs | 10 ++++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index c4eb410e3b..541047ca8a 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1336,7 +1336,7 @@ pub enum TableFactor { Pivot { table: Box, aggregate_functions: Vec, // Function expression - value_column: Vec, // Expr is a identifier or a compound identifier + value_column: Vec, // Expr is a identifier or a compound identifier value_source: PivotValueSource, default_on_null: Option, alias: Option, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0e7d3ce5ab..99f5fe4973 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10826,7 +10826,9 @@ impl<'a> Parser<'a> { allow_empty: bool, ) -> Result, ParserError> { self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| { - Ok(Expr::CompoundIdentifier(p.parse_period_separated(|p| p.parse_identifier())?)) + Ok(Expr::CompoundIdentifier( + p.parse_period_separated(|p| p.parse_identifier())?, + )) }) } @@ -13841,7 +13843,9 @@ impl<'a> Parser<'a> { let value_column = if self.peek_token_ref().token == Token::LParen { self.parse_parenthesized_compound_identifier_list(Mandatory, false)? } else { - vec![Expr::CompoundIdentifier(self.parse_period_separated(|p| p.parse_identifier())?)] + vec![Expr::CompoundIdentifier( + self.parse_period_separated(|p| p.parse_identifier())?, + )] }; self.expect_keyword_is(Keyword::IN)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 939fb9b48a..19ce2493a5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10875,7 +10875,10 @@ fn parse_pivot_table() { expected_function("b", Some("t")), expected_function("c", Some("u")), ], - value_column: vec![Expr::CompoundIdentifier(vec![Ident::new("a"), Ident::new("MONTH")])], + value_column: vec![Expr::CompoundIdentifier(vec![ + Ident::new("a"), + Ident::new("MONTH") + ])], value_source: PivotValueSource::List(vec![ ExprWithAlias { expr: Expr::value(number("1")), @@ -10927,7 +10930,10 @@ fn parse_pivot_table() { "SELECT * FROM person ", "PIVOT(SUM(age) AS a, AVG(class) AS c FOR (name, age) IN (('John', 30) AS c1, ('Mike', 40) AS c2))" ); - assert_eq!(verified_stmt(sql_with_multiple_value_column).to_string(), sql_with_multiple_value_column); + assert_eq!( + verified_stmt(sql_with_multiple_value_column).to_string(), + sql_with_multiple_value_column + ); } #[test] From a2453366f9c951609c8108c45124087161b5416e Mon Sep 17 00:00:00 2001 From: Chongchen Chen Date: Wed, 23 Jul 2025 22:14:07 +0800 Subject: [PATCH 4/7] update ut --- src/ast/visitor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 8e0a3139a1..7840f0e141 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -884,6 +884,8 @@ mod tests { "PRE: EXPR: a.amount", "POST: EXPR: a.amount", "POST: EXPR: SUM(a.amount)", + "PRE: EXPR: a.MONTH", + "POST: EXPR: a.MONTH", "PRE: EXPR: 'JAN'", "POST: EXPR: 'JAN'", "PRE: EXPR: 'FEB'", From 12f8543a077f47253f43479f1606f6076b8376c1 Mon Sep 17 00:00:00 2001 From: Chongchen Chen Date: Fri, 8 Aug 2025 09:02:14 +0800 Subject: [PATCH 5/7] update --- src/ast/query.rs | 2 +- tests/sqlparser_common.rs | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 541047ca8a..a60d2a36f7 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1336,7 +1336,7 @@ pub enum TableFactor { Pivot { table: Box, aggregate_functions: Vec, // Function expression - value_column: Vec, // Expr is a identifier or a compound identifier + value_column: Vec, value_source: PivotValueSource, default_on_null: Option, alias: Option, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 19ce2493a5..1e79e7bda1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10925,15 +10925,6 @@ fn parse_pivot_table() { verified_stmt(sql_without_table_alias).to_string(), sql_without_table_alias ); - - let sql_with_multiple_value_column = concat!( - "SELECT * FROM person ", - "PIVOT(SUM(age) AS a, AVG(class) AS c FOR (name, age) IN (('John', 30) AS c1, ('Mike', 40) AS c2))" - ); - assert_eq!( - verified_stmt(sql_with_multiple_value_column).to_string(), - sql_with_multiple_value_column - ); } #[test] From 62189bcde66968555260350b950b6359cd2ed0b7 Mon Sep 17 00:00:00 2001 From: Chongchen Chen Date: Sat, 9 Aug 2025 19:30:35 +0800 Subject: [PATCH 6/7] ut --- src/parser/mod.rs | 8 ++--- tests/sqlparser_common.rs | 67 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 99f5fe4973..942c551cf7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13841,11 +13841,11 @@ impl<'a> Parser<'a> { let aggregate_functions = self.parse_comma_separated(Self::parse_aliased_function_call)?; self.expect_keyword_is(Keyword::FOR)?; let value_column = if self.peek_token_ref().token == Token::LParen { - self.parse_parenthesized_compound_identifier_list(Mandatory, false)? + self.parse_parenthesized_column_list_inner(Mandatory, false, |p| { + p.parse_subexpr(self.dialect.prec_value(Precedence::Between)) + })? } else { - vec![Expr::CompoundIdentifier( - self.parse_period_separated(|p| p.parse_identifier())?, - )] + vec![self.parse_subexpr(self.dialect.prec_value(Precedence::Between))?] }; self.expect_keyword_is(Keyword::IN)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1e79e7bda1..82a7baa380 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10925,6 +10925,71 @@ fn parse_pivot_table() { verified_stmt(sql_without_table_alias).to_string(), sql_without_table_alias ); + + let multiple_value_columns_sql = concat!( + "SELECT * FROM person ", + "PIVOT(", + "SUM(age) AS a, AVG(class) AS c ", + "FOR (name, age) IN (('John', 30) AS c1, ('Mike', 40) AS c2))", + ); + + assert_eq!( + verified_only_select(multiple_value_columns_sql).from[0].relation, + Pivot { + table: Box::new(TableFactor::Table { + name: ObjectName::from(vec![Ident::new("person")]), + alias: None, + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }), + aggregate_functions: vec![ + ExprWithAlias { + expr: call("SUM", [Expr::Identifier(Ident::new("age"))]), + alias: Some(Ident::new("a")) + }, + ExprWithAlias { + expr: call("AVG", [Expr::Identifier(Ident::new("class"))]), + alias: Some(Ident::new("c")) + }, + ], + value_column: vec![ + Expr::Identifier(Ident::new("name")), + Expr::Identifier(Ident::new("age")), + ], + value_source: PivotValueSource::List(vec![ + ExprWithAlias { + expr: Expr::Tuple(vec![ + Expr::Value( + (Value::SingleQuotedString("John".to_string())).with_empty_span() + ), + Expr::Value((Value::Number("30".into(), false)).with_empty_span()), + ]), + alias: Some(Ident::new("c1")) + }, + ExprWithAlias { + expr: Expr::Tuple(vec![ + Expr::Value( + (Value::SingleQuotedString("Mike".to_string())).with_empty_span() + ), + Expr::Value((Value::Number("40".into(), false)).with_empty_span()), + ]), + alias: Some(Ident::new("c2")) + }, + ]), + default_on_null: None, + alias: None, + } + ); + assert_eq!( + verified_stmt(multiple_value_columns_sql).to_string(), + multiple_value_columns_sql + ); } #[test] @@ -11146,7 +11211,7 @@ fn parse_pivot_unpivot_table() { expr: call("sum", [Expr::Identifier(Ident::new("population"))]), alias: None }], - value_column: vec![Expr::CompoundIdentifier(vec![Ident::new("year")])], + value_column: vec![Expr::Identifier(Ident::new("year"))], value_source: PivotValueSource::List(vec![ ExprWithAlias { expr: Expr::Value( From a69ec0f0062f6a3ad7b36a754867599752ec997a Mon Sep 17 00:00:00 2001 From: Chongchen Chen Date: Sat, 9 Aug 2025 19:37:41 +0800 Subject: [PATCH 7/7] update --- tests/sqlparser_common.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 82a7baa380..5935cdd18a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10968,7 +10968,9 @@ fn parse_pivot_table() { Expr::Value( (Value::SingleQuotedString("John".to_string())).with_empty_span() ), - Expr::Value((Value::Number("30".into(), false)).with_empty_span()), + Expr::Value( + (Value::Number("30".parse().unwrap(), false)).with_empty_span() + ), ]), alias: Some(Ident::new("c1")) }, @@ -10977,7 +10979,9 @@ fn parse_pivot_table() { Expr::Value( (Value::SingleQuotedString("Mike".to_string())).with_empty_span() ), - Expr::Value((Value::Number("40".into(), false)).with_empty_span()), + Expr::Value( + (Value::Number("40".parse().unwrap(), false)).with_empty_span() + ), ]), alias: Some(Ident::new("c2")) },