Skip to content

Commit eaf7c8e

Browse files
committed
unpivot
1 parent 140d723 commit eaf7c8e

3 files changed

Lines changed: 147 additions & 0 deletions

File tree

src/ast/query.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2734,6 +2734,20 @@ pub enum PipeOperator {
27342734
value_source: PivotValueSource,
27352735
alias: Option<Ident>,
27362736
},
2737+
/// The `UNPIVOT` pipe operator transforms columns into rows.
2738+
///
2739+
/// Syntax:
2740+
/// ```sql
2741+
/// |> UNPIVOT(value_column FOR name_column IN (column1, column2, ...)) [alias]
2742+
/// ```
2743+
///
2744+
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#unpivot_pipe_operator>
2745+
Unpivot {
2746+
value_column: Ident,
2747+
name_column: Ident,
2748+
unpivot_columns: Vec<Ident>,
2749+
alias: Option<Ident>,
2750+
},
27372751
}
27382752

27392753
impl fmt::Display for PipeOperator {
@@ -2883,6 +2897,24 @@ impl fmt::Display for PipeOperator {
28832897
}
28842898
Ok(())
28852899
}
2900+
PipeOperator::Unpivot {
2901+
value_column,
2902+
name_column,
2903+
unpivot_columns,
2904+
alias,
2905+
} => {
2906+
write!(
2907+
f,
2908+
"UNPIVOT({} FOR {} IN ({}))",
2909+
value_column,
2910+
name_column,
2911+
display_comma_separated(unpivot_columns)
2912+
)?;
2913+
if let Some(alias) = alias {
2914+
write!(f, " AS {}", alias)?;
2915+
}
2916+
Ok(())
2917+
}
28862918
}
28872919
}
28882920
}

src/parser/mod.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11089,6 +11089,7 @@ impl<'a> Parser<'a> {
1108911089
Keyword::EXCEPT,
1109011090
Keyword::CALL,
1109111091
Keyword::PIVOT,
11092+
Keyword::UNPIVOT,
1109211093
])?;
1109311094
match kw {
1109411095
Keyword::SELECT => {
@@ -11277,6 +11278,51 @@ impl<'a> Parser<'a> {
1127711278
alias,
1127811279
});
1127911280
}
11281+
Keyword::UNPIVOT => {
11282+
// Parse UNPIVOT(value_column FOR name_column IN (column1, column2, ...)) [alias]
11283+
self.expect_token(&Token::LParen)?;
11284+
11285+
// Parse value_column
11286+
let value_column = self.parse_identifier()?;
11287+
11288+
// Parse FOR keyword
11289+
self.expect_keyword(Keyword::FOR)?;
11290+
11291+
// Parse name_column
11292+
let name_column = self.parse_identifier()?;
11293+
11294+
// Parse IN keyword
11295+
self.expect_keyword(Keyword::IN)?;
11296+
11297+
// Parse (column1, column2, ...)
11298+
self.expect_token(&Token::LParen)?;
11299+
let unpivot_columns = self.parse_comma_separated(Parser::parse_identifier)?;
11300+
self.expect_token(&Token::RParen)?;
11301+
11302+
self.expect_token(&Token::RParen)?;
11303+
11304+
// Parse optional alias (with or without AS keyword)
11305+
let alias = if self.parse_keyword(Keyword::AS) {
11306+
Some(self.parse_identifier()?)
11307+
} else {
11308+
// Check if the next token is an identifier (implicit alias)
11309+
let checkpoint = self.index;
11310+
match self.parse_identifier() {
11311+
Ok(ident) => Some(ident),
11312+
Err(_) => {
11313+
self.index = checkpoint; // Rewind on failure
11314+
None
11315+
}
11316+
}
11317+
};
11318+
11319+
pipe_operators.push(PipeOperator::Unpivot {
11320+
value_column,
11321+
name_column,
11322+
unpivot_columns,
11323+
alias,
11324+
});
11325+
}
1128011326
unhandled => {
1128111327
return Err(ParserError::ParserError(format!(
1128211328
"`expect_one_of_keywords` further up allowed unhandled keyword: {unhandled:?}"

tests/sqlparser_common.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15297,6 +15297,33 @@ fn parse_pipeline_operator() {
1529715297
"SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category",
1529815298
);
1529915299

15300+
// unpivot pipe operator basic usage
15301+
dialects.verified_stmt("SELECT * FROM sales |> UNPIVOT(revenue FOR quarter IN (Q1, Q2, Q3, Q4))");
15302+
dialects.verified_stmt("SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C))");
15303+
dialects.verified_stmt("SELECT * FROM metrics |> UNPIVOT(measurement FOR metric_type IN (cpu, memory, disk))");
15304+
15305+
// unpivot pipe operator with multiple columns
15306+
dialects.verified_stmt("SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (jan, feb, mar, apr, may, jun))");
15307+
dialects.verified_stmt("SELECT * FROM report |> UNPIVOT(score FOR subject IN (math, science, english, history))");
15308+
15309+
// unpivot pipe operator mixed with other pipe operators
15310+
dialects.verified_stmt("SELECT * FROM sales_data |> WHERE year = 2023 |> UNPIVOT(revenue FOR quarter IN (Q1, Q2, Q3, Q4))");
15311+
15312+
// unpivot pipe operator with aliases
15313+
dialects.verified_stmt("SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) AS unpivoted_sales");
15314+
dialects.verified_stmt("SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) AS transformed_data");
15315+
dialects.verified_stmt("SELECT * FROM metrics |> UNPIVOT(measurement FOR metric_type IN (cpu, memory)) AS metric_measurements");
15316+
15317+
// unpivot pipe operator with implicit aliases (without AS keyword)
15318+
dialects.verified_query_with_canonical(
15319+
"SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) unpivoted_sales",
15320+
"SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) AS unpivoted_sales",
15321+
);
15322+
dialects.verified_query_with_canonical(
15323+
"SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) transformed_data",
15324+
"SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) AS transformed_data",
15325+
);
15326+
1530015327
// many pipes
1530115328
dialects.verified_stmt(
1530215329
"SELECT * FROM CustomerOrders |> AGGREGATE SUM(cost) AS total_cost GROUP BY customer_id, state, item_type |> EXTEND COUNT(*) OVER (PARTITION BY customer_id) AS num_orders |> WHERE num_orders > 1 |> AGGREGATE AVG(total_cost) AS average GROUP BY state DESC, item_type ASC",
@@ -15404,6 +15431,48 @@ fn parse_pipeline_operator_negative_tests() {
1540415431
assert!(
1540515432
dialects.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ('Jan')) AS").is_err()
1540615433
);
15434+
15435+
// Test UNPIVOT negative cases
15436+
15437+
// Test that UNPIVOT without parentheses fails
15438+
assert!(
15439+
dialects.parse_sql_statements("SELECT * FROM users |> UNPIVOT value FOR name IN col1, col2").is_err()
15440+
);
15441+
15442+
// Test that UNPIVOT without FOR keyword fails
15443+
assert!(
15444+
dialects.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value name IN (col1, col2))").is_err()
15445+
);
15446+
15447+
// Test that UNPIVOT without IN keyword fails
15448+
assert!(
15449+
dialects.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name (col1, col2))").is_err()
15450+
);
15451+
15452+
// Test that UNPIVOT with missing value column fails
15453+
assert!(
15454+
dialects.parse_sql_statements("SELECT * FROM users |> UNPIVOT(FOR name IN (col1, col2))").is_err()
15455+
);
15456+
15457+
// Test that UNPIVOT with missing name column fails
15458+
assert!(
15459+
dialects.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR IN (col1, col2))").is_err()
15460+
);
15461+
15462+
// Test that UNPIVOT with empty IN list fails
15463+
assert!(
15464+
dialects.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN ())").is_err()
15465+
);
15466+
15467+
// Test that UNPIVOT with invalid alias syntax fails
15468+
assert!(
15469+
dialects.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN (col1, col2)) AS").is_err()
15470+
);
15471+
15472+
// Test that UNPIVOT with missing closing parenthesis fails
15473+
assert!(
15474+
dialects.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN (col1, col2)").is_err()
15475+
);
1540715476
}
1540815477

1540915478
#[test]

0 commit comments

Comments
 (0)