Skip to main content

datafusion_functions/unicode/
find_in_set.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
18use std::sync::Arc;
19
20use arrow::array::{
21    ArrayAccessor, ArrayRef, ArrowPrimitiveType, AsArray, OffsetSizeTrait, PrimitiveArray,
22};
23use arrow::datatypes::{ArrowNativeType, DataType, Int32Type, Int64Type};
24use arrow_buffer::NullBuffer;
25
26use crate::utils::utf8_to_int_type;
27use datafusion_common::{
28    HashMap, Result, ScalarValue, exec_err, internal_err, utils::take_function_args,
29};
30use datafusion_expr::TypeSignature::Exact;
31use datafusion_expr::{
32    ColumnarValue, Documentation, ScalarFunctionArgs, ScalarUDFImpl, Signature,
33    Volatility,
34};
35use datafusion_macros::user_doc;
36
37#[user_doc(
38    doc_section(label = "String Functions"),
39    description = "Returns a value in the range of 1 to N if the string str is in the string list strlist consisting of N substrings.",
40    syntax_example = "find_in_set(str, strlist)",
41    sql_example = r#"```sql
42> select find_in_set('b', 'a,b,c,d');
43+----------------------------------------+
44| find_in_set(Utf8("b"),Utf8("a,b,c,d")) |
45+----------------------------------------+
46| 2                                      |
47+----------------------------------------+
48```"#,
49    argument(name = "str", description = "String expression to find in strlist."),
50    argument(
51        name = "strlist",
52        description = "A string list is a string composed of substrings separated by , characters."
53    )
54)]
55#[derive(Debug, PartialEq, Eq, Hash)]
56pub struct FindInSetFunc {
57    signature: Signature,
58}
59
60impl Default for FindInSetFunc {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl FindInSetFunc {
67    pub fn new() -> Self {
68        use DataType::*;
69        Self {
70            signature: Signature::one_of(
71                vec![
72                    Exact(vec![Utf8View, Utf8View]),
73                    Exact(vec![Utf8, Utf8]),
74                    Exact(vec![LargeUtf8, LargeUtf8]),
75                ],
76                Volatility::Immutable,
77            ),
78        }
79    }
80}
81
82impl ScalarUDFImpl for FindInSetFunc {
83    fn name(&self) -> &str {
84        "find_in_set"
85    }
86
87    fn signature(&self) -> &Signature {
88        &self.signature
89    }
90
91    fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
92        utf8_to_int_type(&arg_types[0], "find_in_set")
93    }
94
95    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
96        let return_field = args.return_field;
97        let [string, str_list] = take_function_args(self.name(), args.args)?;
98
99        match (string, str_list) {
100            // both inputs are scalars
101            (
102                ColumnarValue::Scalar(
103                    ScalarValue::Utf8View(string)
104                    | ScalarValue::Utf8(string)
105                    | ScalarValue::LargeUtf8(string),
106                ),
107                ColumnarValue::Scalar(
108                    ScalarValue::Utf8View(str_list)
109                    | ScalarValue::Utf8(str_list)
110                    | ScalarValue::LargeUtf8(str_list),
111                ),
112            ) => {
113                let res = match (string, str_list) {
114                    (Some(string), Some(str_list)) => {
115                        let position = str_list
116                            .split(',')
117                            .position(|s| s == string)
118                            .map_or(0, |idx| idx + 1);
119
120                        Some(position as i32)
121                    }
122                    _ => None,
123                };
124                Ok(ColumnarValue::Scalar(ScalarValue::from(res)))
125            }
126
127            // `string` is an array, `str_list` is scalar
128            (
129                ColumnarValue::Array(str_array),
130                ColumnarValue::Scalar(
131                    ScalarValue::Utf8View(str_list_literal)
132                    | ScalarValue::Utf8(str_list_literal)
133                    | ScalarValue::LargeUtf8(str_list_literal),
134                ),
135            ) => {
136                match str_list_literal {
137                    // find_in_set(column_a, null) = null
138                    None => Ok(ColumnarValue::Scalar(ScalarValue::try_new_null(
139                        return_field.data_type(),
140                    )?)),
141                    Some(str_list_literal) => {
142                        let str_list = str_list_literal.split(',').collect::<Vec<&str>>();
143                        let result = match str_array.data_type() {
144                            DataType::Utf8 => {
145                                let string_array = str_array.as_string::<i32>();
146                                find_in_set_right_literal::<Int32Type, _>(
147                                    string_array,
148                                    &str_list,
149                                )
150                            }
151                            DataType::LargeUtf8 => {
152                                let string_array = str_array.as_string::<i64>();
153                                find_in_set_right_literal::<Int64Type, _>(
154                                    string_array,
155                                    &str_list,
156                                )
157                            }
158                            DataType::Utf8View => {
159                                let string_array = str_array.as_string_view();
160                                find_in_set_right_literal::<Int32Type, _>(
161                                    string_array,
162                                    &str_list,
163                                )
164                            }
165                            other => {
166                                exec_err!(
167                                    "Unsupported data type {other:?} for function find_in_set"
168                                )
169                            }
170                        };
171                        Ok(ColumnarValue::Array(Arc::new(result?)))
172                    }
173                }
174            }
175
176            // `string` is scalar, `str_list` is an array
177            (
178                ColumnarValue::Scalar(
179                    ScalarValue::Utf8View(string_literal)
180                    | ScalarValue::Utf8(string_literal)
181                    | ScalarValue::LargeUtf8(string_literal),
182                ),
183                ColumnarValue::Array(str_list_array),
184            ) => {
185                match string_literal {
186                    // find_in_set(null, column_b) = null
187                    None => Ok(ColumnarValue::Scalar(ScalarValue::try_new_null(
188                        return_field.data_type(),
189                    )?)),
190                    Some(string) => {
191                        let result = match str_list_array.data_type() {
192                            DataType::Utf8 => {
193                                let str_list = str_list_array.as_string::<i32>();
194                                find_in_set_left_literal::<Int32Type, _>(
195                                    &string, str_list,
196                                )
197                            }
198                            DataType::LargeUtf8 => {
199                                let str_list = str_list_array.as_string::<i64>();
200                                find_in_set_left_literal::<Int64Type, _>(
201                                    &string, str_list,
202                                )
203                            }
204                            DataType::Utf8View => {
205                                let str_list = str_list_array.as_string_view();
206                                find_in_set_left_literal::<Int32Type, _>(
207                                    &string, str_list,
208                                )
209                            }
210                            other => {
211                                exec_err!(
212                                    "Unsupported data type {other:?} for function find_in_set"
213                                )
214                            }
215                        };
216                        Ok(ColumnarValue::Array(Arc::new(result?)))
217                    }
218                }
219            }
220
221            // both inputs are arrays
222            (ColumnarValue::Array(base_array), ColumnarValue::Array(exp_array)) => {
223                let res = find_in_set(&base_array, &exp_array)?;
224
225                Ok(ColumnarValue::Array(res))
226            }
227            _ => {
228                internal_err!("Invalid argument types for `find_in_set` function")
229            }
230        }
231    }
232
233    fn documentation(&self) -> Option<&Documentation> {
234        self.doc()
235    }
236}
237
238/// Returns a value in the range of 1 to N if the string `str` is in the string list `strlist`
239/// consisting of N substrings. A string list is a string composed of substrings separated by `,`
240/// characters.
241fn find_in_set(str: &ArrayRef, str_list: &ArrayRef) -> Result<ArrayRef> {
242    match str.data_type() {
243        DataType::Utf8 => {
244            let string_array = str.as_string::<i32>();
245            let str_list_array = str_list.as_string::<i32>();
246            find_in_set_general::<Int32Type, _>(string_array, str_list_array)
247        }
248        DataType::LargeUtf8 => {
249            let string_array = str.as_string::<i64>();
250            let str_list_array = str_list.as_string::<i64>();
251            find_in_set_general::<Int64Type, _>(string_array, str_list_array)
252        }
253        DataType::Utf8View => {
254            let string_array = str.as_string_view();
255            let str_list_array = str_list.as_string_view();
256            find_in_set_general::<Int32Type, _>(string_array, str_list_array)
257        }
258        other => {
259            exec_err!("Unsupported data type {other:?} for function find_in_set")
260        }
261    }
262}
263
264fn find_in_set_general<'a, T, V>(string_array: V, str_list_array: V) -> Result<ArrayRef>
265where
266    T: ArrowPrimitiveType,
267    T::Native: OffsetSizeTrait,
268    V: ArrayAccessor<Item = &'a str> + Copy,
269{
270    let len = string_array.len();
271    let nulls = NullBuffer::union(string_array.nulls(), str_list_array.nulls());
272    let zero = T::Native::from_usize(0).unwrap();
273
274    let values: Vec<T::Native> = (0..len)
275        .map(|i| {
276            if nulls.as_ref().is_some_and(|n| n.is_null(i)) {
277                return zero;
278            }
279            let string = string_array.value(i);
280            let str_list = str_list_array.value(i);
281            let position = str_list
282                .split(',')
283                .position(|s| s == string)
284                .map_or(0, |idx| idx + 1);
285            T::Native::from_usize(position).unwrap()
286        })
287        .collect();
288
289    Ok(Arc::new(PrimitiveArray::<T>::new(values.into(), nulls)) as ArrayRef)
290}
291
292fn find_in_set_left_literal<'a, T, V>(string: &str, str_list_array: V) -> Result<ArrayRef>
293where
294    T: ArrowPrimitiveType,
295    T::Native: OffsetSizeTrait,
296    V: ArrayAccessor<Item = &'a str> + Copy,
297{
298    let len = str_list_array.len();
299    let nulls = str_list_array.nulls().cloned();
300    let zero = T::Native::from_usize(0).unwrap();
301
302    let values: Vec<T::Native> = (0..len)
303        .map(|i| {
304            if nulls.as_ref().is_some_and(|n| n.is_null(i)) {
305                return zero;
306            }
307            let str_list = str_list_array.value(i);
308            let position = str_list
309                .split(',')
310                .position(|s| s == string)
311                .map_or(0, |idx| idx + 1);
312            T::Native::from_usize(position).unwrap()
313        })
314        .collect();
315
316    Ok(Arc::new(PrimitiveArray::<T>::new(values.into(), nulls)) as ArrayRef)
317}
318
319/// Minimum set length at which a pre-built lookup beats a per-row linear scan.
320/// Below this, the linear scan's small constant factor wins, so short sets are
321/// left untouched to avoid regressing them.
322const FIND_IN_SET_LOOKUP_THRESHOLD: usize = 16;
323
324fn find_in_set_right_literal<'a, T, V>(
325    string_array: V,
326    str_list: &[&str],
327) -> Result<ArrayRef>
328where
329    T: ArrowPrimitiveType,
330    T::Native: OffsetSizeTrait,
331    V: ArrayAccessor<Item = &'a str> + Copy,
332{
333    let len = string_array.len();
334    let nulls = string_array.nulls().cloned();
335    let zero = T::Native::from_usize(0).unwrap();
336
337    // The set (`str_list`) is constant across all rows. For a large set, the
338    // per-row `position` linear scan is O(set_len). Building a lookup from each
339    // distinct entry to its 1-based position once turns each row into an O(1)
340    // probe (first occurrence wins, exactly matching `position`). Below the
341    // threshold the linear scan's small constant factor is faster, so the map is
342    // built at most once here rather than per row.
343    let map: Option<HashMap<&str, usize>> =
344        (str_list.len() >= FIND_IN_SET_LOOKUP_THRESHOLD).then(|| {
345            let mut map = HashMap::with_capacity(str_list.len());
346            for (idx, entry) in str_list.iter().enumerate() {
347                map.entry(*entry).or_insert(idx + 1);
348            }
349            map
350        });
351
352    let values: Vec<T::Native> = (0..len)
353        .map(|i| {
354            if nulls.as_ref().is_some_and(|n| n.is_null(i)) {
355                return zero;
356            }
357            let string = string_array.value(i);
358            let position = match &map {
359                Some(map) => map.get(string).copied().unwrap_or(0),
360                None => str_list
361                    .iter()
362                    .position(|s| *s == string)
363                    .map_or(0, |idx| idx + 1),
364            };
365            T::Native::from_usize(position).unwrap()
366        })
367        .collect();
368
369    Ok(Arc::new(PrimitiveArray::<T>::new(values.into(), nulls)) as ArrayRef)
370}
371
372#[cfg(test)]
373mod tests {
374    use crate::unicode::find_in_set::FindInSetFunc;
375    use crate::utils::test::test_function;
376    use arrow::array::{Array, Int32Array, StringArray};
377    use arrow::datatypes::{DataType::Int32, Field};
378    use datafusion_common::config::ConfigOptions;
379    use datafusion_common::{Result, ScalarValue};
380    use datafusion_expr::{ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl};
381    use std::sync::Arc;
382
383    #[test]
384    fn test_functions() -> Result<()> {
385        test_function!(
386            FindInSetFunc::new(),
387            vec![
388                ColumnarValue::Scalar(ScalarValue::Utf8(Some(String::from("a")))),
389                ColumnarValue::Scalar(ScalarValue::Utf8(Some(String::from("a,b,c")))),
390            ],
391            Ok(Some(1)),
392            i32,
393            Int32,
394            Int32Array
395        );
396        test_function!(
397            FindInSetFunc::new(),
398            vec![
399                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from("🔥")))),
400                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from(
401                    "a,Д,🔥"
402                )))),
403            ],
404            Ok(Some(3)),
405            i32,
406            Int32,
407            Int32Array
408        );
409        test_function!(
410            FindInSetFunc::new(),
411            vec![
412                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from("d")))),
413                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from("a,b,c")))),
414            ],
415            Ok(Some(0)),
416            i32,
417            Int32,
418            Int32Array
419        );
420        test_function!(
421            FindInSetFunc::new(),
422            vec![
423                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from(
424                    "Apache Software Foundation"
425                )))),
426                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from(
427                    "Github,Apache Software Foundation,DataFusion"
428                )))),
429            ],
430            Ok(Some(2)),
431            i32,
432            Int32,
433            Int32Array
434        );
435        test_function!(
436            FindInSetFunc::new(),
437            vec![
438                ColumnarValue::Scalar(ScalarValue::Utf8(Some(String::from("")))),
439                ColumnarValue::Scalar(ScalarValue::Utf8(Some(String::from("a,b,c")))),
440            ],
441            Ok(Some(0)),
442            i32,
443            Int32,
444            Int32Array
445        );
446        test_function!(
447            FindInSetFunc::new(),
448            vec![
449                ColumnarValue::Scalar(ScalarValue::Utf8(Some(String::from("a")))),
450                ColumnarValue::Scalar(ScalarValue::Utf8(Some(String::from("")))),
451            ],
452            Ok(Some(0)),
453            i32,
454            Int32,
455            Int32Array
456        );
457        test_function!(
458            FindInSetFunc::new(),
459            vec![
460                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from("a")))),
461                ColumnarValue::Scalar(ScalarValue::Utf8View(None)),
462            ],
463            Ok(None),
464            i32,
465            Int32,
466            Int32Array
467        );
468        test_function!(
469            FindInSetFunc::new(),
470            vec![
471                ColumnarValue::Scalar(ScalarValue::Utf8View(None)),
472                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from("a,b,c")))),
473            ],
474            Ok(None),
475            i32,
476            Int32,
477            Int32Array
478        );
479
480        Ok(())
481    }
482
483    macro_rules! test_find_in_set {
484        ($test_name:ident, $args:expr, $expected:expr) => {
485            #[test]
486            fn $test_name() -> Result<()> {
487                let fis = crate::unicode::find_in_set();
488
489                let args = $args;
490                let expected = $expected;
491
492                let type_array = args.iter().map(|a| a.data_type()).collect::<Vec<_>>();
493                let cardinality = args
494                    .iter()
495                    .fold(Option::<usize>::None, |acc, arg| match arg {
496                        ColumnarValue::Scalar(_) => acc,
497                        ColumnarValue::Array(a) => Some(a.len()),
498                    })
499                    .unwrap_or(1);
500                let return_type = fis.return_type(&type_array)?;
501                let arg_fields = args
502                    .iter()
503                    .enumerate()
504                    .map(|(idx, a)| {
505                        Field::new(format!("arg_{idx}"), a.data_type(), true).into()
506                    })
507                    .collect::<Vec<_>>();
508                let result = fis.invoke_with_args(ScalarFunctionArgs {
509                    args,
510                    arg_fields,
511                    number_rows: cardinality,
512                    return_field: Field::new("f", return_type, true).into(),
513                    config_options: Arc::new(ConfigOptions::default()),
514                });
515                assert!(result.is_ok());
516
517                let result = result?
518                    .to_array(cardinality)
519                    .expect("Failed to convert to array");
520                let result = result
521                    .as_any()
522                    .downcast_ref::<Int32Array>()
523                    .expect("Failed to convert to type");
524                assert_eq!(*result, expected);
525
526                Ok(())
527            }
528        };
529    }
530
531    test_find_in_set!(
532        test_find_in_set_with_scalar_args,
533        vec![
534            ColumnarValue::Array(Arc::new(StringArray::from(vec![
535                "", "a", "b", "c", "d"
536            ]))),
537            ColumnarValue::Scalar(ScalarValue::Utf8(Some("b,c,d".to_string()))),
538        ],
539        Int32Array::from(vec![0, 0, 1, 2, 3])
540    );
541    test_find_in_set!(
542        test_find_in_set_with_scalar_args_2,
543        vec![
544            ColumnarValue::Scalar(ScalarValue::Utf8View(Some(
545                "ApacheSoftware".to_string()
546            ))),
547            ColumnarValue::Array(Arc::new(StringArray::from(vec![
548                "a,b,c",
549                "ApacheSoftware,Github,DataFusion",
550                ""
551            ]))),
552        ],
553        Int32Array::from(vec![0, 1, 0])
554    );
555    test_find_in_set!(
556        test_find_in_set_with_scalar_args_3,
557        vec![
558            ColumnarValue::Array(Arc::new(StringArray::from(vec![None::<&str>; 3]))),
559            ColumnarValue::Scalar(ScalarValue::Utf8View(Some("a,b,c".to_string()))),
560        ],
561        Int32Array::from(vec![None::<i32>; 3])
562    );
563    test_find_in_set!(
564        test_find_in_set_with_scalar_args_4,
565        vec![
566            ColumnarValue::Scalar(ScalarValue::Utf8View(Some("a".to_string()))),
567            ColumnarValue::Array(Arc::new(StringArray::from(vec![None::<&str>; 3]))),
568        ],
569        Int32Array::from(vec![None::<i32>; 3])
570    );
571
572    // Exercises both the lookup-map path (list length >= threshold) and the
573    // linear-scan path (short list), including a duplicate entry to confirm the
574    // first occurrence wins in both.
575    #[test]
576    fn test_right_literal_lookup_matches_linear() {
577        use super::find_in_set_right_literal;
578        use arrow::datatypes::Int32Type;
579
580        // 40 unique entries plus a duplicate of "item5" appended at index 40, so
581        // the length is well over FIND_IN_SET_LOOKUP_THRESHOLD.
582        let mut long_list: Vec<String> = (0..40).map(|i| format!("item{i}")).collect();
583        long_list.push("item5".to_string());
584        let long_refs: Vec<&str> = long_list.iter().map(|s| s.as_str()).collect();
585        let short_refs = ["a", "b", "c"];
586
587        let strings = StringArray::from(vec![
588            Some("item0"),
589            Some("item39"),
590            Some("item5"),
591            Some("missing"),
592            None,
593            Some("b"),
594        ]);
595
596        let long =
597            find_in_set_right_literal::<Int32Type, _>(&strings, &long_refs).unwrap();
598        let long = long.as_any().downcast_ref::<Int32Array>().unwrap();
599        assert_eq!(long.value(0), 1);
600        assert_eq!(long.value(1), 40);
601        assert_eq!(long.value(2), 6); // first occurrence of "item5"
602        assert_eq!(long.value(3), 0);
603        assert!(long.is_null(4));
604        assert_eq!(long.value(5), 0);
605
606        let short =
607            find_in_set_right_literal::<Int32Type, _>(&strings, &short_refs).unwrap();
608        let short = short.as_any().downcast_ref::<Int32Array>().unwrap();
609        assert_eq!(short.value(0), 0);
610        assert!(short.is_null(4));
611        assert_eq!(short.value(5), 2); // "b" at position 2
612    }
613}