This is an automated email from the ASF dual-hosted git repository.

mbutrovich pushed a commit to branch branch-54
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/branch-54 by this push:
     new fcbc9bda1f [branch-54] Handle nulls in type coercion of higher-order 
UDFs, map_extract, spark array_repeat (backport #23071) (#23629)
fcbc9bda1f is described below

commit fcbc9bda1fd004c349fa70f61f8c8b40448294ca
Author: gstvg <[email protected]>
AuthorDate: Sat Jul 18 08:29:16 2026 -0300

    [branch-54] Handle nulls in type coercion of higher-order UDFs, 
map_extract, spark array_repeat (backport #23071) (#23629)
    
    ## Which issue does this PR close?
    
    - Backport of #23071 (clean cherry-pick of non-merge commits)
    - Closes #23068 .
    
    ## Rationale for this change
    
    Unresolved lambda variables and unspecified placeholders report a
    `DataType::Null` data type, which causes an error when used as argument
    of a function that doesn't handle nulls during type coercion
    
    ## What changes are included in this PR?
    
    Handle null in `coerce_single_list_arg` for higher-order functions,
    `map_extract` and in spark `array_repeat`. All other functions (scalar,
    window, agg and higher-order) that uses user defined type coercion have
    been checked to currently handle null args.
    
    ## Are these changes tested?
    
    Test passing `array_filter` with an unresolved lambda variable to
    `Dataframe::with_column` (which indirectly calls
    `value_fields_with_higher_order_udf_and_lambdas` coercion via
    `Projection::try_new`, `projection_schema`, `exprlist_to_fields`,
    `Expr::to_field` ), and sqllogictests
    
    ## Are there any user-facing changes?
    
    No
---
 datafusion/core/tests/dataframe/mod.rs             | 51 ++++++++++++++++++++--
 datafusion/functions-nested/src/array_any_match.rs | 27 ++----------
 datafusion/functions-nested/src/lambda_utils.rs    |  1 +
 datafusion/functions-nested/src/map_extract.rs     | 10 +++++
 datafusion/spark/src/function/array/repeat.rs      |  8 ++--
 .../test_files/array/array_any_match.slt           |  6 +++
 .../sqllogictest/test_files/array/array_filter.slt |  6 +++
 .../test_files/array/array_transform.slt           |  6 +++
 datafusion/sqllogictest/test_files/map.slt         |  6 +++
 .../test_files/spark/array/array_repeat.slt        |  7 +++
 10 files changed, 98 insertions(+), 30 deletions(-)

diff --git a/datafusion/core/tests/dataframe/mod.rs 
b/datafusion/core/tests/dataframe/mod.rs
index e55a373ada..807487bb88 100644
--- a/datafusion/core/tests/dataframe/mod.rs
+++ b/datafusion/core/tests/dataframe/mod.rs
@@ -39,6 +39,7 @@ use datafusion_functions_aggregate::expr_fn::{
     array_agg, avg, avg_distinct, count, count_distinct, max, median, min, sum,
     sum_distinct,
 };
+use datafusion_functions_nested::expr_fn::{array_filter, array_transform, 
make_array};
 use datafusion_functions_nested::make_array::make_array_udf;
 use datafusion_functions_window::expr_fn::{first_value, lead, row_number};
 use insta::assert_snapshot;
@@ -78,8 +79,8 @@ use datafusion_expr::{
     CreateMemoryTable, CreateView, DdlStatement, Expr, ExprFunctionExt, 
ExprSchemable,
     LogicalPlan, LogicalPlanBuilder, ScalarFunctionImplementation, SortExpr, 
TableType,
     WindowFrame, WindowFrameBound, WindowFrameUnits, WindowFunctionDefinition, 
cast, col,
-    create_udf, exists, in_subquery, lit, out_ref_col, placeholder, 
scalar_subquery,
-    when, wildcard,
+    create_udf, exists, in_subquery, lambda, lambda_var, lit, out_ref_col, 
placeholder,
+    scalar_subquery, when, wildcard,
 };
 use datafusion_physical_expr::Partitioning;
 use datafusion_physical_expr::aggregate::AggregateExprBuilder;
@@ -90,7 +91,9 @@ use datafusion_physical_plan::aggregates::{
     AggregateExec, AggregateMode, PhysicalGroupBy,
 };
 use datafusion_physical_plan::empty::EmptyExec;
-use datafusion_physical_plan::{ExecutionPlan, ExecutionPlanProperties, 
displayable};
+use datafusion_physical_plan::{
+    ExecutionPlan, ExecutionPlanProperties, collect, displayable,
+};
 
 use datafusion::error::Result as DataFusionResult;
 use datafusion::execution::options::JsonReadOptions;
@@ -7062,3 +7065,45 @@ async fn test_grouping_with_alias() -> Result<()> {
 
     Ok(())
 }
+
+#[tokio::test]
+async fn test_unresolved_lambda_variable() -> Result<()> {
+    let plan = table_with_mixed_lists()
+        .await?
+        .with_column(
+            "c",
+            array_transform(
+                make_array(vec![col("list")]),
+                lambda(
+                    ["x"],
+                    array_filter(
+                        lambda_var("x"),
+                        lambda(["y"], lambda_var("y").gt_eq(lit(2))),
+                    ),
+                ),
+            ),
+        )?
+        .select_columns(&["list", "c"])?
+        .into_unoptimized_plan()
+        .resolve_lambda_variables()?
+        .data;
+
+    let session = SessionContext::new();
+    let exec = session.state().create_physical_plan(&plan).await?;
+    let context = session.task_ctx();
+    let results = collect(exec, context).await?;
+
+    let expected = [
+        "+-----------+----------+",
+        "| list      | c        |",
+        "+-----------+----------+",
+        "| [1, 2, 3] | [[2, 3]] |",
+        "|           | []       |",
+        "| []        | [[]]     |",
+        "|           | []       |",
+        "+-----------+----------+",
+    ];
+    assert_batches_eq!(expected, &results);
+
+    Ok(())
+}
diff --git a/datafusion/functions-nested/src/array_any_match.rs 
b/datafusion/functions-nested/src/array_any_match.rs
index a14cc8c260..e248dc95aa 100644
--- a/datafusion/functions-nested/src/array_any_match.rs
+++ b/datafusion/functions-nested/src/array_any_match.rs
@@ -37,6 +37,8 @@ use datafusion_expr::{
 use datafusion_macros::user_doc;
 use std::{fmt::Debug, sync::Arc};
 
+use crate::lambda_utils::coerce_single_list_arg;
+
 make_higher_order_function_expr_and_func!(
     ArrayAnyMatch,
     array_any_match,
@@ -120,30 +122,7 @@ impl HigherOrderUDFImpl for ArrayAnyMatch {
     }
 
     fn coerce_value_types(&self, arg_types: &[DataType]) -> 
Result<Vec<DataType>> {
-        let [list] = arg_types else {
-            return plan_err!(
-                "{} function requires 1 value argument, got {}",
-                self.name(),
-                arg_types.len()
-            );
-        };
-
-        let coerced = match list {
-            DataType::List(_) | DataType::LargeList(_) => list.clone(),
-            DataType::ListView(field) | DataType::FixedSizeList(field, _) => {
-                DataType::List(Arc::clone(field))
-            }
-            DataType::LargeListView(field) => 
DataType::LargeList(Arc::clone(field)),
-            _ => {
-                return plan_err!(
-                    "{} expected a list as first argument, got {}",
-                    self.name(),
-                    list
-                );
-            }
-        };
-
-        Ok(vec![coerced])
+        coerce_single_list_arg(self.name(), arg_types)
     }
 
     fn lambda_parameters(
diff --git a/datafusion/functions-nested/src/lambda_utils.rs 
b/datafusion/functions-nested/src/lambda_utils.rs
index 0f208ce5d2..ce596d0f1b 100644
--- a/datafusion/functions-nested/src/lambda_utils.rs
+++ b/datafusion/functions-nested/src/lambda_utils.rs
@@ -65,6 +65,7 @@ pub(crate) fn coerce_single_list_arg(
             DataType::List(Arc::clone(field))
         }
         DataType::LargeListView(field) => 
DataType::LargeList(Arc::clone(field)),
+        DataType::Null => DataType::new_list(DataType::Null, true),
         _ => return plan_err!("{name} expected a list as first argument, got 
{list}"),
     };
 
diff --git a/datafusion/functions-nested/src/map_extract.rs 
b/datafusion/functions-nested/src/map_extract.rs
index aab0d013a4..45dc4b6f10 100644
--- a/datafusion/functions-nested/src/map_extract.rs
+++ b/datafusion/functions-nested/src/map_extract.rs
@@ -105,6 +105,11 @@ impl ScalarUDFImpl for MapExtract {
 
     fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
         let [map_type, _] = take_function_args(self.name(), arg_types)?;
+
+        if map_type.is_null() {
+            return Ok(DataType::Null);
+        }
+
         let map_fields = get_map_entry_field(map_type)?;
         Ok(DataType::List(Arc::new(Field::new_list_field(
             map_fields.last().unwrap().data_type().clone(),
@@ -123,6 +128,10 @@ impl ScalarUDFImpl for MapExtract {
     fn coerce_types(&self, arg_types: &[DataType]) -> Result<Vec<DataType>> {
         let [map_type, _] = take_function_args(self.name(), arg_types)?;
 
+        if map_type.is_null() {
+            return Ok(arg_types.to_vec());
+        }
+
         let field = get_map_entry_field(map_type)?;
         Ok(vec![
             map_type.clone(),
@@ -185,6 +194,7 @@ fn map_extract_inner(args: &[ArrayRef]) -> Result<ArrayRef> 
{
 
     let map_array = match map_arg.data_type() {
         DataType::Map(_, _) => as_map_array(&map_arg)?,
+        DataType::Null => return Ok(Arc::clone(map_arg)),
         _ => return exec_err!("The first argument in map_extract must be a 
map"),
     };
 
diff --git a/datafusion/spark/src/function/array/repeat.rs 
b/datafusion/spark/src/function/array/repeat.rs
index da9b19a768..6effdf9a50 100644
--- a/datafusion/spark/src/function/array/repeat.rs
+++ b/datafusion/spark/src/function/array/repeat.rs
@@ -74,9 +74,11 @@ impl ScalarUDFImpl for SparkArrayRepeat {
 
         // Coerce the second argument to Int64/UInt64 if it's a numeric type
         let second = match second_type {
-            DataType::Int8 | DataType::Int16 | DataType::Int32 | 
DataType::Int64 => {
-                DataType::Int64
-            }
+            DataType::Int8
+            | DataType::Int16
+            | DataType::Int32
+            | DataType::Int64
+            | DataType::Null => DataType::Int64,
             DataType::UInt8 | DataType::UInt16 | DataType::UInt32 | 
DataType::UInt64 => {
                 DataType::UInt64
             }
diff --git a/datafusion/sqllogictest/test_files/array/array_any_match.slt 
b/datafusion/sqllogictest/test_files/array/array_any_match.slt
index 27f2a5339e..37aa47c55a 100644
--- a/datafusion/sqllogictest/test_files/array/array_any_match.slt
+++ b/datafusion/sqllogictest/test_files/array/array_any_match.slt
@@ -103,6 +103,12 @@ SELECT list_any_match([1, 2, 3], x -> x > 2);
 ----
 true
 
+# null arg
+query B
+SELECT array_any_match(NULL, x -> x > 2);
+----
+NULL
+
 statement ok
 drop table t;
 
diff --git a/datafusion/sqllogictest/test_files/array/array_filter.slt 
b/datafusion/sqllogictest/test_files/array/array_filter.slt
index f22cfb2198..9b564c5061 100644
--- a/datafusion/sqllogictest/test_files/array/array_filter.slt
+++ b/datafusion/sqllogictest/test_files/array/array_filter.slt
@@ -204,6 +204,12 @@ SELECT array_transform(array_filter(list, v -> v > 1), v 
-> v * 3) FROM with_nul
 [6]
 NULL
 
+# null arg
+query ?
+SELECT array_filter(NULL, x -> x > 2);
+----
+NULL
+
 statement ok
 drop table t;
 
diff --git a/datafusion/sqllogictest/test_files/array/array_transform.slt 
b/datafusion/sqllogictest/test_files/array/array_transform.slt
index c8c43588c8..5439d74411 100644
--- a/datafusion/sqllogictest/test_files/array/array_transform.slt
+++ b/datafusion/sqllogictest/test_files/array/array_transform.slt
@@ -393,6 +393,12 @@ physical_plan
 02)--ProjectionExec: expr=[text@0 as text, list@1 as list, number@2 as number, 
CASE WHEN number@2 > 30 THEN array_transform(make_array(make_array(list@1)), 
(list) -> array_transform(list@3, (list) -> array_transform(list@4, (v) -> 
number@2 + v@5 + array_element(list@4, 1)))) ELSE 
array_transform(make_array(make_array(list@1)), (list) -> 
array_transform(list@3, (list) -> array_transform(list@4, (v) -> number@2 + 
array_element(list@4, 1)))) END as CASE WHEN t.number > Int64(30) THEN array_ 
[...]
 03)----DataSourceExec: partitions=1, partition_sizes=[1]
 
+# null arg
+query ?
+SELECT array_transform(NULL, x -> x * 2);
+----
+NULL
+
 query error
 select array_transform();
 ----
diff --git a/datafusion/sqllogictest/test_files/map.slt 
b/datafusion/sqllogictest/test_files/map.slt
index 62e70e6080..accf713544 100644
--- a/datafusion/sqllogictest/test_files/map.slt
+++ b/datafusion/sqllogictest/test_files/map.slt
@@ -642,6 +642,12 @@ select map_extract(MAP {1: 1, 2: 2, 3:3}, '1'), 
map_extract(MAP {1: 1, 2: 2, 3:3
 ----
 [1] [1] [1] [NULL] [1]
 
+# null arg
+query ?
+select map_extract(NULL, 'a');
+----
+NULL
+
 # map_extract with columns
 query ???
 select map_extract(column1, 1), map_extract(column1, 5), map_extract(column1, 
7) from map_array_table_1;
diff --git a/datafusion/sqllogictest/test_files/spark/array/array_repeat.slt 
b/datafusion/sqllogictest/test_files/spark/array/array_repeat.slt
index 923e349140..d51767a264 100644
--- a/datafusion/sqllogictest/test_files/spark/array/array_repeat.slt
+++ b/datafusion/sqllogictest/test_files/spark/array/array_repeat.slt
@@ -112,3 +112,10 @@ FROM VALUES
 [[123], [123]]
 [[], []]
 [[NULL], [NULL]]
+
+
+# null count
+query ?
+select array_repeat('a', NULL);
+----
+NULL


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to