Skip to main content

datafusion_functions_nested/
array_any_match.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! [`datafusion_expr::HigherOrderUDF`] definitions for array_any_match function.
19
20use arrow::{
21    array::{Array, BooleanArray, BooleanBuilder},
22    buffer::NullBuffer,
23    datatypes::{DataType, Field, FieldRef},
24};
25use datafusion_common::{Result, plan_err, utils::take_function_args};
26use datafusion_expr::{
27    ColumnarValue, Documentation, HigherOrderFunctionArgs, HigherOrderReturnFieldArgs,
28    HigherOrderSignature, HigherOrderUDFImpl, LambdaParametersProgress, ValueOrLambda,
29    Volatility,
30};
31use datafusion_macros::user_doc;
32use std::{fmt::Debug, sync::Arc};
33
34use crate::lambda_utils::{
35    SingleListLambdaResult, coerce_single_list_arg, evaluate_single_list_predicate,
36};
37
38make_higher_order_function_expr_and_func!(
39    ArrayAnyMatch,
40    array_any_match,
41    array lambda,
42    "returns true if any element in the array satisfies the predicate",
43    array_any_match_higher_order_function
44);
45
46#[user_doc(
47    doc_section(label = "Array Functions"),
48    description = "Returns whether any elements of an array match the given predicate. Returns true if one or more elements match, false if none match (including empty arrays), and null if the predicate returns null for some elements and false for all others.",
49    syntax_example = "any_match(array, predicate)",
50    sql_example = r#"```sql
51> select any_match([1, 2, 3], x -> x > 2);
52+----------------------------------+
53| any_match([1, 2, 3], x -> x > 2) |
54+----------------------------------+
55| true                             |
56+----------------------------------+
57```"#,
58    argument(
59        name = "array",
60        description = "Array expression. Can be a constant, column, or function, and any combination of array operators."
61    ),
62    argument(
63        name = "predicate",
64        description = "Lambda predicate that returns a boolean"
65    )
66)]
67#[derive(Debug, PartialEq, Eq, Hash)]
68pub struct ArrayAnyMatch {
69    signature: HigherOrderSignature,
70    aliases: Vec<String>,
71}
72
73impl Default for ArrayAnyMatch {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79impl ArrayAnyMatch {
80    pub fn new() -> Self {
81        Self {
82            signature: HigherOrderSignature::exact(
83                vec![ValueOrLambda::Value(()), ValueOrLambda::Lambda(())],
84                Volatility::Immutable,
85            ),
86            aliases: vec![String::from("any_match"), String::from("list_any_match")],
87        }
88    }
89}
90
91// Returns Some(true) if any element in [start, end) is true,
92// None if no element is true but some are null,
93// Some(false) if all are false or range is empty.
94fn any_match_for_range(
95    predicate: &BooleanArray,
96    start: usize,
97    end: usize,
98) -> Option<bool> {
99    let any_true = (start..end).any(|j| predicate.is_valid(j) && predicate.value(j));
100    if any_true {
101        return Some(true);
102    }
103    let any_null = (start..end).any(|j| predicate.is_null(j));
104    if any_null { None } else { Some(false) }
105}
106
107impl HigherOrderUDFImpl for ArrayAnyMatch {
108    fn name(&self) -> &str {
109        "array_any_match"
110    }
111
112    fn aliases(&self) -> &[String] {
113        &self.aliases
114    }
115
116    fn signature(&self) -> &HigherOrderSignature {
117        &self.signature
118    }
119
120    fn coerce_value_types(&self, arg_types: &[DataType]) -> Result<Vec<DataType>> {
121        coerce_single_list_arg(self.name(), arg_types)
122    }
123
124    fn lambda_parameters(
125        &self,
126        _step: usize,
127        fields: &[ValueOrLambda<FieldRef, Option<FieldRef>>],
128    ) -> Result<LambdaParametersProgress> {
129        let [list, _] = take_function_args(self.name(), fields)?;
130        let ValueOrLambda::Value(list) = list else {
131            return plan_err!("{} expects a value as first argument", self.name());
132        };
133
134        let field = match list.data_type() {
135            DataType::List(field) => field,
136            DataType::LargeList(field) => field,
137            other => return plan_err!("expected list, got {other}"),
138        };
139
140        Ok(LambdaParametersProgress::Complete(vec![vec![Arc::clone(
141            field,
142        )]]))
143    }
144
145    fn return_field_from_args(
146        &self,
147        args: HigherOrderReturnFieldArgs,
148    ) -> Result<Arc<Field>> {
149        let [ValueOrLambda::Value(list), ValueOrLambda::Lambda(lambda)] =
150            take_function_args(self.name(), args.arg_fields)?
151        else {
152            return plan_err!("{} expects a value as first argument", self.name());
153        };
154        let nullable = list.is_nullable() || lambda.is_nullable();
155        Ok(Arc::new(Field::new("", DataType::Boolean, nullable)))
156    }
157
158    fn invoke_with_args(&self, args: HigherOrderFunctionArgs) -> Result<ColumnarValue> {
159        let evaluated = match evaluate_single_list_predicate(self.name(), &args)? {
160            SingleListLambdaResult::EarlyReturn(v) => return Ok(v),
161            SingleListLambdaResult::Ready(v) => v,
162        };
163
164        let predicate = evaluated.boolean_predicate(self.name())?;
165
166        let mut values = BooleanBuilder::with_capacity(evaluated.len());
167        for i in 0..evaluated.len() {
168            let (start, end) = evaluated.row_range(i);
169            // any_match_for_range returns None when nulls poison the result;
170            // null rows produce an empty range and return Some(false), but their
171            // null bit is preserved by attaching the original null bitmap below.
172            values.append_option(any_match_for_range(&predicate, start, end));
173        }
174
175        let (boolean_buffer, predicate_nulls) = values.finish().into_parts();
176        // Merge: a row is null if the input list row was null or the predicate returned null.
177        let nulls = NullBuffer::union(evaluated.nulls(), predicate_nulls.as_ref());
178        Ok(ColumnarValue::Array(Arc::new(BooleanArray::new(
179            boolean_buffer,
180            nulls,
181        ))))
182    }
183
184    fn documentation(&self) -> Option<&Documentation> {
185        self.doc()
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use std::{collections::HashMap, sync::Arc};
192
193    use arrow::{
194        array::{ArrayRef, BooleanArray, Int32Array, ListArray, RecordBatch},
195        buffer::{NullBuffer, OffsetBuffer},
196        datatypes::{DataType, Field},
197    };
198    use datafusion_common::{DFSchema, Result};
199    use datafusion_expr::{
200        Expr, HigherOrderReturnFieldArgs, HigherOrderUDFImpl, ValueOrLambda, col,
201        execution_props::ExecutionProps,
202        expr::{HigherOrderFunction, LambdaVariable},
203        lambda, lit,
204        physical_planning_context::PhysicalPlanningContext,
205    };
206    use datafusion_physical_expr::create_physical_expr;
207
208    use crate::array_any_match::{ArrayAnyMatch, array_any_match_higher_order_function};
209    use crate::lambda_utils::test_utils::{
210        create_i32_large_list, create_i32_list, eval_hof_on_i32_list,
211        eval_hof_on_i32_list_with_outer, v,
212    };
213
214    fn run_any_match(
215        list: impl arrow::array::Array + Clone + 'static,
216    ) -> Result<ArrayRef> {
217        let schema = DFSchema::from_unqualified_fields(
218            vec![Field::new(
219                "list",
220                list.data_type().clone(),
221                list.is_nullable(),
222            )]
223            .into(),
224            HashMap::new(),
225        )?;
226
227        create_physical_expr(
228            &Expr::HigherOrderFunction(HigherOrderFunction::new(
229                array_any_match_higher_order_function(),
230                vec![
231                    col("list"),
232                    lambda(
233                        ["x"],
234                        Expr::LambdaVariable(LambdaVariable::new(
235                            "x".to_string(),
236                            Some(Arc::new(Field::new("x", DataType::Int32, true))),
237                        ))
238                        .gt(lit(2i32)),
239                    ),
240                ],
241            )),
242            &schema,
243            &ExecutionProps::new(),
244            &PhysicalPlanningContext::default(),
245        )?
246        .evaluate(&RecordBatch::try_new(
247            Arc::clone(schema.inner()),
248            vec![Arc::new(list.clone())],
249        )?)?
250        .into_array(list.len())
251    }
252
253    fn run_any_match_div(
254        list: impl arrow::array::Array + Clone + 'static,
255    ) -> Result<ArrayRef> {
256        let schema = DFSchema::from_unqualified_fields(
257            vec![Field::new(
258                "list",
259                list.data_type().clone(),
260                list.is_nullable(),
261            )]
262            .into(),
263            HashMap::new(),
264        )?;
265
266        let x = Expr::LambdaVariable(LambdaVariable::new(
267            "x".to_string(),
268            Some(Arc::new(Field::new("x", DataType::Int32, true))),
269        ));
270        // predicate: (100 / x) > 5 — panics on divide by zero if x == 0 is evaluated
271        create_physical_expr(
272            &Expr::HigherOrderFunction(HigherOrderFunction::new(
273                array_any_match_higher_order_function(),
274                vec![col("list"), lambda(["x"], (lit(100i32) / x).gt(lit(5i32)))],
275            )),
276            &schema,
277            &ExecutionProps::new(),
278            &PhysicalPlanningContext::default(),
279        )?
280        .evaluate(&RecordBatch::try_new(
281            Arc::clone(schema.inner()),
282            vec![Arc::new(list.clone())],
283        )?)?
284        .into_array(list.len())
285    }
286
287    fn make_list(values: Vec<i32>, offsets: OffsetBuffer<i32>) -> ListArray {
288        make_list_with_nulls(values, offsets, None)
289    }
290
291    fn make_list_with_nulls(
292        values: Vec<i32>,
293        offsets: OffsetBuffer<i32>,
294        nulls: Option<NullBuffer>,
295    ) -> ListArray {
296        ListArray::new(
297            Arc::new(Field::new_list_field(DataType::Int32, true)),
298            offsets,
299            Arc::new(Int32Array::from(values)),
300            nulls,
301        )
302    }
303
304    #[test]
305    fn test_any_match_some_true() -> Result<()> {
306        let list = make_list(vec![1, 2, 3], OffsetBuffer::from_lengths(vec![3]));
307        let result = run_any_match(list)?;
308        assert_eq!(
309            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
310            &BooleanArray::from(vec![Some(true)])
311        );
312        Ok(())
313    }
314
315    #[test]
316    fn test_any_match_none_true() -> Result<()> {
317        let list = make_list(vec![1, 2], OffsetBuffer::from_lengths(vec![2]));
318        let result = run_any_match(list)?;
319        assert_eq!(
320            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
321            &BooleanArray::from(vec![Some(false)])
322        );
323        Ok(())
324    }
325
326    #[test]
327    fn test_any_match_empty_array() -> Result<()> {
328        let list = make_list(vec![], OffsetBuffer::from_lengths(vec![0]));
329        let result = run_any_match(list)?;
330        assert_eq!(
331            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
332            &BooleanArray::from(vec![Some(false)])
333        );
334        Ok(())
335    }
336
337    #[test]
338    fn test_any_match_multiple_rows() -> Result<()> {
339        let list = make_list(vec![1, 2, 3, 1, 2], OffsetBuffer::from_lengths(vec![3, 2]));
340        let result = run_any_match(list)?;
341        assert_eq!(
342            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
343            &BooleanArray::from(vec![Some(true), Some(false)])
344        );
345        Ok(())
346    }
347
348    #[test]
349    fn test_any_match_return_field_nullability() -> Result<()> {
350        for list_nullable in [true, false] {
351            for lambda_nullable in [true, false] {
352                let list = Arc::new(Field::new(
353                    "list",
354                    DataType::new_list(DataType::Int32, true),
355                    list_nullable,
356                ));
357                let lambda =
358                    Arc::new(Field::new("predicate", DataType::Boolean, lambda_nullable));
359                let arg_fields = [
360                    ValueOrLambda::Value(Arc::clone(&list)),
361                    ValueOrLambda::Lambda(Arc::clone(&lambda)),
362                ];
363                let scalar_arguments = [None, None];
364
365                let result = ArrayAnyMatch::new().return_field_from_args(
366                    HigherOrderReturnFieldArgs {
367                        arg_fields: &arg_fields,
368                        scalar_arguments: &scalar_arguments,
369                    },
370                )?;
371
372                assert_eq!(
373                    result,
374                    Arc::new(Field::new(
375                        "",
376                        DataType::Boolean,
377                        list_nullable || lambda_nullable,
378                    ))
379                );
380            }
381        }
382
383        Ok(())
384    }
385
386    // Predicate must not be evaluated on elements belonging to null rows.
387    // The 10 in the null row would satisfy x > 5, but the row result must be None.
388    #[test]
389    fn test_any_match_should_not_evaluate_predicate_on_values_underlying_null()
390    -> Result<()> {
391        let list = make_list_with_nulls(
392            vec![1, 2, 10, 1, 2],
393            OffsetBuffer::from_lengths(vec![3, 2]),
394            Some(NullBuffer::from(vec![false, true])),
395        );
396        let result = run_any_match(list)?;
397        assert_eq!(
398            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
399            &BooleanArray::from(vec![None, Some(false)])
400        );
401        Ok(())
402    }
403
404    // Predicate must not be evaluated on elements before the slice offset.
405    // The 10 before the slice would satisfy x > 5, but it is unreachable.
406    #[test]
407    fn test_any_match_on_sliced_list_should_not_evaluate_on_unreachable_values()
408    -> Result<()> {
409        let list = make_list(
410            vec![10, 1, 2, 1, 2],
411            OffsetBuffer::from_lengths(vec![1, 2, 2]),
412        )
413        .slice(1, 2);
414        let result = run_any_match(list)?;
415        assert_eq!(
416            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
417            &BooleanArray::from(vec![Some(false), Some(false)])
418        );
419        Ok(())
420    }
421
422    // 0 in the null row would cause divide by zero if the predicate is evaluated on it.
423    #[test]
424    fn test_any_match_does_not_evaluate_predicate_on_null_row_values() -> Result<()> {
425        let list = make_list_with_nulls(
426            vec![1, 2, 0, 4, 5],
427            OffsetBuffer::from_lengths(vec![3, 2]),
428            Some(NullBuffer::from(vec![false, true])),
429        );
430        let result = run_any_match_div(list)?;
431        assert_eq!(
432            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
433            &BooleanArray::from(vec![None, Some(true)])
434        );
435        Ok(())
436    }
437
438    // 0 before the slice offset would cause divide by zero if evaluated.
439    #[test]
440    fn test_any_match_does_not_evaluate_predicate_on_unreachable_values() -> Result<()> {
441        let list = make_list(
442            vec![0, 4, 5, 50, 100],
443            OffsetBuffer::from_lengths(vec![1, 2, 2]),
444        )
445        .slice(1, 2);
446        let result = run_any_match_div(list)?;
447        assert_eq!(
448            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
449            &BooleanArray::from(vec![Some(true), Some(false)])
450        );
451        Ok(())
452    }
453
454    #[test]
455    fn test_any_match_large_list_parity() -> Result<()> {
456        let list = create_i32_large_list(
457            vec![1, 2, 3],
458            OffsetBuffer::<i64>::from_lengths(vec![3]),
459            None,
460        );
461        let result = eval_hof_on_i32_list(
462            array_any_match_higher_order_function(),
463            list,
464            v().gt(lit(2i32)),
465        )?;
466        assert_eq!(
467            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
468            &BooleanArray::from(vec![Some(true)])
469        );
470        Ok(())
471    }
472
473    #[test]
474    fn test_any_match_captured_outer_column() -> Result<()> {
475        let list = create_i32_list(
476            vec![1, 50, 4, 50, 7, 50],
477            OffsetBuffer::<i32>::from_lengths(vec![2, 2, 2]),
478            None,
479        );
480        let number = Int32Array::from(vec![10, 40, 60]);
481        let result = eval_hof_on_i32_list_with_outer(
482            array_any_match_higher_order_function(),
483            list,
484            number,
485            v().gt(col("number")),
486        )?;
487        assert_eq!(
488            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
489            &BooleanArray::from(vec![Some(true), Some(true), Some(false)])
490        );
491        Ok(())
492    }
493}