Skip to content
26 changes: 26 additions & 0 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1653,6 +1653,21 @@ pub enum TableFactor {
/// Optional alias for the resulting table.
alias: Option<TableAlias>,
},
/// Object unpivoting on a SUPER expression in the FROM clause.
///
/// Syntax:
/// ```sql
/// UNPIVOT expression AS value_alias [AT attribute_alias]
/// ```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a link to the docs describing the syntax?

@kfirSatori kfirSatori Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iffyio , Added link to docs

/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/query-super.html#unpivoting)
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<Ident>,
},
/// A `MATCH_RECOGNIZE` operation on a table.
///
/// See <https://docs.snowflake.com/en/sql-reference/constructs/match_recognize>.
Expand Down Expand Up @@ -2422,6 +2437,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,
Expand Down
9 changes: 9 additions & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2063,6 +2063,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,
Expand Down
10 changes: 10 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,16 @@ pub trait Dialect: Debug + Any {
false
}

/// Returns true if the dialect supports object-unpivot table factors in the FROM clause.
///
/// Syntax:
/// ```sql
/// SELECT * FROM T UNPIVOT expression AS value_alias [AT attribute_alias]
/// ```
fn supports_unpivot_expr(&self) -> bool {
false
}

/// Returns true if the dialect supports the `CONSTRAINT` keyword without a name
/// in table constraint definitions.
///
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/redshift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ impl Dialect for RedshiftSqlDialect {
true
}

fn supports_unpivot_expr(&self) -> bool {
true
}

fn supports_string_escape_constant(&self) -> bool {
true
}
Expand Down
30 changes: 30 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16584,6 +16584,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
Expand Down Expand Up @@ -16665,6 +16671,8 @@ impl<'a> Parser<'a> {
with_offset_alias,
with_ordinality,
})
} else if self.dialect.supports_unpivot_expr() && self.peek_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)?;
Expand Down Expand Up @@ -17663,6 +17671,28 @@ 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<TableFactor, ParserError> {
self.expect_keyword_is(Keyword::UNPIVOT)?;
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 <expr>`, `USING (...)`, or no constraint).
pub fn parse_join_constraint(&mut self, natural: bool) -> Result<JoinConstraint, ParserError> {
if natural {
Expand Down
10 changes: 10 additions & 0 deletions tests/sqlparser_redshift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,3 +542,13 @@ fn test_partiql_from_alias_with_at_index() {
_ => panic!("expected table factor"),
}
}

#[test]
fn parse_unpivot_expression() {
let dialects = all_dialects_where(|d| d.supports_unpivot_expr());

dialects.verified_stmt(
"SELECT t.id, k, v FROM test_colors AS t, UNPIVOT t.count_by_color AS v AT k",
);
dialects.verified_stmt("SELECT t.id, k, v FROM test_colors AS t, UNPIVOT t AS v AT k");
}