adriangb opened a new issue, #26183:
URL: https://github.com/apache/datafusion/issues/26183

   ### Describe the bug
   
   An aggregate with a `FILTER` clause evaluates its arguments only for the 
rows that pass the filter.
   Thus a fallible argument, such as a cast or a division, does not fail on a 
rejected row.
   
   This stops working when two aggregates in the same query have the same 
argument expression.
   The `CommonSubexprEliminate` optimizer rule moves the shared argument into a 
projection below the aggregate.
   The projection evaluates the expression for every row, and that includes the 
rows that the filter rejects.
   The query then fails with an error from a row that must not take part in the 
aggregate.
   
   ### To Reproduce
   
   In `datafusion-cli`:
   
   ```sql
   CREATE TABLE t AS
   SELECT * FROM (VALUES (CAST(-950 AS SMALLINT), 0), (CAST(5 AS SMALLINT), 1)) 
AS v(x, y);
   
   -- One aggregate: succeeds and returns 5.0.
   SELECT avg(arrow_cast(x, 'UInt64')) FILTER (WHERE y > 0) AS a FROM t;
   
   -- Two aggregates with the same argument: fails.
   SELECT
       sum(arrow_cast(x, 'UInt64')) FILTER (WHERE y > 0) AS s,
       count(arrow_cast(x, 'UInt64')) FILTER (WHERE y > 0) AS c
   FROM t;
   ```
   
   Actual result of the second query:
   
   ```
   Failed to cast field 'x' from Int16 to UInt64
   caused by
   Arrow error: Cast error: Can't cast value -950 to type UInt64
   ```
   
   `EXPLAIN` (with `SET datafusion.explain.format = 'indent'`) shows the cast 
in a projection below the aggregate, as `__common_expr_1`:
   
   ```
   logical_plan
   Projection: sum(arrow_cast(t.x,Utf8("UInt64"))) FILTER (WHERE t.y > 
Int64(0)) AS s, count(arrow_cast(t.x,Utf8("UInt64"))) FILTER (WHERE t.y > 
Int64(0)) AS c
     Aggregate: groupBy=[[]], aggr=[[sum(__common_expr_1) FILTER (WHERE 
__common_expr_2) AS sum(arrow_cast(t.x,Utf8("UInt64"))) FILTER (WHERE t.y > 
Int64(0)), count(__common_expr_1) FILTER (WHERE __common_expr_2) AS 
count(arrow_cast(t.x,Utf8("UInt64"))) FILTER (WHERE t.y > Int64(0))]]
       Projection: CAST(t.x AS UInt64) AS __common_expr_1, t.y > Int64(0) AS 
__common_expr_2
         TableScan: t projection=[x, y]
   physical_plan
   ProjectionExec: expr=[sum(arrow_cast(t.x,Utf8("UInt64"))) FILTER (WHERE t.y 
> Int64(0))@0 as s, count(arrow_cast(t.x,Utf8("UInt64"))) FILTER (WHERE t.y > 
Int64(0))@1 as c]
     AggregateExec: mode=Single, gby=[], aggr=[sum(__common_expr_1) FILTER 
(WHERE __common_expr_2) as ..., count(__common_expr_1) FILTER (WHERE 
__common_expr_2) as ...]
       ProjectionExec: expr=[CAST(x@0 AS UInt64) as __common_expr_1, y@1 > 0 as 
__common_expr_2]
         DataSourceExec: partitions=1, partition_sizes=[1]
   ```
   
   The same failure occurs with `GROUP BY y`, and with any other fallible 
argument, for example `sum(10 / v) FILTER (WHERE v <> 0), count(10 / v) FILTER 
(WHERE v <> 0)`.
   
   ### Expected behavior
   
   The second query returns one row:
   
   ```
   +---+---+
   | s | c |
   +---+---+
   | 5 | 1 |
   +---+---+
   ```
   
   A subexpression that occurs only in the arguments of aggregates with a 
`FILTER` stays in the aggregates.
   
   ### Additional context
   
   Versions: `datafusion-cli` 54.0.0, and `main` at 334f29317c (reports 
`DataFusion CLI v55.1.0`).
   
   Suspected cause: `CommonSubexprEliminate` already does not extract a 
subexpression that is only conditionally evaluated, such as a `CASE` branch or 
the right side of `AND` / `OR`.
   `ExprCSEController::conditional_children` in 
`datafusion/optimizer/src/common_subexpr_eliminate.rs` makes that decision, and 
it has no case for `Expr::AggregateFunction` with a `filter`.
   The rule thus counts the arguments of a filtered aggregate as always 
evaluated.
   
   The executor side of this contract is deliberate.
   The ungrouped aggregate path filters the batch before it evaluates the 
arguments (`aggregate_batch` in 
`datafusion/physical-plan/src/aggregates/aggregate_stream.rs`).
   The grouped path does the same since #24444 (issue #24443), with tests in 
`aggregate_filter_selection.slt`.
   The optimizer rule undoes that guarantee.
   
   Related: #24507 and #24508 cover the window function executor, which still 
evaluates the arguments before the `FILTER`.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to