comphead commented on PR #22134:
URL: https://github.com/apache/datafusion/pull/22134#issuecomment-5111426508

   I made an initial review with assistance of LLM and it comes up with some 
high severity issues that needed to be checked
   
   ### `LEAD ... IGNORE NULLS` with expression default returns default for 
every row
   
     `datafusion/functions-window/src/lead_lag.rs`, new 
`evaluate_all_with_ignore_null_and_array_default` (diff lines 220-262).
   
     `shift_offset` for `LEAD` is negative `i64`. Both arms do 
`pos.checked_add(offset as usize)`. Casting a negative `i64` to `usize` wraps 
to `usize::MAX`, `checked_add` overflows to `None`, and every row falls
     back to `default_values[id]`.
   
     The `Err(pos)` arm is also off-by-one for `LEAD` independent of the sign 
bug: it needs `pos + k - 1` (the null's insertion point already points at the 
first at-or-after valid), while `Ok(pos)` needs `pos + 
     k`.
   
     ```sql
     SELECT lead(a, 1, b) IGNORE NULLS OVER (ORDER BY id)
     FROM (VALUES (1,1,100),(2,NULL,200),(3,3,300)) AS t(id,a,b);
     -- returns [100, 200, 300] instead of [3, 3, 300]
     ```
   
     ---
     
     ### `.unwrap()` in `WindowUDFImpl::expressions()` can panic
   
     `datafusion/functions-window/src/lead_lag.rs`, diff lines 112-117.
   
     ```rust
     let main_expr = parse_expr(...).unwrap();
     let main_expr_nullable = 
expr_args.input_fields().first().unwrap().data_type().is_null();
     ```
   
     `parse_expr` returns `Result` and `.first()` returns `Option`. A caller 
with empty `input_fields` (optimizer rule rebuilding the plan) panics the 
executor thread.
   
     ---
     
     ### `expressions()` materializes literal defaults on every batch
   
     `datafusion/functions-window/src/lead_lag.rs`, diff lines 118-141.
     
     `expressions()` pushes `input_exprs[2]` (or a `CastExpr` wrapping it) 
regardless of whether it is a `Literal`. Downstream 
`evaluate_expressions_to_arrays` calls `into_array_of_size(num_rows)` for every
     expression. In `evaluate_all`, the `DefaultValue::Literal` arm then 
ignores `values[1]`.
   
     `lag(x, 1, 0)` over a 10M-row partition allocates an extra 10M-element 
array on every batch and discards it. Regression on the most common form of 
`LAG`/`LEAD`.
   
     ---
     
     ###  Silent fallbacks mask invariant breaks
   
     `datafusion/functions-window/src/lead_lag.rs`, diff lines 305-316 and 
353-356.
   
     ```rust
     // evaluate_all
     let default_array = values.get(1).cloned()
         .unwrap_or_else(|| 
Arc::new(arrow::array::NullArray::new(value.len())));
   
     // evaluate
     values.get(1).map(|defaults| { ... })
         .unwrap_or_else(|| ScalarValue::try_from(array.data_type()))
     ```
   
     Both paths silently emit NULLs when `DefaultValue::Expression` disagrees 
with `values.len()`. Wrong answer instead of an error.
   
     ---
     
     ### Test coverage gaps
   
     `datafusion/sqllogictest/test_files/window.slt`, diff lines 415-742. 
Missing cases:
   
     - `LAG`/`LEAD ... IGNORE NULLS` with a per-row expression default (would 
immediately expose finding #1).
     - Per-row default that is `NULL` for some rows.
     - Main and default with different `DataType`s (exercises `CastExpr` 
insertion at diff line 134-138).
     - Reversed execution (`ORDER BY <col> DESC`).
     - Empty partition.


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