diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5bc7070e8..b2777ccbf 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8294,6 +8294,13 @@ pub enum FunctionArgumentClause { /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/navigation_functions#first_value IgnoreOrRespectNulls(NullTreatment), + /// The inline `WHERE` filter clause on an aggregate call, e.g. + /// `COUNT(* WHERE cond)` / `SUM(x WHERE cond)` / `ARRAY_AGG(x WHERE cond ORDER BY ..)`. + /// Popularized by [GoogleSQL]; equivalent to the standard `AGG(x) FILTER (WHERE cond)`. + /// Accepted for all dialects since `WHERE` cannot otherwise begin a function argument. + /// + /// [GoogleSQL]: https://cloud.google.com/bigquery/docs/reference/standard-sql/aggregate_functions#grouping_and_filtering + Where(Expr), /// Specifies the the ordering for some ordered set aggregates, e.g. `ARRAY_AGG` on [BigQuery]. /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/aggregate_functions#array_agg @@ -8335,6 +8342,7 @@ impl fmt::Display for FunctionArgumentClause { FunctionArgumentClause::IgnoreOrRespectNulls(null_treatment) => { write!(f, "{null_treatment}") } + FunctionArgumentClause::Where(expr) => write!(f, "WHERE {expr}"), FunctionArgumentClause::OrderBy(order_by) => { write!(f, "ORDER BY {}", display_comma_separated(order_by)) } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 6505b209e..aaad33a5c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1806,6 +1806,7 @@ impl Spanned for FunctionArgumentClause { fn span(&self) -> Span { match self { FunctionArgumentClause::IgnoreOrRespectNulls(_) => Span::empty(), + FunctionArgumentClause::Where(expr) => expr.span(), FunctionArgumentClause::OrderBy(vec) => union_spans(vec.iter().map(|i| i.expr.span())), FunctionArgumentClause::Limit(expr) => expr.span(), FunctionArgumentClause::OnOverflow(_) => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 09715bbc7..eadd1682c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -18870,6 +18870,14 @@ impl<'a> Parser<'a> { let duplicate_treatment = self.parse_duplicate_treatment()?; let args = self.parse_comma_separated(Parser::parse_function_args)?; + // Inline aggregate filter: `AGG(expr WHERE cond [ORDER BY ..])`, popularized by + // GoogleSQL. Precedes ORDER BY / LIMIT / HAVING; equivalent to the standard + // `AGG(expr) FILTER (WHERE cond)`. Accepted for all dialects: `WHERE` is a reserved + // keyword that cannot begin a function argument, so the syntax is unambiguous. + if self.parse_keyword(Keyword::WHERE) { + clauses.push(FunctionArgumentClause::Where(self.parse_expr()?)); + } + if self.dialect.supports_window_function_null_treatment_arg() { if let Some(null_treatment) = self.parse_null_treatment()? { clauses.push(FunctionArgumentClause::IgnoreOrRespectNulls(null_treatment)); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 50832d062..ff3d80790 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6148,6 +6148,33 @@ fn parse_aggregate_with_group_by() { //TODO: assertions } +#[test] +fn parse_aggregate_with_where_filter() { + // The inline `WHERE` filter inside an aggregate call, e.g. `COUNT(* WHERE cond)` / + // `SUM(x WHERE cond)`, is the in-argument spelling of the standard + // `AGG(x) FILTER (WHERE cond)`. Popularized by GoogleSQL, it is accepted for all + // dialects (`verified_stmt` round-trips through every dialect). + verified_stmt("SELECT COUNT(* WHERE x > 1) FROM t"); + verified_stmt("SELECT SUM(x WHERE y > 0) FROM t"); + // Co-occurs with (and precedes) an in-argument ORDER BY. + verified_stmt("SELECT ARRAY_AGG(x WHERE x > 100 ORDER BY x DESC) FROM t"); + // A compound predicate referencing multiple columns round-trips intact. + verified_stmt("SELECT ARRAY_AGG(a WHERE b > 0 AND c < 10) FROM t"); + + // The filter is captured as a FunctionArgumentClause::Where holding the predicate. + let select = verified_only_select("SELECT SUM(salary WHERE dept = 1) FROM emp"); + let Expr::Function(func) = expr_from_projection(&select.projection[0]) else { + panic!("expected a function projection"); + }; + let FunctionArguments::List(list) = &func.args else { + panic!("expected a function argument list"); + }; + assert!(list + .clauses + .iter() + .any(|c| matches!(c, FunctionArgumentClause::Where(Expr::BinaryOp { .. })))); +} + #[test] fn parse_literal_integer() { let sql = "SELECT 1, -10, +20";