This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-23775-dd1ebde2d312a81ab42cbc8f2f3e3ecc53e5c97c in repository https://gitbox.apache.org/repos/asf/datafusion.git
commit e084034f7c45c5e8e38d7a2a619369a43e6cf347 Author: Ben Chambers <[email protected]> AuthorDate: Thu Jul 23 13:28:27 2026 -0700 fix: array_any_value returns NULL for empty list elements (#23775) ## Which issue does this PR close? - Closes #23773. ## Rationale for this change `array_any_value` reads the wrong value (or panics) when its input list column contains a non-null **empty** (length-0) element. `general_array_any_value` guards null and all-null elements, but a non-null empty element falls through to the no-nulls branch, which unconditionally reads `values[start]`: - **interior empty list** → reads `values[start]`, i.e. the *next* element's value (silently wrong data) - **trailing empty list** (`start == values.len()`) → out-of-bounds slice → panic `range end index N out of range for slice of length N-1` The panic is easy to trigger in practice when the `array_any_value` output flows into a hash `RepartitionExec` (e.g. the value is used as an equi-join key): repartitioning slices batches so an empty element can land at the end of a values buffer, tripping the out-of-bounds read on a spawned task. ## What changes are included in this PR? Guard the empty case explicitly in `general_array_any_value` — an empty list has no value to take, so the result is `NULL`. Sibling functions in `extract.rs` were audited and are already safe: `array_element` bounds-checks the index against `len`; `array_slice` / `array_pop_front` / `array_pop_back` guard `len == 0`. ## Are these changes tested? Yes: - Kernel-level regression tests for `general_array_any_value`: an interior empty element (previously wrong value) and a trailing empty element (previously panic). - `array_any_value.slt` cases covering `List` and `LargeList` with interior and trailing empty elements. ## Are there any user-facing changes? `array_any_value` now returns `NULL` for an empty list element instead of returning the next element's value or panicking the query. No API changes. --------- Signed-off-by: Ben Chambers <[email protected]> --- datafusion/functions-nested/src/extract.rs | 12 +++++++-- .../test_files/array/array_any_value.slt | 29 ++++++++++++++++++++++ docs/source/user-guide/sql/scalar_functions.md | 2 +- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/datafusion/functions-nested/src/extract.rs b/datafusion/functions-nested/src/extract.rs index 900b408bff..b1c22822df 100644 --- a/datafusion/functions-nested/src/extract.rs +++ b/datafusion/functions-nested/src/extract.rs @@ -970,7 +970,7 @@ where #[user_doc( doc_section(label = "Array Functions"), - description = "Returns the first non-null element in the array.", + description = "Returns the first non-null element in the array. Returns NULL if the array is empty or NULL.", syntax_example = "array_any_value(array)", sql_example = r#"```sql > select array_any_value([NULL, 1, 2, 3]); @@ -1062,13 +1062,21 @@ where for (row_index, offset_window) in array.offsets().windows(2).enumerate() { let start = offset_window[0]; + let end = offset_window[1]; - // array is null + // the list element is null if array.is_null(row_index) { mutable.try_extend_nulls(1)?; continue; } + // the list element is empty; there is no value to take, so the result + // is NULL. + if start == end { + mutable.try_extend_nulls(1)?; + continue; + } + let row_value = array.value(row_index); match row_value.nulls() { Some(row_nulls_buffer) => { diff --git a/datafusion/sqllogictest/test_files/array/array_any_value.slt b/datafusion/sqllogictest/test_files/array/array_any_value.slt index 6579e88ac7..c8976e8493 100644 --- a/datafusion/sqllogictest/test_files/array/array_any_value.slt +++ b/datafusion/sqllogictest/test_files/array/array_any_value.slt @@ -145,6 +145,35 @@ select array_any_value(make_array(NULL, 1, 2, 3, 4, 5)), array_any_value(column1 1 41 1 51 +# array_any_value with empty (length-0) list elements +# A non-null but empty list must yield NULL, including a trailing empty element +# whose start offset equals the values length +statement ok +create table any_value_empty (id int, tags bigint[]) as values + (1, make_array(10)), + (2, cast(make_array() as bigint[])), + (3, make_array(20, 30)), + (4, cast(make_array() as bigint[])); + +query II +select id, array_any_value(tags) from any_value_empty order by id; +---- +1 10 +2 NULL +3 20 +4 NULL + +query II +select id, array_any_value(arrow_cast(tags, 'LargeList(Int64)')) from any_value_empty order by id; +---- +1 10 +2 NULL +3 20 +4 NULL + +statement ok +drop table any_value_empty; + # make_array with nulls query ??????? select make_array(make_array('a','b'), null), diff --git a/docs/source/user-guide/sql/scalar_functions.md b/docs/source/user-guide/sql/scalar_functions.md index cb748f57d9..a285b9e5f5 100644 --- a/docs/source/user-guide/sql/scalar_functions.md +++ b/docs/source/user-guide/sql/scalar_functions.md @@ -3430,7 +3430,7 @@ any_match(array, predicate) ### `array_any_value` -Returns the first non-null element in the array. +Returns the first non-null element in the array. Returns NULL if the array is empty or NULL. ```sql array_any_value(array) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
