From 777243f7b20ac0f1185040e22b13974d6ce5d6cb Mon Sep 17 00:00:00 2001 From: Kfir Ben Shimon Date: Tue, 9 Jun 2026 19:04:13 +0300 Subject: [PATCH] Added support for unpivot in Redshift with expression and bracketsless --- src/ast/query.rs | 25 +++++++++++++++++++++++++ src/ast/spans.rs | 9 +++++++++ src/dialect/mod.rs | 10 ++++++++++ src/dialect/redshift.rs | 4 ++++ src/parser/mod.rs | 31 +++++++++++++++++++++++++++++++ tests/sqlparser_redshift.rs | 18 ++++++++++++++++++ 6 files changed, 97 insertions(+) diff --git a/src/ast/query.rs b/src/ast/query.rs index 1de0e0e9db..97c9105eae 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1653,6 +1653,20 @@ pub enum TableFactor { /// Optional alias for the resulting table. alias: Option, }, + /// Object unpivoting on a SUPER expression in the FROM clause. + /// + /// Syntax: + /// ```sql + /// UNPIVOT expression AS value_alias [AT attribute_alias] + /// ``` + UnpivotExpr { + /// SUPER expression to unpivot. + expression: Expr, + /// Alias for the generated unpivoted value. + value_alias: Ident, + /// Optional alias for the generated attribute key/index. + attribute_alias: Option, + }, /// A `MATCH_RECOGNIZE` operation on a table. /// /// See . @@ -2422,6 +2436,17 @@ impl fmt::Display for TableFactor { } Ok(()) } + TableFactor::UnpivotExpr { + expression, + value_alias, + attribute_alias, + } => { + write!(f, "UNPIVOT {expression} AS {value_alias}")?; + if let Some(attribute_alias) = attribute_alias { + write!(f, " AT {attribute_alias}")?; + } + Ok(()) + } TableFactor::MatchRecognize { table, partition_by, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0e328db433..50686a8d11 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2044,6 +2044,15 @@ impl Spanned for TableFactor { .chain(columns.iter().map(|ilist| ilist.span())) .chain(alias.as_ref().map(|alias| alias.span())), ), + TableFactor::UnpivotExpr { + expression, + value_alias, + attribute_alias, + } => union_spans( + core::iter::once(expression.span()) + .chain(core::iter::once(value_alias.span)) + .chain(attribute_alias.as_ref().map(|alias| alias.span)), + ), TableFactor::MatchRecognize { table, partition_by, diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 9b2ede40d2..6a8204938a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1240,6 +1240,16 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports object-unpivot table factors in the FROM clause. + /// + /// Syntax: + /// ```sql + /// UNPIVOT expression AS value_alias [AT attribute_alias] + /// ``` + fn supports_unpivot_expr_in_from(&self) -> bool { + false + } + /// Returns true if the dialect supports the `CONSTRAINT` keyword without a name /// in table constraint definitions. /// diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index db5bc53a0d..1f3db5a514 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -113,6 +113,10 @@ impl Dialect for RedshiftSqlDialect { true } + fn supports_unpivot_expr_in_from(&self) -> bool { + true + } + fn supports_string_escape_constant(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3c61851930..ec125e3b53 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -16162,6 +16162,12 @@ impl<'a> Parser<'a> { // `(mytable AS alias)` alias.replace(outer_alias); } + TableFactor::UnpivotExpr { .. } => { + return Err(ParserError::ParserError( + "alias after parenthesized UNPIVOT expression is not supported" + .to_string(), + )) + } }; } // Do not store the extra set of parens in the AST @@ -16243,6 +16249,10 @@ impl<'a> Parser<'a> { with_offset_alias, with_ordinality, }) + } else if self.dialect.supports_unpivot_expr_in_from() + && self.parse_keyword(Keyword::UNPIVOT) + { + self.parse_unpivot_expr_table_factor() } else if self.parse_keyword_with_tokens(Keyword::JSON_TABLE, &[Token::LParen]) { let json_expr = self.parse_expr()?; self.expect_token(&Token::Comma)?; @@ -17241,6 +17251,27 @@ impl<'a> Parser<'a> { }) } + /// Parse an object UNPIVOT table factor in FROM clause. + /// + /// Syntax: + /// `UNPIVOT expression AS value_alias [AT attribute_alias]` + pub fn parse_unpivot_expr_table_factor(&mut self) -> Result { + let expression = self.parse_expr()?; + self.expect_keyword_is(Keyword::AS)?; + let value_alias = self.parse_identifier()?; + let attribute_alias = if self.parse_keyword(Keyword::AT) { + Some(self.parse_identifier()?) + } else { + None + }; + + Ok(TableFactor::UnpivotExpr { + expression, + value_alias, + attribute_alias, + }) + } + /// Parse a JOIN constraint (`NATURAL`, `ON `, `USING (...)`, or no constraint). pub fn parse_join_constraint(&mut self, natural: bool) -> Result { if natural { diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 9609f52196..041ca3ea18 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -542,3 +542,21 @@ fn test_partiql_from_alias_with_at_index() { _ => panic!("expected table factor"), } } + +#[test] +fn parse_unpivot_expression() { + let sql = r#"SELECT t.id, k, v FROM test_colors as t, UNPIVOT t.count_by_color AS v AT k; +"#; + + redshift().parse_sql_statements(sql).unwrap(); + +} + +#[test] +fn parse_unpivot_no_brackets() { + let sql = r#"SELECT t.id, k, v FROM test_colors as t, UNPIVOT t AS v AT k; +"#; + + redshift().parse_sql_statements(sql).unwrap(); + +} \ No newline at end of file