adriangb commented on issue #4610:
URL: https://github.com/apache/datafusion/issues/4610#issuecomment-5611065807

   This still reproduces on 55.0.0 and on current `main` (`40488988`, today). 
Adding a reproducer, the current error text, and the mechanism, since this 
issue has none of them.
   
   There is also a second problem alongside the missing check, which the title 
does not cover: the error the user actually gets.
   
   A window function used as a `HAVING` predicate is accepted by the SQL 
planner and by logical planning, and then fails during physical planning.
   
   Two separate problems:
   
   1. **The query should be rejected at planning time.** `HAVING` is evaluated 
before window functions are computed, so a window function cannot appear there. 
PostgreSQL rejects it outright with `window functions are not allowed in 
HAVING`. DataFusion instead builds a `Filter` whose predicate contains an 
`Expr::WindowFunction`, which has no physical equivalent, and fails late.
   2. **The error text is unusable.** It is the `Debug` formatting of the 
internal `Expr::WindowFunction`, just under 2,000 characters on one line, 
covering the whole `Signature` coercion table, `WindowFunctionParams` and 
`WindowFrame`. It never mentions `HAVING`, the offending function, or the 
offending column, so nothing in it points the user back at their SQL.
   
   This is narrowly about rejecting an illegal placement, not about supporting 
anything new. The same expression is legal and already works in the `SELECT` 
list, and DataFusion already supports `QUALIFY`, which is the correct clause 
for exactly this predicate and plans and runs correctly. `WHERE` behaves the 
same way as `HAVING`.
   
   ### Reproducer
   
   One `datafusion-cli` session, no data files required:
   
   ```console
   $ datafusion-cli
   DataFusion CLI v55.0.0
   
   > CREATE TABLE t(a INT, b INT) AS VALUES (1, 1), (2, 2);
   0 row(s) fetched.
   
   > SELECT a, count(*) AS cnt, sum(count(*)) OVER () AS total
     FROM t
     GROUP BY a
     HAVING sum(count(*)) OVER () >= 10;
   This feature is not implemented: Physical plan does not support logical 
expression WindowFunction(WindowFunction { fun: AggregateUDF(AggregateUDF { 
inner: Sum { signature: Signature { type_signature: OneOf([Coercible([Exact { 
desired_type: Decimal, encoding_preservation: EncodingPreservation { 
preserve_dictionary: false } }]), Coercible([Implicit { desired_type: 
Native(LogicalType(Native(UInt64), UInt64)), implicit_coercion: 
ImplicitCoercion { allowed_source_types: [Native(LogicalType(Native(UInt8), 
UInt8)), Native(LogicalType(Native(UInt16), UInt16)), 
Native(LogicalType(Native(UInt32), UInt32))], default_casted_type: UInt64 }, 
encoding_preservation: EncodingPreservation { preserve_dictionary: false } }]), 
Coercible([Implicit { desired_type: Native(LogicalType(Native(Int64), Int64)), 
implicit_coercion: ImplicitCoercion { allowed_source_types: 
[Native(LogicalType(Native(Int8), Int8)), Native(LogicalType(Native(Int16), 
Int16)), Native(LogicalType(Native(Int32), Int32))], default_cas
 ted_type: Int64 }, encoding_preservation: EncodingPreservation { 
preserve_dictionary: false } }]), Coercible([Implicit { desired_type: 
Native(LogicalType(Native(Float64), Float64)), implicit_coercion: 
ImplicitCoercion { allowed_source_types: [Float], default_casted_type: Float64 
}, encoding_preservation: EncodingPreservation { preserve_dictionary: false } 
}]), Coercible([Exact { desired_type: Duration, encoding_preservation: 
EncodingPreservation { preserve_dictionary: false } }]), Coercible([Exact { 
desired_type: Interval, encoding_preservation: EncodingPreservation { 
preserve_dictionary: false } }])]), volatility: Immutable, parameter_names: 
None } } }), params: WindowFunctionParams { args: [Column(Column { relation: 
None, name: "count(Int64(1))" })], partition_by: [], order_by: [], 
window_frame: WindowFrame { units: Rows, start_bound: Preceding(UInt64(NULL)), 
end_bound: Following(UInt64(NULL)), is_causal: false }, filter: None, 
null_treatment: None, distinct: false } })
   ```
   
   That is a single 1,981-character line. The shortest form of the same 
failure, with a nullary window function, is still 526 characters and equally 
uninformative:
   
   ```console
   > SELECT a FROM t GROUP BY a HAVING row_number() OVER () = 1;
   This feature is not implemented: Physical plan does not support logical 
expression WindowFunction(WindowFunction { fun: WindowUDF(WindowUDF { inner: 
RowNumber { signature: Signature { type_signature: Nullary, volatility: 
Immutable, parameter_names: None } } }), params: WindowFunctionParams { args: 
[], partition_by: [], order_by: [], window_frame: WindowFrame { units: Rows, 
start_bound: Preceding(UInt64(NULL)), end_bound: Following(UInt64(NULL)), 
is_causal: false }, filter: None, null_treatment: None, distinct: false } })
   ```
   
   Variants, all one-liners against the same table:
   
   | Query | Result |
   |---|---|
   | `SELECT a, count(*) AS cnt, sum(count(*)) OVER () AS total FROM t GROUP BY 
a HAVING sum(count(*)) OVER () >= 10;` | fails at physical planning |
   | `SELECT a FROM t GROUP BY a HAVING row_number() OVER () = 1;` | fails at 
physical planning |
   | `SELECT a FROM t WHERE row_number() OVER () = 1;` | fails at physical 
planning |
   | `SELECT a, count(*) AS cnt, sum(count(*)) OVER () AS total FROM t GROUP BY 
a;` | works (correct: window over aggregate in the projection) |
   | `SELECT a, count(*) AS cnt, sum(count(*)) OVER () AS total FROM t GROUP BY 
a QUALIFY sum(count(*)) OVER () >= 2;` | works (correct: `QUALIFY` is the 
clause for this) |
   | `SELECT * FROM (SELECT a, count(*) AS cnt, sum(count(*)) OVER () AS total 
FROM t GROUP BY a) s WHERE total >= 2;` | works (correct: filter in an outer 
query) |
   
   `EXPLAIN` cannot show the accepted plan: it fails the same way even with 
`datafusion.explain.logical_plan_only = true`.
   
   ### Expected behavior
   
   The query should fail while the logical plan is built, with a message that 
names the problem. PostgreSQL 17.11, for comparison:
   
   ```
   postgres=# SELECT a, count(*) AS cnt, sum(count(*)) OVER () AS total FROM t 
GROUP BY a HAVING sum(count(*)) OVER () >= 10;
   ERROR:  window functions are not allowed in HAVING
   LINE 1: ...unt(*)) OVER () AS total FROM t GROUP BY a HAVING sum(count(...
                                                                ^
   
   postgres=# SELECT a FROM t WHERE row_number() OVER () = 1;
   ERROR:  window functions are not allowed in WHERE
   LINE 1: SELECT a FROM t WHERE row_number() OVER () = 1;
                                 ^
   ```
   
   DataFusion already produces exactly this kind of message for the 
neighbouring illegal-nesting case, so the target is well established in the 
codebase:
   
   ```console
   > SELECT sum(sum(1));
   Error during planning: Aggregate function calls cannot be nested: 
'sum(Int64(1))' is nested inside 'sum(sum(Int64(1)))'
   ```
   
   Something along the lines of `Error during planning: Window function calls 
are not allowed in HAVING: 'sum(count(*)) ROWS BETWEEN UNBOUNDED PRECEDING AND 
UNBOUNDED FOLLOWING'` would be equally actionable here. Since DataFusion 
already supports `QUALIFY`, pointing at it in a `Diagnostic` help message would 
make the fix self-explanatory.
   
   The legal cases in the table above must keep working, in particular 
`QUALIFY` and a window function over an aggregate in the projection.
   
   ### Mechanism and prior art
   
   Versions tested: `datafusion-cli` 55.0.0 (latest release) and `main` at 
`40488988`, which fail identically; also 54.0.0, which fails the same way. 
PostgreSQL 17.11 for the comparison above.
   
   Mechanism, as far as I traced it:
   
   - The `HAVING` predicate becomes a `Filter` above the `Aggregate` 
(`select_to_plan`, `datafusion/sql/src/select.rs`).
   - `Filter::try_new_internal` (`datafusion/expr/src/logical_plan/plan.rs`) 
validates only that the predicate's type is boolean, so a predicate containing 
`Expr::WindowFunction` is accepted.
   - `DefaultPhysicalPlanner` then calls `create_physical_expr` on that 
predicate. `Expr::WindowFunction` has no physical-expression equivalent, so it 
falls through to the catch-all `not_impl_err!("Physical plan does not support 
logical expression {other:?}")` in `datafusion/physical-expr/src/planner.rs`, 
which is where the `Debug` dump comes from.
   
   There is prior art in the codebase for the shape a fix could take. #23813 
added a crate-private `check_aggregate_and_window_nesting` in 
`datafusion/expr/src/utils.rs`, called from `Aggregate::try_new` and 
`Window::try_new`, which returns a `plan_err!` naming both expressions and 
carries a `Diagnostic` with a span into the original SQL. That check is about 
one call nested inside another, so it does not cover a window call appearing in 
a `Filter` predicate. An equivalent placement check in `Filter::try_new` would 
cover both the SQL path and the `DataFrame`/`LogicalPlanBuilder` path.
   


-- 
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