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

   ### Describe the bug
   
   `ScalarValue::new_list` takes a `data_type` parameter but honors it **only 
when `values` is empty**:
   
   ```rust
   // datafusion/common/src/scalar/mod.rs (main)
   pub fn new_list(
       values: &[ScalarValue],
       data_type: &DataType,
       nullable: bool,
   ) -> Arc<ListArray> {
       let values = if values.is_empty() {
           new_empty_array(data_type)
       } else {
           Self::iter_to_array(values.iter().cloned()).unwrap()   // data_type 
ignored
       };
       ...
   }
   ```
   
   For non-empty input the element type comes entirely from the collected 
scalars, so the returned `ListArray` can have an element type that differs from 
the `data_type` the caller asked for. `new_list_nullable` and `new_large_list` 
inherit the same behaviour.
   
   The practical consequence is that an accumulator built on `new_list` cannot 
guarantee the type it advertises. `ArrayAggAccumulator` and 
`DistinctArrayAggAccumulator` pass `self.datatype` to `new_list`, and 
`datafusion-spark`'s `collect_list` / `collect_set` declare their `return_type` 
/ `state_fields` from the *argument's declared type*:
   
   ```rust
   // datafusion/spark/src/function/aggregate/collect.rs (main)
   fn state_fields(&self, args: StateFieldsArgs) -> Result<Vec<FieldRef>> {
       Ok(vec![Field::new_list(
           format_state_name(args.name, "collect_set"),
           Field::new_list_field(args.input_fields[0].data_type().clone(), 
true),
           true,
       ).into()])
   }
   ```
   
   while the accumulator stores `ScalarValue::try_from_array(...)` slices of 
the argument array it actually received. Nothing reconciles the two.
   
   So if an aggregate argument expression's `evaluate()` output type differs 
from its `data_type(schema)` — in our case in the **nullability of a nested 
struct field**, which `DataType::equals_datatype` does compare — then 
`GroupedHashAggregateStream::emit` fails validating its own output batch:
   
   ```
   Invalid argument error: column types must match schema types,
   expected List(Struct("flag": Boolean, "items": List(Struct("n": Int32))))
   but found  List(Struct("flag": non-null Boolean, "items": List(Struct("n": 
non-null Int32))))
   at column index 1
   ```
   
   This hits both emit paths: the normal one (validated against 
`AggregateExec`'s schema, from `return_type`) and the spill one (validated 
against `spill_state.spill_schema`, built from `state_fields()`).
   
   ### To Reproduce
   
   Build `collect_set` over an argument whose declared type has nullable nested 
leaves, and feed the accumulator an array whose actual type has non-nullable 
leaves. `state_fields()[0].data_type()` and the type of `acc.state()[0]` then 
disagree, and constructing the output `RecordBatch` fails with the message 
above.
   
   ### Expected behavior
   
   Either:
   
   1. `new_list` / `new_list_nullable` / `new_large_list` reconcile the 
concatenated values to `data_type` (a `cast_with_options` call, which 
short-circuits to a shallow clone when the types already match), so the 
function's declared contract holds; or
   2. the parameter is documented as advisory and 
`ArrayAggAccumulator::evaluate` / `DistinctArrayAggAccumulator::evaluate` cast 
to `self.datatype` themselves, so an accumulator cannot violate its own 
`state_fields()` / `return_type()`.
   
   (1) seems preferable — it fixes every current and future caller rather than 
two.
   
   ### Additional context
   
   Downstream, this shows up in Apache DataFusion Comet, which has to 
pre-normalize `collect_list` / `collect_set` arguments with an explicit cast to 
work around it.
   
   Separately, and more broadly: there is currently no invariant check that a 
`PhysicalExpr`'s `evaluate()` output type matches its `data_type(schema)`. A 
debug-only assertion (in `ProjectionExec` / aggregate argument evaluation, 
alongside the existing `ExecutionPlan` invariant checks) would surface this 
class of bug at the drifting expression rather than several operators 
downstream in a spill 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