Skip to main content

datafusion_functions/string/
repeat.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 crate::strings::{
19    BulkNullStringArrayBuilder, GenericStringArrayBuilder, StringViewArrayBuilder,
20};
21use crate::utils::utf8_to_str_type;
22use arrow::array::{Array, ArrayRef, AsArray, Int64Array, StringArrayType};
23use arrow::buffer::NullBuffer;
24use arrow::datatypes::DataType;
25use arrow::datatypes::DataType::{LargeUtf8, Utf8, Utf8View};
26use datafusion_common::cast::as_int64_array;
27use datafusion_common::types::{NativeType, logical_int64, logical_string};
28use datafusion_common::utils::take_function_args;
29use datafusion_common::{
30    DataFusionError, Result, ScalarValue, exec_datafusion_err, exec_err, internal_err,
31};
32use datafusion_expr::{ColumnarValue, Documentation, Volatility};
33use datafusion_expr::{ScalarFunctionArgs, ScalarUDFImpl, Signature};
34use datafusion_expr_common::signature::{Coercion, TypeSignatureClass};
35use datafusion_macros::user_doc;
36
37#[user_doc(
38    doc_section(label = "String Functions"),
39    description = "Returns a string with an input string repeated a specified number.",
40    syntax_example = "repeat(str, n)",
41    sql_example = r#"```sql
42> select repeat('data', 3);
43+-------------------------------+
44| repeat(Utf8("data"),Int64(3)) |
45+-------------------------------+
46| datadatadata                  |
47+-------------------------------+
48```"#,
49    standard_argument(name = "str", prefix = "String"),
50    argument(
51        name = "n",
52        description = "Number of times to repeat the input string."
53    )
54)]
55#[derive(Debug, PartialEq, Eq, Hash)]
56pub struct RepeatFunc {
57    signature: Signature,
58}
59
60impl Default for RepeatFunc {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl RepeatFunc {
67    pub fn new() -> Self {
68        Self {
69            signature: Signature::coercible(
70                vec![
71                    Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
72                    // Accept all integer types but cast them to i64
73                    Coercion::new_implicit(
74                        TypeSignatureClass::Native(logical_int64()),
75                        vec![TypeSignatureClass::Integer],
76                        NativeType::Int64,
77                    ),
78                ],
79                Volatility::Immutable,
80            ),
81        }
82    }
83}
84
85impl ScalarUDFImpl for RepeatFunc {
86    fn name(&self) -> &str {
87        "repeat"
88    }
89
90    fn signature(&self) -> &Signature {
91        &self.signature
92    }
93
94    fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
95        if arg_types[0] == Utf8View {
96            return Ok(Utf8View);
97        }
98        utf8_to_str_type(&arg_types[0], "repeat")
99    }
100
101    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
102        let return_type = args.return_field.data_type().clone();
103        let [string_arg, count_arg] = take_function_args(self.name(), args.args)?;
104
105        // Early return if either argument is a scalar null
106        if let ColumnarValue::Scalar(s) = &string_arg
107            && s.is_null()
108        {
109            return Ok(ColumnarValue::Scalar(ScalarValue::try_from(&return_type)?));
110        }
111        if let ColumnarValue::Scalar(c) = &count_arg
112            && c.is_null()
113        {
114            return Ok(ColumnarValue::Scalar(ScalarValue::try_from(&return_type)?));
115        }
116
117        match (&string_arg, &count_arg) {
118            (
119                ColumnarValue::Scalar(string_scalar),
120                ColumnarValue::Scalar(count_scalar),
121            ) => {
122                let count = match count_scalar {
123                    ScalarValue::Int64(Some(n)) => *n,
124                    _ => {
125                        return internal_err!(
126                            "Unexpected data type {:?} for repeat count",
127                            count_scalar.data_type()
128                        );
129                    }
130                };
131
132                let result = match string_scalar {
133                    ScalarValue::Utf8View(Some(s)) => ScalarValue::Utf8View(Some(
134                        compute_repeat(s, count, i32::MAX as usize)?,
135                    )),
136                    ScalarValue::Utf8(Some(s)) => ScalarValue::Utf8(Some(
137                        compute_repeat(s, count, i32::MAX as usize)?,
138                    )),
139                    ScalarValue::LargeUtf8(Some(s)) => ScalarValue::LargeUtf8(Some(
140                        compute_repeat(s, count, i64::MAX as usize)?,
141                    )),
142                    _ => {
143                        return internal_err!(
144                            "Unexpected data type {:?} for function repeat",
145                            string_scalar.data_type()
146                        );
147                    }
148                };
149
150                Ok(ColumnarValue::Scalar(result))
151            }
152            _ => {
153                let string_array = string_arg.to_array(args.number_rows)?;
154                let count_array = count_arg.to_array(args.number_rows)?;
155                Ok(ColumnarValue::Array(repeat(&string_array, &count_array)?))
156            }
157        }
158    }
159
160    fn documentation(&self) -> Option<&Documentation> {
161        self.doc()
162    }
163}
164
165/// Computes repeat for a single string value with max size check
166#[inline]
167fn compute_repeat(s: &str, count: i64, max_size: usize) -> Result<String> {
168    if count <= 0 {
169        return Ok(String::new());
170    }
171    let result_len = repeat_len(s.len(), count, max_size)?;
172    debug_assert!(result_len <= max_size);
173    let count = repeat_count(count, max_size)?;
174    Ok(s.repeat(count))
175}
176
177fn repeat_len(string_len: usize, count: i64, max_size: usize) -> Result<usize> {
178    let count = repeat_count(count, max_size)?;
179    let result_len = string_len.checked_mul(count).ok_or_else(|| {
180        exec_datafusion_err!(
181            "string size overflow on repeat, max size is {}, but got {}",
182            max_size,
183            usize::MAX
184        )
185    })?;
186    if result_len > max_size {
187        return exec_err!(
188            "string size overflow on repeat, max size is {}, but got {}",
189            max_size,
190            result_len
191        );
192    }
193    Ok(result_len)
194}
195
196fn repeat_count(count: i64, max_size: usize) -> Result<usize> {
197    match usize::try_from(count) {
198        Ok(count) => Ok(count),
199        Err(_) => exec_err!(
200            "string size overflow on repeat, max size is {}, but got {}",
201            max_size,
202            usize::MAX
203        ),
204    }
205}
206
207/// Repeats string the specified number of times.
208/// repeat('Pg', 4) = 'PgPgPgPg'
209fn repeat(string_array: &ArrayRef, count_array: &ArrayRef) -> Result<ArrayRef> {
210    let number_array = as_int64_array(count_array)?;
211    match string_array.data_type() {
212        Utf8View => {
213            let string_view_array = string_array.as_string_view();
214            let (_, max_item_capacity) = calculate_capacities(
215                &string_view_array,
216                number_array,
217                i32::MAX as usize,
218            )?;
219            let builder = StringViewArrayBuilder::with_capacity(string_array.len());
220            repeat_impl(&string_view_array, number_array, max_item_capacity, builder)
221        }
222        Utf8 => {
223            let string_arr = string_array.as_string::<i32>();
224            let (total_capacity, max_item_capacity) =
225                calculate_capacities(&string_arr, number_array, i32::MAX as usize)?;
226            let builder = GenericStringArrayBuilder::<i32>::with_capacity(
227                string_array.len(),
228                total_capacity,
229            );
230            repeat_impl(&string_arr, number_array, max_item_capacity, builder)
231        }
232        LargeUtf8 => {
233            let string_arr = string_array.as_string::<i64>();
234            let (total_capacity, max_item_capacity) =
235                calculate_capacities(&string_arr, number_array, i64::MAX as usize)?;
236            let builder = GenericStringArrayBuilder::<i64>::with_capacity(
237                string_array.len(),
238                total_capacity,
239            );
240            repeat_impl(&string_arr, number_array, max_item_capacity, builder)
241        }
242        other => exec_err!(
243            "Unsupported data type {other:?} for function repeat. \
244        Expected Utf8, Utf8View or LargeUtf8."
245        ),
246    }
247}
248
249fn calculate_capacities<'a, S>(
250    string_array: &S,
251    number_array: &Int64Array,
252    max_str_len: usize,
253) -> Result<(usize, usize)>
254where
255    S: StringArrayType<'a>,
256{
257    let mut total_capacity = 0usize;
258    let mut max_item_capacity = 0usize;
259
260    string_array.iter().zip(number_array.iter()).try_for_each(
261        |(string, number)| -> Result<(), DataFusionError> {
262            match (string, number) {
263                (Some(string), Some(number)) if number >= 0 => {
264                    let item_capacity = repeat_len(string.len(), number, max_str_len)?;
265                    total_capacity =
266                        total_capacity.checked_add(item_capacity).ok_or_else(|| {
267                            exec_datafusion_err!(
268                                "string size overflow on repeat, max size is {}, but got {}",
269                                max_str_len,
270                                usize::MAX
271                            )
272                        })?;
273                    max_item_capacity = max_item_capacity.max(item_capacity);
274                }
275                _ => (),
276            }
277            Ok(())
278        },
279    )?;
280
281    Ok((total_capacity, max_item_capacity))
282}
283
284fn repeat_impl<'a, S, B>(
285    string_array: &S,
286    number_array: &Int64Array,
287    max_item_capacity: usize,
288    mut builder: B,
289) -> Result<ArrayRef>
290where
291    S: StringArrayType<'a> + 'a,
292    B: BulkNullStringArrayBuilder,
293{
294    // Reusable buffer to avoid allocations in string.repeat()
295    let mut buffer = Vec::<u8>::with_capacity(max_item_capacity);
296
297    // Helper function to repeat a string into a buffer using doubling strategy
298    // count must be > 0
299    #[inline]
300    fn repeat_to_buffer(buffer: &mut Vec<u8>, string: &str, count: usize) {
301        buffer.clear();
302        if !string.is_empty() {
303            let src = string.as_bytes();
304            // Initial copy
305            buffer.extend_from_slice(src);
306            // Doubling strategy: copy what we have so far until we reach the target
307            while buffer.len() < src.len() * count {
308                let copy_len = buffer.len().min(src.len() * count - buffer.len());
309                // SAFETY: we're copying valid UTF-8 bytes that we already verified
310                buffer.extend_from_within(..copy_len);
311            }
312        }
313    }
314
315    // Output is null IFF either input is null
316    let nulls = NullBuffer::union(string_array.nulls(), number_array.nulls());
317
318    if let Some(ref n) = nulls {
319        for i in 0..string_array.len() {
320            if n.is_null(i) {
321                builder.append_placeholder();
322                continue;
323            }
324            // SAFETY: index `i` in both arrays is valid
325            let string = unsafe { string_array.value_unchecked(i) };
326            let count = unsafe { number_array.value_unchecked(i) };
327            if count > 0 {
328                repeat_to_buffer(&mut buffer, string, count as usize);
329                // SAFETY: buffer contains valid UTF-8 since we only copy from a valid &str
330                builder.append_value(unsafe { std::str::from_utf8_unchecked(&buffer) });
331            } else {
332                builder.append_value("");
333            }
334        }
335    } else {
336        for i in 0..string_array.len() {
337            // SAFETY: no nulls, so every index in both arrays is valid
338            let string = unsafe { string_array.value_unchecked(i) };
339            let count = unsafe { number_array.value_unchecked(i) };
340            if count > 0 {
341                repeat_to_buffer(&mut buffer, string, count as usize);
342                // SAFETY: buffer contains valid UTF-8 since we only copy from a valid &str
343                builder.append_value(unsafe { std::str::from_utf8_unchecked(&buffer) });
344            } else {
345                builder.append_value("");
346            }
347        }
348    }
349
350    builder.finish(nulls)
351}
352
353#[cfg(test)]
354mod tests {
355    use std::sync::Arc;
356
357    use arrow::array::{
358        Array, ArrayRef, Int64Array, LargeStringArray, StringArray, StringViewArray,
359    };
360    use arrow::datatypes::DataType::{LargeUtf8, Utf8, Utf8View};
361
362    use datafusion_common::ScalarValue;
363    use datafusion_common::{Result, exec_err};
364    use datafusion_expr::{ColumnarValue, ScalarUDFImpl};
365
366    use crate::string::repeat::RepeatFunc;
367    use crate::utils::test::test_function;
368
369    #[test]
370    fn test_functions() -> Result<()> {
371        test_function!(
372            RepeatFunc::new(),
373            vec![
374                ColumnarValue::Scalar(ScalarValue::Utf8(Some(String::from("Pg")))),
375                ColumnarValue::Scalar(ScalarValue::Int64(Some(4))),
376            ],
377            Ok(Some("PgPgPgPg")),
378            &str,
379            Utf8,
380            StringArray
381        );
382        test_function!(
383            RepeatFunc::new(),
384            vec![
385                ColumnarValue::Scalar(ScalarValue::Utf8(None)),
386                ColumnarValue::Scalar(ScalarValue::Int64(Some(4))),
387            ],
388            Ok(None),
389            &str,
390            Utf8,
391            StringArray
392        );
393        test_function!(
394            RepeatFunc::new(),
395            vec![
396                ColumnarValue::Scalar(ScalarValue::Utf8(Some(String::from("Pg")))),
397                ColumnarValue::Scalar(ScalarValue::Int64(None)),
398            ],
399            Ok(None),
400            &str,
401            Utf8,
402            StringArray
403        );
404
405        test_function!(
406            RepeatFunc::new(),
407            vec![
408                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from("Pg")))),
409                ColumnarValue::Scalar(ScalarValue::Int64(Some(4))),
410            ],
411            Ok(Some("PgPgPgPg")),
412            &str,
413            Utf8View,
414            StringViewArray
415        );
416        test_function!(
417            RepeatFunc::new(),
418            vec![
419                ColumnarValue::Scalar(ScalarValue::Utf8View(None)),
420                ColumnarValue::Scalar(ScalarValue::Int64(Some(4))),
421            ],
422            Ok(None),
423            &str,
424            Utf8View,
425            StringViewArray
426        );
427        test_function!(
428            RepeatFunc::new(),
429            vec![
430                ColumnarValue::Scalar(ScalarValue::LargeUtf8(Some(String::from("Pg")))),
431                ColumnarValue::Scalar(ScalarValue::Int64(None)),
432            ],
433            Ok(None),
434            &str,
435            LargeUtf8,
436            LargeStringArray
437        );
438        test_function!(
439            RepeatFunc::new(),
440            vec![
441                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from("Pg")))),
442                ColumnarValue::Scalar(ScalarValue::Int64(None)),
443            ],
444            Ok(None),
445            &str,
446            Utf8View,
447            StringViewArray
448        );
449        test_function!(
450            RepeatFunc::new(),
451            vec![
452                ColumnarValue::Scalar(ScalarValue::Utf8(Some(String::from("Pg")))),
453                ColumnarValue::Scalar(ScalarValue::Int64(Some(1073741824))),
454            ],
455            exec_err!(
456                "string size overflow on repeat, max size is {}, but got {}",
457                i32::MAX,
458                2usize * 1073741824
459            ),
460            &str,
461            Utf8,
462            StringArray
463        );
464
465        Ok(())
466    }
467
468    // Slicing the input arrays produces a NullBuffer with a non-zero offset.
469    // The tests below use 6-row inputs sliced to (1, 4) so that:
470    //   slot 0 (orig 1): "a"  × 3    → "aaa"
471    //   slot 1 (orig 2): "bb" × 2    → "bbbb"
472    //   slot 2 (orig 3): "c"  × NULL → NULL (count-side null)
473    //   slot 3 (orig 4): NULL × 1    → NULL (string-side null)
474    fn sliced_offset_inputs<F>(make_strings: F) -> (ArrayRef, ArrayRef)
475    where
476        F: FnOnce(Vec<Option<&'static str>>) -> ArrayRef,
477    {
478        let strings = make_strings(vec![
479            None,
480            Some("a"),
481            Some("bb"),
482            Some("c"),
483            None,
484            Some("d"),
485        ]);
486        let counts: ArrayRef = Arc::new(Int64Array::from(vec![
487            Some(2),
488            Some(3),
489            Some(2),
490            None,
491            Some(1),
492            Some(2),
493        ]));
494        (strings.slice(1, 4), counts.slice(1, 4))
495    }
496
497    fn assert_sliced_offset_output<A: Array + 'static>(result: ArrayRef)
498    where
499        for<'a> &'a A: arrow::array::ArrayAccessor<Item = &'a str>,
500    {
501        let result = result.as_any().downcast_ref::<A>().unwrap();
502        assert_eq!(result.len(), 4);
503        assert_eq!(arrow::array::ArrayAccessor::value(&result, 0), "aaa");
504        assert_eq!(arrow::array::ArrayAccessor::value(&result, 1), "bbbb");
505        assert!(result.is_null(2));
506        assert!(result.is_null(3));
507        assert_eq!(result.null_count(), 2);
508    }
509
510    #[test]
511    fn test_repeat_sliced_string_with_null_offset() {
512        let (strings, counts) = sliced_offset_inputs(|v| Arc::new(StringArray::from(v)));
513        let result = super::repeat(&strings, &counts).unwrap();
514        assert_sliced_offset_output::<StringArray>(result);
515    }
516
517    #[test]
518    fn test_repeat_string_array_overflow() {
519        let strings: ArrayRef = Arc::new(StringArray::from(vec![Some("abc")]));
520        let counts: ArrayRef = Arc::new(Int64Array::from(vec![Some(i64::MAX)]));
521
522        let err = super::repeat(&strings, &counts).unwrap_err().to_string();
523        assert!(
524            err.contains("string size overflow on repeat"),
525            "unexpected error: {err}"
526        );
527    }
528
529    #[test]
530    fn test_repeat_sliced_large_string_with_null_offset() {
531        let (strings, counts) =
532            sliced_offset_inputs(|v| Arc::new(LargeStringArray::from(v)));
533        let result = super::repeat(&strings, &counts).unwrap();
534        assert_sliced_offset_output::<LargeStringArray>(result);
535    }
536
537    #[test]
538    fn test_repeat_sliced_string_view_with_null_offset() {
539        let (strings, counts) =
540            sliced_offset_inputs(|v| Arc::new(StringViewArray::from(v)));
541        let result = super::repeat(&strings, &counts).unwrap();
542        assert_sliced_offset_output::<StringViewArray>(result);
543    }
544}