From 2cd3d6f487219be21148d19dbb23e176efe71ad3 Mon Sep 17 00:00:00 2001 From: Chris Eubank <108756251+christianeu-db@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:41:07 +0000 Subject: [PATCH 1/5] feat(core-spec): add FILTER (WHERE ...) aggregate modifier Define the SQL-standard FILTER (WHERE ) postfix modifier on aggregate expressions in the Conditional Aggregations (REQUIRED) section of core-spec/expression_language.md. The clause has SQL:2003 semantics (optional feature T612): the aggregate is computed over only the rows where the predicate is TRUE, with the aggregate's own empty-input value when no row matches. The change is additive and backward compatible. It adds no schema node and no field to Metric, Field, or Dataset; the existing CASE form stays valid. The predicate MUST reference only fields of the same dataset as the aggregate's arguments. Clarifies that FILTER (WHERE ...) is an aggregate modifier, distinct from the unsupported standalone WHERE clause. Co-authored-by: Isaac --- core-spec/expression_language.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core-spec/expression_language.md b/core-spec/expression_language.md index 617aeefd3..800e6cc5e 100644 --- a/core-spec/expression_language.md +++ b/core-spec/expression_language.md @@ -217,7 +217,7 @@ APPROX_PERCENTILE(response_time, 0.95) ### Conditional Aggregations (REQUIRED) SUM / COUNT aggregation functions support `DISTINCT.` -All aggregations should support filtered aggregation: +All aggregations must support filtered aggregation, expressed either with a `CASE` argument or with a postfix `FILTER (WHERE ...)` modifier: ```sql -- DISTINCT modifier @@ -227,7 +227,25 @@ COUNT(DISTINCT customer_id) -- Filtered aggregation via CASE SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END) COUNT(CASE WHEN status = 'completed' THEN 1 END) + +-- Filtered aggregation via FILTER (WHERE ...) +SUM(amount) FILTER (WHERE status = 'completed') +COUNT(*) FILTER (WHERE status = 'completed') +``` + +#### `FILTER (WHERE ...)` (REQUIRED) + +Every aggregate function must support a postfix `FILTER (WHERE )` modifier: + ``` +() FILTER (WHERE ) +``` + +The clause has the semantics of the SQL:2003 `` (optional feature T612): the aggregate considers only the rows for which `` is `TRUE`. + +`FILTER (WHERE ...)` is a modifier on an aggregate expression. It is not the standalone `WHERE` clause listed under [Not Supported in Expressions](#not-supported-in-expressions). The `` must reference only fields of the same dataset as the aggregate's arguments. + +Engines without native `FILTER (WHERE ...)` support MAY lower it to the equivalent `CASE` form. ### Decomposability Reference From 8380cb1dbef2fbc5bde5ff1d4dbe058f5c552702 Mon Sep 17 00:00:00 2001 From: Chris Eubank <108756251+christianeu-db@users.noreply.github.com> Date: Sat, 12 Sep 2026 06:04:14 +0000 Subject: [PATCH 2/5] docs(core-spec): respond to @kayemkim's feedback on FILTER (WHERE) - State that FILTER is applied to the aggregate's input, not a query WHERE, so it never removes output groups. - Add two CASE-lowering examples (value aggregate and COUNT(*)). - Soften " is TRUE" to " succeeds" so engines with truthy (non-strict-TRUE) evaluation are covered. Co-authored-by: Isaac --- core-spec/expression_language.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/core-spec/expression_language.md b/core-spec/expression_language.md index 800e6cc5e..74cdaeed9 100644 --- a/core-spec/expression_language.md +++ b/core-spec/expression_language.md @@ -241,11 +241,19 @@ Every aggregate function must support a postfix `FILTER (WHERE )` mod () FILTER (WHERE ) ``` -The clause has the semantics of the SQL:2003 `` (optional feature T612): the aggregate considers only the rows for which `` is `TRUE`. +The clause has the semantics of the SQL:2003 `` (optional feature T612): the aggregate considers only the rows for which `` succeeds. `FILTER` is applied to the aggregate's input, not as a query `WHERE`, so it never removes output groups. `FILTER (WHERE ...)` is a modifier on an aggregate expression. It is not the standalone `WHERE` clause listed under [Not Supported in Expressions](#not-supported-in-expressions). The `` must reference only fields of the same dataset as the aggregate's arguments. -Engines without native `FILTER (WHERE ...)` support MAY lower it to the equivalent `CASE` form. +Engines without native `FILTER (WHERE ...)` support MAY lower it to the equivalent `CASE` form: + +```sql +-- value aggregate: filter the argument +SUM(amount) FILTER (WHERE status = 'completed') --> SUM(CASE WHEN status = 'completed' THEN amount END) + +-- COUNT(*): filter a constant +COUNT(*) FILTER (WHERE status = 'completed') --> COUNT(CASE WHEN status = 'completed' THEN 1 END) +``` ### Decomposability Reference From 8ba14efb59d090f7449d0fc6b7c4944bfaf12d8d Mon Sep 17 00:00:00 2001 From: Chris Eubank <108756251+christianeu-db@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:25:12 +0000 Subject: [PATCH 3/5] docs(core-spec): address @jbonofre's pre-vote feedback on FILTER (WHERE) - Prohibit rewriting an intrinsic FILTER into a query-level WHERE when it would change group membership (MUST NOT). - Drop ELSE 0 from the pre-existing SUM(CASE ...) example so implementers do not copy the sentinel pattern. - State the CASE lowering explicitly: non-matching rows contribute an implicit NULL (never 0); COUNT(*) lowers to COUNT(CASE WHEN p THEN 1 END). Co-authored-by: Isaac --- core-spec/expression_language.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core-spec/expression_language.md b/core-spec/expression_language.md index 74cdaeed9..e64780db5 100644 --- a/core-spec/expression_language.md +++ b/core-spec/expression_language.md @@ -225,7 +225,7 @@ SUM(DISTINCT amount) COUNT(DISTINCT customer_id) -- Filtered aggregation via CASE -SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END) +SUM(CASE WHEN status = 'completed' THEN amount END) COUNT(CASE WHEN status = 'completed' THEN 1 END) -- Filtered aggregation via FILTER (WHERE ...) @@ -241,7 +241,7 @@ Every aggregate function must support a postfix `FILTER (WHERE )` mod () FILTER (WHERE ) ``` -The clause has the semantics of the SQL:2003 `` (optional feature T612): the aggregate considers only the rows for which `` succeeds. `FILTER` is applied to the aggregate's input, not as a query `WHERE`, so it never removes output groups. +The clause has the semantics of the SQL:2003 `` (optional feature T612): the aggregate considers only the rows for which `` succeeds. `FILTER` is applied to the aggregate's input, not as a query `WHERE`, so it never removes output groups. A converter or engine MUST NOT rewrite an intrinsic `FILTER` predicate into a query-level `WHERE` clause when doing so would change group membership. `FILTER (WHERE ...)` is a modifier on an aggregate expression. It is not the standalone `WHERE` clause listed under [Not Supported in Expressions](#not-supported-in-expressions). The `` must reference only fields of the same dataset as the aggregate's arguments. @@ -255,6 +255,8 @@ SUM(amount) FILTER (WHERE status = 'completed') --> SUM(CASE WHEN status = 'com COUNT(*) FILTER (WHERE status = 'completed') --> COUNT(CASE WHEN status = 'completed' THEN 1 END) ``` +The `CASE` form has no `ELSE`, so non-matching rows contribute an implicit `NULL` (never `0`): value aggregates such as `SUM`, `AVG`, `MIN`, and `MAX` ignore the `NULL`, and `COUNT(*)` becomes `COUNT(CASE WHEN THEN 1 END)`. An `ELSE 0` sentinel would be counted as a real row and would corrupt `AVG`, `MIN`, `MAX`, and `COUNT`. + ### Decomposability Reference For multi-stage aggregation (see [Ossie Analytical Context Extension](https://docs.google.com/document/d/1MKNySGmEv_C6CzBZ7um9Ym3_mMvmOolpDuwPvRzQ1bo/edit?usp=sharing)): From 8fc83676142d97b0ef431e9e2654036f52483ad4 Mon Sep 17 00:00:00 2001 From: Chris Eubank <108756251+christianeu-db@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:02:09 +0000 Subject: [PATCH 4/5] docs(core-spec): respond to review feedback on FILTER (WHERE) - Define FILTER (WHERE ...) by the SQL:2003 semantics only. An engine MAY rewrite it, but the result MUST be identical to those semantics; the spec no longer names a required rewrite target. - Move the CASE lowering into a non-normative note, add a COUNT(DISTINCT ...) example, and state that the identity does not hold for aggregates that do not ignore NULL inputs, such as ARRAY_AGG. - Drop the CASE-or-FILTER wording from the section intro. Co-authored-by: Isaac --- core-spec/expression_language.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core-spec/expression_language.md b/core-spec/expression_language.md index e64780db5..8bca97364 100644 --- a/core-spec/expression_language.md +++ b/core-spec/expression_language.md @@ -217,7 +217,7 @@ APPROX_PERCENTILE(response_time, 0.95) ### Conditional Aggregations (REQUIRED) SUM / COUNT aggregation functions support `DISTINCT.` -All aggregations must support filtered aggregation, expressed either with a `CASE` argument or with a postfix `FILTER (WHERE ...)` modifier: +All aggregations must support filtered aggregation: ```sql -- DISTINCT modifier @@ -245,17 +245,17 @@ The clause has the semantics of the SQL:2003 `` (optional feature `FILTER (WHERE ...)` is a modifier on an aggregate expression. It is not the standalone `WHERE` clause listed under [Not Supported in Expressions](#not-supported-in-expressions). The `` must reference only fields of the same dataset as the aggregate's arguments. -Engines without native `FILTER (WHERE ...)` support MAY lower it to the equivalent `CASE` form: +An engine MAY evaluate `FILTER (WHERE ...)` by rewriting it to an equivalent expression, but the result MUST be identical to the result that the `` semantics define. -```sql --- value aggregate: filter the argument -SUM(amount) FILTER (WHERE status = 'completed') --> SUM(CASE WHEN status = 'completed' THEN amount END) +*Note (non-normative):* For an aggregate function that ignores `NULL` inputs, `FILTER (WHERE ...)` is equivalent to a `CASE` expression with no `ELSE`: --- COUNT(*): filter a constant -COUNT(*) FILTER (WHERE status = 'completed') --> COUNT(CASE WHEN status = 'completed' THEN 1 END) +```sql +SUM(amount) FILTER (WHERE status = 'completed') --> SUM(CASE WHEN status = 'completed' THEN amount END) +COUNT(*) FILTER (WHERE status = 'completed') --> COUNT(CASE WHEN status = 'completed' THEN 1 END) +COUNT(DISTINCT customer_id) FILTER (WHERE status = 'completed') --> COUNT(DISTINCT CASE WHEN status = 'completed' THEN customer_id END) ``` -The `CASE` form has no `ELSE`, so non-matching rows contribute an implicit `NULL` (never `0`): value aggregates such as `SUM`, `AVG`, `MIN`, and `MAX` ignore the `NULL`, and `COUNT(*)` becomes `COUNT(CASE WHEN THEN 1 END)`. An `ELSE 0` sentinel would be counted as a real row and would corrupt `AVG`, `MIN`, `MAX`, and `COUNT`. +A non-matching row contributes `NULL`, which these functions ignore. An `ELSE 0` would count as a real row and would corrupt `AVG`, `MIN`, `MAX`, and `COUNT`. The identity does not hold for an aggregate function that does not ignore `NULL` inputs, such as `ARRAY_AGG`. ### Decomposability Reference From 9cad5415d916875f84803ce8e718ced453d477b6 Mon Sep 17 00:00:00 2001 From: Chris Eubank <108756251+christianeu-db@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:41:19 +0000 Subject: [PATCH 5/5] docs(core-spec): reference optional feature T612 in Standards Reference Co-authored-by: Isaac --- core-spec/expression_language.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core-spec/expression_language.md b/core-spec/expression_language.md index 8bca97364..0f108ebad 100644 --- a/core-spec/expression_language.md +++ b/core-spec/expression_language.md @@ -62,6 +62,8 @@ The core language is based on **ANSI SQL:2003 Core** (ISO/IEC 9075-2:2003), sele - Well-defined semantics - Support for modern analytical features (window functions, CTEs) +The language also requires optional SQL:2003 feature T612, the aggregate `` (`FILTER (WHERE ...)`), which is not part of Core. See [Conditional Aggregations](#conditional-aggregations-required). + ### Namespacing and Identifier Resolution The identifiers will match standard SQL identifiers: