1use 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 (
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 (
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 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 (
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 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 (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
238fn 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
319const 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 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 #[test]
576 fn test_right_literal_lookup_matches_linear() {
577 use super::find_in_set_right_literal;
578 use arrow::datatypes::Int32Type;
579
580 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); 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); }
613}