Define filter (where ...) clause - #203
Conversation
Came up as part of #134
gaborcsardi
left a comment
There was a problem hiding this comment.
Mostly straightforward, at least at the spec level.
I think we don't actually need to make FILTER and WHERE a reserved word, but I guess we might as well.
The interaction with COLUMNS() is not explicitly defined, we should probably say something about that.
We'll need to add some check spec to validation.md for FILTER.
| expr: SUM(order_total) FILTER (WHERE status_cd = 90) | ||
| ``` | ||
|
|
||
| The condition is a `row`-shape boolean expression over the same table, and only rows where it is true are folded; following [CHECK semantics](#truth-and-null), a row whose condition is null is excluded, exactly as `false` is. A filter that admits no rows leaves the aggregate with [empty input](#empty-input), so `SUM(x) FILTER (WHERE false)` is null and `COUNT(x) FILTER (WHERE false)` is 0. |
There was a problem hiding this comment.
Small contradiction: WHERE false is actually not row-shape.
|
|
||
| #### Filtering: `FILTER (WHERE ...)` {#filter} | ||
|
|
||
| Any aggregate may be narrowed to a subset of rows with a `FILTER (WHERE ...)` clause, written after the closing parenthesis: |
There was a problem hiding this comment.
I guess FILTER and WHERE are case insensitive. Also, we need to add them as reserved words?
| expr: SUM(order_total) FILTER (WHERE status_cd = 90) | ||
| ``` | ||
|
|
||
| The condition is a `row`-shape boolean expression over the same table, and only rows where it is true are folded; following [CHECK semantics](#truth-and-null), a row whose condition is null is excluded, exactly as `false` is. A filter that admits no rows leaves the aggregate with [empty input](#empty-input), so `SUM(x) FILTER (WHERE false)` is null and `COUNT(x) FILTER (WHERE false)` is 0. |
There was a problem hiding this comment.
This is not CHECK semantics, I think. it is WHERE and HAVING semantics in SQL. CHECK semantics allows nulls.
|
You're imagining that you could use Although that does not do what I expect in duckdb (from claude) CREATE TABLE t AS SELECT * FROM (VALUES (1,10,100),(2,NULL,200),(3,30,NULL)) v(x,a,b);
SELECT sum(x) FILTER (WHERE COLUMNS(*) IS NOT NULL) FROM t;
-- ┌───┬───┬───┐
-- │ x │ a │ b │
-- ├───┼───┼───┤
-- │ 6 │ 4 │ 3 │ |
|
I think we can make So technically nothing is needed, although an example may be still nice. |
The clause parses on any call, as the grammar admits; the checker then requires an aggregate (S21), a boolean condition (S21), and no aggregate inside the condition (S30) — it is evaluated per row, before any fold has finished. The IR carries the lowered filter, eval folds only rows the condition keeps (null excludes, as false does), and both emitters refuse it as Unsupported until translation is implemented.
#Conflicts: # crates/data-dict/src/emit/mod.rs # crates/data-dict/src/emit/r_tidyverse.rs
|
@gaborcsardi now with implementation |
| Any aggregate may be narrowed to a subset of rows with a `FILTER (WHERE ...)` clause, written after the closing parenthesis: | ||
|
|
||
| ```yaml | ||
| - name: net_revenue |
| expr: SUM(order_total) FILTER (WHERE status_cd = 90) | ||
| ``` | ||
|
|
||
| You can use `COLUMNS()` in the filter clause, with its usual interpretation of `AND`ing together the results from all selected columns: |
There was a problem hiding this comment.
I think the example is wrong here, it does not do what you want if you AND together the results from all selected columns. We could be more explicit and say that the aggregate is folded once per selected column, and use a different example. E.g:
`COLUMNS()` may appear in the filter clause, and [the usual rule](#selecting-multiple-columns) applies: the whole expression — the fold included — is evaluated once per selected column, so the `AND` sits outside the aggregate, not inside the condition.
- assert: AVG(qty) FILTER (WHERE COLUMNS('flag_.*')) > 0
description: Mean quantity is positive among the rows each flag marks.
| Some(filter) => { | ||
| cx.push("sum("); | ||
| cx.free(filter)?; | ||
| cx.push(", na.rm = TRUE)"); |
There was a problem hiding this comment.
This is going to break on constant conditions, e.g. ROW_COUNT() FILTER (WHERE TRUE) is translated to sum(TRUE, na.rm = TRUE), which is always 1.
I guess we'd need to add a rep_len() with length n() / .N here, and it would be unsupported in base R.
Op::RowCount => match filter {
Some(f) if f.shape == Shape::Const => {
// A constant condition recycles, so it needs the row count to
// count against.
cx.push("sum(rep_len(");
cx.free(f)?;
match d {
Dialect::Tidyverse => cx.push(", n())"),
Dialect::DataTable => cx.push(", .N)"),
Dialect::Base => return Err(Unsupported { .. }),
}
cx.push(", na.rm = TRUE)");
}
Some(f) => { /* current code */ }
None => { /* current code */ }
}
| | S19 | Malformed assertion | E | An `assert` expression fails to parse (a syntax error in the [expression language](expressions.md)). | | ||
| | S20 | Unknown assertion column | E | An `assert` expression, or a `COLUMNS([...])` list, references a column not present on the table, or a field access names a field not declared on its `struct`. | | ||
| | S21 | Ill-typed assertion | E | An `assert` expression is syntactically valid but semantically wrong: an operator or function applied to the wrong operand type (including a column a `COLUMNS(...)` selects, and an argument outside a signature's [type class](expressions.md#type-classes), such as `SUM` of a string), a wrong function arity, a non-boolean top-level expression, more than one `COLUMNS(...)`, a malformed `SIMILAR TO` / `COLUMNS('...')` regex, a field access on anything but a `struct` (including through a `list`), or a bare `struct` or `list` column used where a value is needed (anywhere but `IS [NOT] NULL` and `COUNT`, which ask only whether a value is null). | | ||
| | S21 | Ill-typed assertion | E | An `assert` expression is syntactically valid but semantically wrong: an operator or function applied to the wrong operand type (including a column a `COLUMNS(...)` selects, and an argument outside a signature's [type class](expressions.md#type-classes), such as `SUM` of a string), a wrong function arity, a non-boolean top-level expression, a `FILTER (WHERE ...)` clause on a non-aggregate function or with a non-boolean condition, more than one `COLUMNS(...)`, a malformed `SIMILAR TO` / `COLUMNS('...')` regex, a field access on anything but a `struct` (including through a `list`), or a bare `struct` or `list` column used where a value is needed (anywhere but `IS [NOT] NULL` and `COUNT`, which ask only whether a value is null). | |
There was a problem hiding this comment.
At least for NOW() FILTER (...) we do not report S21, but the NOW() is parsed and then S19 is reported for the malformed expression. (I am not saying that this is super important, though...)
| | S28 | Invalid type | E | A column's `type` is not a recognised type string. Valid types are the fixed scalars (`string`, `number`, `number(id)`, `number(ordinal)`, `number(quantity)`, `boolean`, `date`, `datetime`), `enum`, `struct`, and `list(element_type)` where the element type is any of the above — including another `list(...)`, nested to any depth. | | ||
| | S29 | Invalid constraint on list or struct | E | A `primary_key`, `foreign_key`, or `unique` constraint appears on a `list` or `struct` column. (There is no such check for fields: a field can't carry `constraints` at all, which the schema enforces structurally.) | | ||
| | S30 | Nested aggregate | E | An aggregate function's argument contains another aggregate call, as in `AVG(MIN(x))`: an aggregate folds one value per row, and another aggregate gives it a single value. This is the only [shape](expressions.md#shapes) an expression can get wrong — every other combination is legal, including mixing a row-level subexpression with an aggregate one (`value <= 2 * MIN(value)`). | | ||
| | S30 | Nested aggregate | E | An aggregate function's argument contains another aggregate call, as in `AVG(MIN(x))`: an aggregate folds one value per row, and another aggregate gives it a single value. The same goes for a `FILTER (WHERE ...)` condition. These are the only ways an expression's [shape](expressions.md#shapes) can be wrong — every other combination is legal, including mixing a row-level subexpression with an aggregate one (`value <= 2 * MIN(value)`). | |
There was a problem hiding this comment.
We need to change the message in validate_spec.rs to reflect this change. Currently it is
"S30" => "An aggregate can't be nested inside another aggregate.",
which does not cover the new failure mode.
Came up as part of #134
This is the equivalent of
sum(order_total[status_cd == 90])