Skip to content

Commit 140d723

Browse files
committed
impl pivot
1 parent 96eb232 commit 140d723

3 files changed

Lines changed: 128 additions & 0 deletions

File tree

src/ast/query.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2723,6 +2723,17 @@ pub enum PipeOperator {
27232723
///
27242724
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#call_pipe_operator>
27252725
Call { function: Function, alias: Option<Ident> },
2726+
/// Pivots data from rows to columns.
2727+
///
2728+
/// Syntax: `|> PIVOT(aggregate_function(column) FOR pivot_column IN (value1, value2, ...)) [AS alias]`
2729+
///
2730+
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#pivot_pipe_operator>
2731+
Pivot {
2732+
aggregate_functions: Vec<ExprWithAlias>,
2733+
value_column: Vec<Ident>,
2734+
value_source: PivotValueSource,
2735+
alias: Option<Ident>,
2736+
},
27262737
}
27272738

27282739
impl fmt::Display for PipeOperator {
@@ -2854,6 +2865,24 @@ impl fmt::Display for PipeOperator {
28542865
}
28552866
Ok(())
28562867
}
2868+
PipeOperator::Pivot {
2869+
aggregate_functions,
2870+
value_column,
2871+
value_source,
2872+
alias,
2873+
} => {
2874+
write!(
2875+
f,
2876+
"PIVOT({} FOR {} IN ({}))",
2877+
display_comma_separated(aggregate_functions),
2878+
Expr::CompoundIdentifier(value_column.to_vec()),
2879+
value_source
2880+
)?;
2881+
if let Some(alias) = alias {
2882+
write!(f, " AS {}", alias)?;
2883+
}
2884+
Ok(())
2885+
}
28572886
}
28582887
}
28592888
}

src/parser/mod.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11088,6 +11088,7 @@ impl<'a> Parser<'a> {
1108811088
Keyword::INTERSECT,
1108911089
Keyword::EXCEPT,
1109011090
Keyword::CALL,
11091+
Keyword::PIVOT,
1109111092
])?;
1109211093
match kw {
1109311094
Keyword::SELECT => {
@@ -11231,6 +11232,51 @@ impl<'a> Parser<'a> {
1123111232
));
1123211233
}
1123311234
}
11235+
Keyword::PIVOT => {
11236+
self.expect_token(&Token::LParen)?;
11237+
let aggregate_functions = self.parse_comma_separated(Self::parse_aliased_function_call)?;
11238+
self.expect_keyword_is(Keyword::FOR)?;
11239+
let value_column = self.parse_period_separated(|p| p.parse_identifier())?;
11240+
self.expect_keyword_is(Keyword::IN)?;
11241+
11242+
self.expect_token(&Token::LParen)?;
11243+
let value_source = if self.parse_keyword(Keyword::ANY) {
11244+
let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) {
11245+
self.parse_comma_separated(Parser::parse_order_by_expr)?
11246+
} else {
11247+
vec![]
11248+
};
11249+
PivotValueSource::Any(order_by)
11250+
} else if self.peek_sub_query() {
11251+
PivotValueSource::Subquery(self.parse_query()?)
11252+
} else {
11253+
PivotValueSource::List(self.parse_comma_separated(Self::parse_expr_with_alias)?)
11254+
};
11255+
self.expect_token(&Token::RParen)?;
11256+
self.expect_token(&Token::RParen)?;
11257+
11258+
// Parse optional alias (with or without AS keyword)
11259+
let alias = if self.parse_keyword(Keyword::AS) {
11260+
Some(self.parse_identifier()?)
11261+
} else {
11262+
// Check if the next token is an identifier (implicit alias)
11263+
let checkpoint = self.index;
11264+
match self.parse_identifier() {
11265+
Ok(ident) => Some(ident),
11266+
Err(_) => {
11267+
self.index = checkpoint; // Rewind on failure
11268+
None
11269+
}
11270+
}
11271+
};
11272+
11273+
pipe_operators.push(PipeOperator::Pivot {
11274+
aggregate_functions,
11275+
value_column,
11276+
value_source,
11277+
alias,
11278+
});
11279+
}
1123411280
unhandled => {
1123511281
return Err(ParserError::ParserError(format!(
1123611282
"`expect_one_of_keywords` further up allowed unhandled keyword: {unhandled:?}"

tests/sqlparser_common.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15269,6 +15269,34 @@ fn parse_pipeline_operator() {
1526915269
dialects.verified_stmt("SELECT * FROM users |> CALL transform() |> WHERE status = 'active' |> CALL process(param)");
1527015270
dialects.verified_stmt("SELECT * FROM data |> CALL preprocess() AS clean |> SELECT col1, col2 |> CALL validate()");
1527115271

15272+
// pivot pipe operator
15273+
dialects.verified_stmt("SELECT * FROM monthly_sales |> PIVOT(SUM(amount) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))");
15274+
dialects.verified_stmt("SELECT * FROM sales_data |> PIVOT(AVG(revenue) FOR region IN ('North', 'South', 'East', 'West'))");
15275+
15276+
// pivot pipe operator with multiple aggregate functions
15277+
dialects.verified_stmt("SELECT * FROM data |> PIVOT(SUM(sales) AS total_sales, COUNT(*) AS num_transactions FOR month IN ('Jan', 'Feb', 'Mar'))");
15278+
15279+
// pivot pipe operator with compound column names
15280+
dialects.verified_stmt("SELECT * FROM sales |> PIVOT(SUM(amount) FOR product.category IN ('Electronics', 'Clothing'))");
15281+
15282+
// pivot pipe operator mixed with other pipe operators
15283+
dialects.verified_stmt("SELECT * FROM sales_data |> WHERE year = 2023 |> PIVOT(SUM(revenue) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))");
15284+
15285+
// pivot pipe operator with aliases
15286+
dialects.verified_stmt("SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) AS quarterly_sales");
15287+
dialects.verified_stmt("SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category");
15288+
dialects.verified_stmt("SELECT * FROM sales |> PIVOT(COUNT(*) AS transactions, SUM(amount) AS total FOR region IN ('North', 'South')) AS regional_summary");
15289+
15290+
// pivot pipe operator with implicit aliases (without AS keyword)
15291+
dialects.verified_query_with_canonical(
15292+
"SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) quarterly_sales",
15293+
"SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) AS quarterly_sales",
15294+
);
15295+
dialects.verified_query_with_canonical(
15296+
"SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) avg_by_category",
15297+
"SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category",
15298+
);
15299+
1527215300
// many pipes
1527315301
dialects.verified_stmt(
1527415302
"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",
@@ -15351,6 +15379,31 @@ fn parse_pipeline_operator_negative_tests() {
1535115379
assert!(
1535215380
dialects.parse_sql_statements("SELECT * FROM users |> CALL my_function() AS").is_err()
1535315381
);
15382+
15383+
// Test that PIVOT without parentheses fails
15384+
assert!(
15385+
dialects.parse_sql_statements("SELECT * FROM users |> PIVOT SUM(amount) FOR month IN ('Jan')").is_err()
15386+
);
15387+
15388+
// Test that PIVOT without FOR keyword fails
15389+
assert!(
15390+
dialects.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) month IN ('Jan'))").is_err()
15391+
);
15392+
15393+
// Test that PIVOT without IN keyword fails
15394+
assert!(
15395+
dialects.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month ('Jan'))").is_err()
15396+
);
15397+
15398+
// Test that PIVOT with empty IN list fails
15399+
assert!(
15400+
dialects.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ())").is_err()
15401+
);
15402+
15403+
// Test that PIVOT with invalid alias syntax fails
15404+
assert!(
15405+
dialects.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ('Jan')) AS").is_err()
15406+
);
1535415407
}
1535515408

1535615409
#[test]

0 commit comments

Comments
 (0)