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, AsArray, BooleanArray, BooleanBuilder, new_null_array},
22    buffer::NullBuffer,
23    compute::take_arrays,
24    datatypes::{ArrowNativeType, DataType, Field, FieldRef},
25};
26use datafusion_common::{
27    Result, exec_datafusion_err, exec_err, plan_err,
28    utils::{
29        adjust_offsets_for_slice, list_values, list_values_row_number, take_function_args,
30    },
31};
32use datafusion_expr::{
33    ColumnarValue, Documentation, HigherOrderFunctionArgs, HigherOrderReturnFieldArgs,
34    HigherOrderSignature, HigherOrderUDFImpl, LambdaParametersProgress, ValueOrLambda,
35    Volatility,
36};
37use datafusion_macros::user_doc;
38use std::{fmt::Debug, sync::Arc};
39
40use crate::lambda_utils::coerce_single_list_arg;
41
42make_higher_order_function_expr_and_func!(
43    ArrayAnyMatch,
44    array_any_match,
45    array lambda,
46    "returns true if any element in the array satisfies the predicate",
47    array_any_match_higher_order_function
48);
49
50#[user_doc(
51    doc_section(label = "Array Functions"),
52    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.",
53    syntax_example = "any_match(array, predicate)",
54    sql_example = r#"```sql
55> select any_match([1, 2, 3], x -> x > 2);
56+----------------------------------+
57| any_match([1, 2, 3], x -> x > 2) |
58+----------------------------------+
59| true                             |
60+----------------------------------+
61```"#,
62    argument(
63        name = "array",
64        description = "Array expression. Can be a constant, column, or function, and any combination of array operators."
65    ),
66    argument(
67        name = "predicate",
68        description = "Lambda predicate that returns a boolean"
69    )
70)]
71#[derive(Debug, PartialEq, Eq, Hash)]
72pub struct ArrayAnyMatch {
73    signature: HigherOrderSignature,
74    aliases: Vec<String>,
75}
76
77impl Default for ArrayAnyMatch {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl ArrayAnyMatch {
84    pub fn new() -> Self {
85        Self {
86            signature: HigherOrderSignature::exact(
87                vec![ValueOrLambda::Value(()), ValueOrLambda::Lambda(())],
88                Volatility::Immutable,
89            ),
90            aliases: vec![String::from("any_match"), String::from("list_any_match")],
91        }
92    }
93}
94
95// Returns Some(true) if any element in [start, end) is true,
96// None if no element is true but some are null,
97// Some(false) if all are false or range is empty.
98fn any_match_for_range(
99    predicate: &BooleanArray,
100    start: usize,
101    end: usize,
102) -> Option<bool> {
103    let any_true = (start..end).any(|j| predicate.is_valid(j) && predicate.value(j));
104    if any_true {
105        return Some(true);
106    }
107    let any_null = (start..end).any(|j| predicate.is_null(j));
108    if any_null { None } else { Some(false) }
109}
110
111impl HigherOrderUDFImpl for ArrayAnyMatch {
112    fn name(&self) -> &str {
113        "array_any_match"
114    }
115
116    fn aliases(&self) -> &[String] {
117        &self.aliases
118    }
119
120    fn signature(&self) -> &HigherOrderSignature {
121        &self.signature
122    }
123
124    fn coerce_value_types(&self, arg_types: &[DataType]) -> Result<Vec<DataType>> {
125        coerce_single_list_arg(self.name(), arg_types)
126    }
127
128    fn lambda_parameters(
129        &self,
130        _step: usize,
131        fields: &[ValueOrLambda<FieldRef, Option<FieldRef>>],
132    ) -> Result<LambdaParametersProgress> {
133        let [list, _] = take_function_args(self.name(), fields)?;
134        let ValueOrLambda::Value(list) = list else {
135            return plan_err!("{} expects a value as first argument", self.name());
136        };
137
138        let field = match list.data_type() {
139            DataType::List(field) => field,
140            DataType::LargeList(field) => field,
141            other => return plan_err!("expected list, got {other}"),
142        };
143
144        Ok(LambdaParametersProgress::Complete(vec![vec![Arc::clone(
145            field,
146        )]]))
147    }
148
149    fn return_field_from_args(
150        &self,
151        args: HigherOrderReturnFieldArgs,
152    ) -> Result<Arc<Field>> {
153        let [ValueOrLambda::Value(list), _] =
154            take_function_args(self.name(), args.arg_fields)?
155        else {
156            return plan_err!("{} expects a value as first argument", self.name());
157        };
158        let nullable = list.is_nullable();
159        Ok(Arc::new(Field::new("", DataType::Boolean, nullable)))
160    }
161
162    fn invoke_with_args(&self, args: HigherOrderFunctionArgs) -> Result<ColumnarValue> {
163        let [ValueOrLambda::Value(list), ValueOrLambda::Lambda(lambda)] =
164            take_function_args(self.name(), &args.args)?
165        else {
166            return exec_err!("{} expects a value followed by a lambda", self.name());
167        };
168
169        let list_array = list.to_array(args.number_rows)?;
170
171        // fast path: fully null input — also required for FixedSizeList which can't be
172        // handled by clear_null_values when fully null
173        if list_array.null_count() == list_array.len() {
174            return Ok(ColumnarValue::Array(new_null_array(
175                args.return_type(),
176                list_array.len(),
177            )));
178        }
179
180        let list_values = list_values(&list_array)?;
181
182        let values_param = || Ok(Arc::clone(&list_values));
183
184        let predicate_results = lambda
185            .evaluate(&[&values_param], |arrays| {
186                let indices = list_values_row_number(&list_array)?;
187                Ok(take_arrays(arrays, &indices, None)?)
188            })?
189            .into_array(list_values.len())?;
190
191        let predicate_bool = predicate_results
192            .as_any()
193            .downcast_ref::<BooleanArray>()
194            .ok_or_else(|| {
195                exec_datafusion_err!(
196                    "{} predicate must return boolean array",
197                    self.name()
198                )
199            })?;
200
201        let mut values = BooleanBuilder::with_capacity(list_array.len());
202
203        // Maps predicate results (flat over all elements) back to one Boolean per row.
204        // Uses adjusted offsets so sliced lists index correctly into the predicate array.
205        macro_rules! process_list {
206            ($list_typed:expr) => {{
207                let offsets = adjust_offsets_for_slice($list_typed);
208                for i in 0..$list_typed.len() {
209                    let start = offsets[i].as_usize();
210                    let end = offsets[i + 1].as_usize();
211                    // any_match_for_range returns None when nulls poison the result;
212                    // null rows produce an empty range and return Some(false), but their
213                    // null bit is preserved by attaching the original null bitmap below.
214                    values.append_option(any_match_for_range(predicate_bool, start, end));
215                }
216            }};
217        }
218
219        match list_array.data_type() {
220            DataType::List(_) => {
221                process_list!(list_array.as_list::<i32>());
222            }
223            DataType::LargeList(_) => {
224                process_list!(list_array.as_list::<i64>());
225            }
226            other => return exec_err!("expected list, got {other}"),
227        }
228
229        let (boolean_buffer, predicate_nulls) = values.finish().into_parts();
230        // Merge: a row is null if the input list row was null or the predicate returned null.
231        let nulls = NullBuffer::union(list_array.nulls(), predicate_nulls.as_ref());
232        Ok(ColumnarValue::Array(Arc::new(BooleanArray::new(
233            boolean_buffer,
234            nulls,
235        ))))
236    }
237
238    fn documentation(&self) -> Option<&Documentation> {
239        self.doc()
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use std::{collections::HashMap, sync::Arc};
246
247    use arrow::{
248        array::{ArrayRef, BooleanArray, Int32Array, ListArray, RecordBatch},
249        buffer::{NullBuffer, OffsetBuffer},
250        datatypes::{DataType, Field},
251    };
252    use datafusion_common::{DFSchema, Result};
253    use datafusion_expr::{
254        Expr, col,
255        execution_props::ExecutionProps,
256        expr::{HigherOrderFunction, LambdaVariable},
257        lambda, lit,
258    };
259    use datafusion_physical_expr::create_physical_expr;
260
261    use crate::array_any_match::array_any_match_higher_order_function;
262
263    fn run_any_match(
264        list: impl arrow::array::Array + Clone + 'static,
265    ) -> Result<ArrayRef> {
266        let schema = DFSchema::from_unqualified_fields(
267            vec![Field::new(
268                "list",
269                list.data_type().clone(),
270                list.is_nullable(),
271            )]
272            .into(),
273            HashMap::new(),
274        )?;
275
276        create_physical_expr(
277            &Expr::HigherOrderFunction(HigherOrderFunction::new(
278                array_any_match_higher_order_function(),
279                vec![
280                    col("list"),
281                    lambda(
282                        ["x"],
283                        Expr::LambdaVariable(LambdaVariable::new(
284                            "x".to_string(),
285                            Some(Arc::new(Field::new("x", DataType::Int32, true))),
286                        ))
287                        .gt(lit(2i32)),
288                    ),
289                ],
290            )),
291            &schema,
292            &ExecutionProps::new(),
293        )?
294        .evaluate(&RecordBatch::try_new(
295            Arc::clone(schema.inner()),
296            vec![Arc::new(list.clone())],
297        )?)?
298        .into_array(list.len())
299    }
300
301    fn run_any_match_div(
302        list: impl arrow::array::Array + Clone + 'static,
303    ) -> Result<ArrayRef> {
304        let schema = DFSchema::from_unqualified_fields(
305            vec![Field::new(
306                "list",
307                list.data_type().clone(),
308                list.is_nullable(),
309            )]
310            .into(),
311            HashMap::new(),
312        )?;
313
314        let x = Expr::LambdaVariable(LambdaVariable::new(
315            "x".to_string(),
316            Some(Arc::new(Field::new("x", DataType::Int32, true))),
317        ));
318        // predicate: (100 / x) > 5 — panics on divide by zero if x == 0 is evaluated
319        create_physical_expr(
320            &Expr::HigherOrderFunction(HigherOrderFunction::new(
321                array_any_match_higher_order_function(),
322                vec![col("list"), lambda(["x"], (lit(100i32) / x).gt(lit(5i32)))],
323            )),
324            &schema,
325            &ExecutionProps::new(),
326        )?
327        .evaluate(&RecordBatch::try_new(
328            Arc::clone(schema.inner()),
329            vec![Arc::new(list.clone())],
330        )?)?
331        .into_array(list.len())
332    }
333
334    fn make_list(values: Vec<i32>, offsets: OffsetBuffer<i32>) -> ListArray {
335        make_list_with_nulls(values, offsets, None)
336    }
337
338    fn make_list_with_nulls(
339        values: Vec<i32>,
340        offsets: OffsetBuffer<i32>,
341        nulls: Option<NullBuffer>,
342    ) -> ListArray {
343        ListArray::new(
344            Arc::new(Field::new_list_field(DataType::Int32, true)),
345            offsets,
346            Arc::new(Int32Array::from(values)),
347            nulls,
348        )
349    }
350
351    #[test]
352    fn test_any_match_some_true() -> Result<()> {
353        let list = make_list(vec![1, 2, 3], OffsetBuffer::from_lengths(vec![3]));
354        let result = run_any_match(list)?;
355        assert_eq!(
356            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
357            &BooleanArray::from(vec![Some(true)])
358        );
359        Ok(())
360    }
361
362    #[test]
363    fn test_any_match_none_true() -> Result<()> {
364        let list = make_list(vec![1, 2], OffsetBuffer::from_lengths(vec![2]));
365        let result = run_any_match(list)?;
366        assert_eq!(
367            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
368            &BooleanArray::from(vec![Some(false)])
369        );
370        Ok(())
371    }
372
373    #[test]
374    fn test_any_match_empty_array() -> Result<()> {
375        let list = make_list(vec![], OffsetBuffer::from_lengths(vec![0]));
376        let result = run_any_match(list)?;
377        assert_eq!(
378            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
379            &BooleanArray::from(vec![Some(false)])
380        );
381        Ok(())
382    }
383
384    #[test]
385    fn test_any_match_multiple_rows() -> Result<()> {
386        let list = make_list(vec![1, 2, 3, 1, 2], OffsetBuffer::from_lengths(vec![3, 2]));
387        let result = run_any_match(list)?;
388        assert_eq!(
389            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
390            &BooleanArray::from(vec![Some(true), Some(false)])
391        );
392        Ok(())
393    }
394
395    // Predicate must not be evaluated on elements belonging to null rows.
396    // The 10 in the null row would satisfy x > 5, but the row result must be None.
397    #[test]
398    fn test_any_match_should_not_evaluate_predicate_on_values_underlying_null()
399    -> Result<()> {
400        let list = make_list_with_nulls(
401            vec![1, 2, 10, 1, 2],
402            OffsetBuffer::from_lengths(vec![3, 2]),
403            Some(NullBuffer::from(vec![false, true])),
404        );
405        let result = run_any_match(list)?;
406        assert_eq!(
407            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
408            &BooleanArray::from(vec![None, Some(false)])
409        );
410        Ok(())
411    }
412
413    // Predicate must not be evaluated on elements before the slice offset.
414    // The 10 before the slice would satisfy x > 5, but it is unreachable.
415    #[test]
416    fn test_any_match_on_sliced_list_should_not_evaluate_on_unreachable_values()
417    -> Result<()> {
418        let list = make_list(
419            vec![10, 1, 2, 1, 2],
420            OffsetBuffer::from_lengths(vec![1, 2, 2]),
421        )
422        .slice(1, 2);
423        let result = run_any_match(list)?;
424        assert_eq!(
425            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
426            &BooleanArray::from(vec![Some(false), Some(false)])
427        );
428        Ok(())
429    }
430
431    // 0 in the null row would cause divide by zero if the predicate is evaluated on it.
432    #[test]
433    fn test_any_match_does_not_evaluate_predicate_on_null_row_values() -> Result<()> {
434        let list = make_list_with_nulls(
435            vec![1, 2, 0, 4, 5],
436            OffsetBuffer::from_lengths(vec![3, 2]),
437            Some(NullBuffer::from(vec![false, true])),
438        );
439        let result = run_any_match_div(list)?;
440        assert_eq!(
441            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
442            &BooleanArray::from(vec![None, Some(true)])
443        );
444        Ok(())
445    }
446
447    // 0 before the slice offset would cause divide by zero if evaluated.
448    #[test]
449    fn test_any_match_does_not_evaluate_predicate_on_unreachable_values() -> Result<()> {
450        let list = make_list(
451            vec![0, 4, 5, 50, 100],
452            OffsetBuffer::from_lengths(vec![1, 2, 2]),
453        )
454        .slice(1, 2);
455        let result = run_any_match_div(list)?;
456        assert_eq!(
457            result.as_any().downcast_ref::<BooleanArray>().unwrap(),
458            &BooleanArray::from(vec![Some(true), Some(false)])
459        );
460        Ok(())
461    }
462}