1use 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
91fn 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 values.append_option(any_match_for_range(&predicate, start, end));
173 }
174
175 let (boolean_buffer, predicate_nulls) = values.finish().into_parts();
176 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 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 #[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 #[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 #[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 #[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}