sunchao commented on code in PR #24767:
URL: https://github.com/apache/datafusion/pull/24767#discussion_r3963773684


##########
datafusion/common/src/utils/mod.rs:
##########
@@ -603,9 +601,31 @@ impl SingleRowListArrayBuilder {
 
     /// Build a single element [`LargeListArray`]
     pub fn build_large_list_array(self) -> LargeListArray {
+        self.build_generic_list_array()
+    }
+
+    fn build_generic_list_array<OffsetSize: OffsetSizeTrait>(
+        self,
+    ) -> GenericListArray<OffsetSize> {
         let (field, arr) = self.into_field_and_arr();
         let offsets = OffsetBuffer::from_lengths([arr.len()]);
-        LargeListArray::new(field, offsets, arr, None)
+
+        // `is_nullable` is conservative for encoded arrays and can be true
+        // even when the array contains no logical nulls. In that case the
+        // generic constructor rejects a valid non-nullable list child.
+        if !field.is_nullable() && arr.is_nullable() && 
arr.logical_null_count() == 0 {
+            let data = ArrayData::builder(
+                GenericListArray::<OffsetSize>::DATA_TYPE_CONSTRUCTOR(field),
+            )
+            .len(1)
+            .add_buffer(offsets.into_inner().into_inner())
+            .add_child_data(arr.to_data())
+            .build()
+            .expect("single-row list array should contain valid data");
+            return GenericListArray::from(data);

Review Comment:
   ### [P2] Handle encoded nullability in downstream list operations
   
   This preserves a dictionary child containing unused null entries. The 
resulting list passes `ArrayData::validate_full()`, but Arrow's `concat` and 
`take` constructors still reject its non-nullable element field.
   
   I reproduced this with `Dictionary<Int8, Utf8>` keys `[0, 0, 0, 0]`, 
dictionary values `[Some("a"), None]`, group keys `[0, 0, 1, 1]`, and row IDs 
`[0, 1, 2, 3]`. Both queries fail on this head:
   
   ```sql
   SELECT collect_list(x) FROM t GROUP BY g ORDER BY g;
   SELECT collect_list(x) OVER (
     ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
   ) FROM t ORDER BY id;
   ```
   
   The error is `Non-nullable field of ListArray "item" cannot contain nulls`. 
Grouped/window output concatenates list scalars through 
`ScalarValue::iter_to_array`, reaching the conservative constructor again. Both 
queries pass with merge-base production code; the corresponding `collect_set` 
controls pass on both versions.
   
   Please reconcile encoded-child nullability beyond this constructor and add 
grouped/window regression coverage. The existing global-aggregation test 
produces only one scalar and misses this path.



##########
datafusion/spark/src/function/aggregate/collect.rs:
##########
@@ -180,27 +234,49 @@ impl<T: Accumulator> NullToEmptyListAccumulator<T> {
     pub fn new(inner: T, list_type: DataType) -> Self {
         Self { inner, list_type }
     }
+
+    fn normalize_input(&self, value: &ArrayRef) -> Result<ArrayRef> {
+        let DataType::List(field) = &self.list_type else {
+            return internal_err!(
+                "collect_list/collect_set expected List return type, got {:?}",
+                self.list_type
+            );
+        };
+        if value.data_type() == field.data_type() {
+            Ok(Arc::clone(value))
+        } else {
+            Ok(cast(value.as_ref(), field.data_type())?)
+        }
+    }
 }
 
 impl<T: Accumulator> Accumulator for NullToEmptyListAccumulator<T> {
     fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
-        self.inner.update_batch(values)
+        let [value] = values else {
+            return self.inner.update_batch(values);
+        };
+        let value = self.normalize_input(value)?;
+        self.inner.update_batch(&[value])

Review Comment:
   ### [P2] Normalize retractions consistently with updates
   
   For the declared-nonnullable/runtime-nullable nested Struct scenario covered 
by this PR, this update path initializes `collect_set`'s `RowConverter` with 
the normalized schema. `retract_batch` still forwards the original runtime 
array.
   
   Using declared `Struct(required: Int32 non-null)` and runtime 
`Struct(required: Int32 nullable)` containing `[1, 2]`, `update_batch` succeeds 
but retracting the first original row now fails:
   
   ```text
   RowConverter column schema mismatch, expected Struct("required": non-null 
Int32) got Struct("required": Int32)
   ```
   
   The identical update/retract probe passes on the merge base. Sliding windows 
pass slices of the original input to both operations, so this breaks retraction 
when the downstream runtime/declared schema mismatch occurs. This reproduction 
is at the accumulator API; I have not established a native SQL producer of that 
mismatch.
   
   Please apply equivalent normalization during retraction and add an 
update/retract round-trip to the nested-schema regression coverage.



##########
datafusion/spark/src/function/aggregate/collect.rs:
##########
@@ -180,27 +234,49 @@ impl<T: Accumulator> NullToEmptyListAccumulator<T> {
     pub fn new(inner: T, list_type: DataType) -> Self {
         Self { inner, list_type }
     }
+
+    fn normalize_input(&self, value: &ArrayRef) -> Result<ArrayRef> {
+        let DataType::List(field) = &self.list_type else {
+            return internal_err!(
+                "collect_list/collect_set expected List return type, got {:?}",
+                self.list_type
+            );
+        };
+        if value.data_type() == field.data_type() {
+            Ok(Arc::clone(value))
+        } else {
+            Ok(cast(value.as_ref(), field.data_type())?)

Review Comment:
   ### [P2] Drop ignored null rows before narrowing nested types
   
   This cast processes rows before the inner accumulator drops null inputs. In 
the runtime/declared nested-nullability mismatch scenario, it can reject the 
backing payload of a row that both collect aggregates must ignore.
   
   A concrete input has declared element type `List<non-null Int32>` and 
runtime type `List<nullable Int32>`, with offsets `[0, 1, 2]`, child values 
`[NULL, 1]`, and validity `[false, true]`. Its logical rows are `[NULL, [1]]`; 
the null list legally retains a null backing child, and the input passes 
`ArrayData::validate_full()`.
   
   Both `collect_list` and `collect_set` now fail during `update_batch` with 
`Non-nullable field of ListArray "item" cannot contain nulls`. The identical 
accumulator probes return `[[1]]` with merge-base production code. There is no 
null in the retained nested value.
   
   Please remove ignored null rows before narrowing nested nullability, and 
cover this masked-payload case for both aggregates.



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