From 77e0c2e315a36bb4f98848a87e4d640b523b2c48 Mon Sep 17 00:00:00 2001 From: Mosha Pasumansky Date: Mon, 13 Jul 2026 09:57:35 -0700 Subject: [PATCH 1/3] BigQuery: Support inline aggregate WHERE filter (`AGG(expr WHERE cond)`) GoogleSQL allows an aggregate call to carry an inline WHERE filter, e.g. `COUNT(* WHERE cond)`, `SUM(x WHERE cond)`, `ARRAY_AGG(x WHERE cond ORDER BY y)`. It is the in-argument spelling of the standard `AGG(expr) FILTER (WHERE cond)`. Add `FunctionArgumentClause::Where(Expr)` and parse it in `parse_function_argument_list` for the BigQuery and Generic dialects (before ORDER BY / LIMIT / HAVING), with `Display` and span support. Covered by a new `parse_aggregate_with_where_filter` test. --- src/ast/mod.rs | 7 +++++++ src/ast/spans.rs | 1 + src/parser/mod.rs | 8 ++++++++ tests/sqlparser_bigquery.rs | 25 +++++++++++++++++++++++++ 4 files changed, 41 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5bc7070e86..c3f10993dd 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8294,6 +8294,12 @@ 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 a GoogleSQL aggregate, e.g. + /// `COUNT(* WHERE cond)` / `SUM(x WHERE cond)` / `ARRAY_AGG(x WHERE cond ORDER BY ..)`. + /// Equivalent to the standard `AGG(x) FILTER (WHERE cond)`. + /// + /// [BigQuery]: 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 +8341,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 6505b209e3..aaad33a5c1 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 09715bbc71..e9e7184a02 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)?; + // GoogleSQL inline aggregate filter: `AGG(expr WHERE cond [ORDER BY ..])`. + // Precedes ORDER BY / LIMIT / HAVING; equivalent to `AGG(expr) FILTER (WHERE cond)`. + if dialect_of!(self is GenericDialect | BigQueryDialect) + && 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_bigquery.rs b/tests/sqlparser_bigquery.rs index f6d4483c24..1610e44e05 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2950,3 +2950,28 @@ fn test_create_snapshot_table() { "CREATE SNAPSHOT TABLE IF NOT EXISTS dataset_id.table1 CLONE dataset_id.table2 FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR) OPTIONS(expiration_timestamp = TIMESTAMP '2025-01-01 00:00:00 UTC')", ); } + +#[test] +fn parse_aggregate_with_where_filter() { + // GoogleSQL allows an inline `WHERE` filter inside an aggregate call, + // e.g. `COUNT(* WHERE cond)` / `SUM(x WHERE cond)`. It is the in-argument + // spelling of the standard `AGG(x) FILTER (WHERE cond)`. Round-trips via + // the `FunctionArgumentClause::Where` clause. + bigquery().verified_stmt("SELECT COUNT(* WHERE x > 1) FROM t"); + bigquery().verified_stmt("SELECT SUM(x WHERE y > 0) FROM t"); + // Co-occurs with (and precedes) an in-argument ORDER BY. + bigquery().verified_stmt("SELECT ARRAY_AGG(x WHERE x > 100 ORDER BY x DESC) FROM t"); + + // The filter is captured as a FunctionArgumentClause::Where. + let select = bigquery().verified_only_select("SELECT SUM(salary WHERE dept = 1) FROM emp"); + let SelectItem::UnnamedExpr(Expr::Function(func)) = &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(_)))); +} From 23ca1d1bde053243ad82245e2b2ecdcdcb8b3009 Mon Sep 17 00:00:00 2001 From: moshap Date: Wed, 15 Jul 2026 07:41:43 -0700 Subject: [PATCH 2/3] Accept inline aggregate WHERE filter for all dialects Address review feedback on the inline aggregate `WHERE` filter (`AGG(expr WHERE cond)`): accept it unconditionally rather than only for the BigQuery and Generic dialects. `WHERE` is a reserved keyword that cannot begin a function argument, so parsing it after the argument list is unambiguous in every dialect and reinterprets no existing syntax. - Drop the dialect gate in `parse_function_argument_list`. - Reword the `FunctionArgumentClause::Where` docs accordingly. - Move the test from `sqlparser_bigquery.rs` to `sqlparser_common.rs` so it round-trips through every dialect via `verified_stmt`. --- src/ast/mod.rs | 7 ++++--- src/parser/mod.rs | 10 +++++----- tests/sqlparser_bigquery.rs | 25 ------------------------- tests/sqlparser_common.rs | 27 +++++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c3f10993dd..b2777ccbf1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8294,11 +8294,12 @@ 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 a GoogleSQL aggregate, e.g. + /// 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 ..)`. - /// Equivalent to the standard `AGG(x) FILTER (WHERE cond)`. + /// Popularized by [GoogleSQL]; equivalent to the standard `AGG(x) FILTER (WHERE cond)`. + /// Accepted for all dialects since `WHERE` cannot otherwise begin a function argument. /// - /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/aggregate_functions#grouping_and_filtering + /// [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]. /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e9e7184a02..eadd1682c2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -18870,11 +18870,11 @@ impl<'a> Parser<'a> { let duplicate_treatment = self.parse_duplicate_treatment()?; let args = self.parse_comma_separated(Parser::parse_function_args)?; - // GoogleSQL inline aggregate filter: `AGG(expr WHERE cond [ORDER BY ..])`. - // Precedes ORDER BY / LIMIT / HAVING; equivalent to `AGG(expr) FILTER (WHERE cond)`. - if dialect_of!(self is GenericDialect | BigQueryDialect) - && self.parse_keyword(Keyword::WHERE) - { + // 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()?)); } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 1610e44e05..f6d4483c24 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2950,28 +2950,3 @@ fn test_create_snapshot_table() { "CREATE SNAPSHOT TABLE IF NOT EXISTS dataset_id.table1 CLONE dataset_id.table2 FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR) OPTIONS(expiration_timestamp = TIMESTAMP '2025-01-01 00:00:00 UTC')", ); } - -#[test] -fn parse_aggregate_with_where_filter() { - // GoogleSQL allows an inline `WHERE` filter inside an aggregate call, - // e.g. `COUNT(* WHERE cond)` / `SUM(x WHERE cond)`. It is the in-argument - // spelling of the standard `AGG(x) FILTER (WHERE cond)`. Round-trips via - // the `FunctionArgumentClause::Where` clause. - bigquery().verified_stmt("SELECT COUNT(* WHERE x > 1) FROM t"); - bigquery().verified_stmt("SELECT SUM(x WHERE y > 0) FROM t"); - // Co-occurs with (and precedes) an in-argument ORDER BY. - bigquery().verified_stmt("SELECT ARRAY_AGG(x WHERE x > 100 ORDER BY x DESC) FROM t"); - - // The filter is captured as a FunctionArgumentClause::Where. - let select = bigquery().verified_only_select("SELECT SUM(salary WHERE dept = 1) FROM emp"); - let SelectItem::UnnamedExpr(Expr::Function(func)) = &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(_)))); -} diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 50832d0625..ff3d80790f 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"; From b870788d55b067de673fae94d047658ba58beeb4 Mon Sep 17 00:00:00 2001 From: Mosha Pasumansky <93998884+moshap-firebolt@users.noreply.github.com> Date: Thu, 16 Jul 2026 04:34:08 -0700 Subject: [PATCH 3/3] Update src/parser/mod.rs Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index eadd1682c2..6a2292f318 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -18870,10 +18870,6 @@ 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()?)); }