Skip to main content

datafusion_functions/string/
replace.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::{ArrayRef, OffsetSizeTrait, StringArrayType};
21use arrow::buffer::NullBuffer;
22use arrow::datatypes::DataType;
23use memchr::memmem;
24
25use crate::strings::{GenericStringArrayBuilder, StringWriter};
26use crate::utils::{make_scalar_function, utf8_to_str_type};
27use datafusion_common::cast::{as_generic_string_array, as_string_view_array};
28use datafusion_common::types::logical_string;
29use datafusion_common::{Result, exec_err};
30use datafusion_expr::type_coercion::binary::{
31    binary_to_string_coercion, string_coercion,
32};
33use datafusion_expr::{
34    Coercion, ColumnarValue, Documentation, ScalarFunctionArgs, ScalarUDFImpl, Signature,
35    TypeSignatureClass, Volatility,
36};
37use datafusion_macros::user_doc;
38#[user_doc(
39    doc_section(label = "String Functions"),
40    description = "Replaces all occurrences of a specified substring in a string with a new substring.",
41    syntax_example = "replace(str, substr, replacement)",
42    sql_example = r#"```sql
43> select replace('ABabbaBA', 'ab', 'cd');
44+-------------------------------------------------+
45| replace(Utf8("ABabbaBA"),Utf8("ab"),Utf8("cd")) |
46+-------------------------------------------------+
47| ABcdbaBA                                        |
48+-------------------------------------------------+
49```"#,
50    standard_argument(name = "str", prefix = "String"),
51    standard_argument(
52        name = "substr",
53        prefix = "Substring expression to replace in the input string. Substring"
54    ),
55    standard_argument(name = "replacement", prefix = "Replacement substring")
56)]
57#[derive(Debug, PartialEq, Eq, Hash)]
58pub struct ReplaceFunc {
59    signature: Signature,
60}
61
62impl Default for ReplaceFunc {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68impl ReplaceFunc {
69    pub fn new() -> Self {
70        Self {
71            signature: Signature::coercible(
72                vec![
73                    Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
74                    Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
75                    Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
76                ],
77                Volatility::Immutable,
78            ),
79        }
80    }
81}
82
83impl ScalarUDFImpl for ReplaceFunc {
84    fn name(&self) -> &str {
85        "replace"
86    }
87
88    fn signature(&self) -> &Signature {
89        &self.signature
90    }
91
92    fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
93        if let Some(coercion_data_type) = string_coercion(&arg_types[0], &arg_types[1])
94            .and_then(|dt| string_coercion(&dt, &arg_types[2]))
95            .or_else(|| {
96                binary_to_string_coercion(&arg_types[0], &arg_types[1])
97                    .and_then(|dt| binary_to_string_coercion(&dt, &arg_types[2]))
98            })
99        {
100            utf8_to_str_type(&coercion_data_type, "replace")
101        } else {
102            exec_err!(
103                "Unsupported data types for replace. Expected Utf8, LargeUtf8 or Utf8View"
104            )
105        }
106    }
107
108    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
109        let data_types = args
110            .args
111            .iter()
112            .map(|arg| arg.data_type())
113            .collect::<Vec<_>>();
114
115        if let Some(coercion_type) = string_coercion(&data_types[0], &data_types[1])
116            .and_then(|dt| string_coercion(&dt, &data_types[2]))
117            .or_else(|| {
118                binary_to_string_coercion(&data_types[0], &data_types[1])
119                    .and_then(|dt| binary_to_string_coercion(&dt, &data_types[2]))
120            })
121        {
122            let mut converted_args = Vec::with_capacity(args.args.len());
123            for arg in &args.args {
124                if arg.data_type() == coercion_type {
125                    converted_args.push(arg.clone());
126                } else {
127                    let converted = arg.cast_to(&coercion_type, None)?;
128                    converted_args.push(converted);
129                }
130            }
131
132            // Fast path: when `from` and `to` are non-null scalars we can
133            // pre-build a substring finder once and reuse it for every haystack
134            // row, mirroring the scalar-argument fast paths in
135            // `strpos`/`translate`/`split_part`.
136            if let (
137                ColumnarValue::Array(haystack),
138                ColumnarValue::Scalar(from),
139                ColumnarValue::Scalar(to),
140            ) = (&converted_args[0], &converted_args[1], &converted_args[2])
141                && let (Some(Some(from)), Some(Some(to))) =
142                    (from.try_as_str(), to.try_as_str())
143            {
144                let result = match coercion_type {
145                    DataType::Utf8 => replace_scalar::<_, i32>(
146                        as_generic_string_array::<i32>(haystack)?,
147                        from,
148                        to,
149                    ),
150                    DataType::LargeUtf8 => replace_scalar::<_, i64>(
151                        as_generic_string_array::<i64>(haystack)?,
152                        from,
153                        to,
154                    ),
155                    DataType::Utf8View => replace_scalar::<_, i32>(
156                        as_string_view_array(haystack)?,
157                        from,
158                        to,
159                    ),
160                    other => {
161                        return exec_err!(
162                            "Unsupported coercion data type {other:?} for function replace"
163                        );
164                    }
165                };
166                return result.map(ColumnarValue::Array);
167            }
168
169            match coercion_type {
170                DataType::Utf8 => {
171                    make_scalar_function(replace::<i32>, vec![])(&converted_args)
172                }
173                DataType::LargeUtf8 => {
174                    make_scalar_function(replace::<i64>, vec![])(&converted_args)
175                }
176                DataType::Utf8View => {
177                    make_scalar_function(replace_view, vec![])(&converted_args)
178                }
179                other => exec_err!(
180                    "Unsupported coercion data type {other:?} for function replace"
181                ),
182            }
183        } else {
184            exec_err!(
185                "Unsupported data type {}, {:?}, {:?} for function replace.",
186                data_types[0],
187                data_types[1],
188                data_types[2]
189            )
190        }
191    }
192
193    fn documentation(&self) -> Option<&Documentation> {
194        self.doc()
195    }
196}
197
198fn replace_view(args: &[ArrayRef]) -> Result<ArrayRef> {
199    let string_array = as_string_view_array(&args[0])?;
200    let from_array = as_string_view_array(&args[1])?;
201    let to_array = as_string_view_array(&args[2])?;
202
203    replace_arrays::<_, i32>(string_array, from_array, to_array)
204}
205
206/// Replaces all occurrences in string of substring from with substring to.
207/// replace('abcdefabcdef', 'cd', 'XX') = 'abXXefabXXef'
208fn replace<T: OffsetSizeTrait>(args: &[ArrayRef]) -> Result<ArrayRef> {
209    let string_array = as_generic_string_array::<T>(&args[0])?;
210    let from_array = as_generic_string_array::<T>(&args[1])?;
211    let to_array = as_generic_string_array::<T>(&args[2])?;
212
213    replace_arrays::<_, T>(string_array, from_array, to_array)
214}
215
216fn replace_arrays<'a, S, O>(
217    string_array: S,
218    from_array: S,
219    to_array: S,
220) -> Result<ArrayRef>
221where
222    S: StringArrayType<'a> + Copy,
223    O: OffsetSizeTrait,
224{
225    let len = string_array.len();
226    let nulls = NullBuffer::union_many([
227        string_array.nulls(),
228        from_array.nulls(),
229        to_array.nulls(),
230    ]);
231    build_replaced::<O>(len, nulls, |builder, i| {
232        // SAFETY: build_replaced only calls this for rows that are non-null in
233        // the union buffer, so every input array is non-null at i.
234        let string = unsafe { string_array.value_unchecked(i) };
235        let from = unsafe { from_array.value_unchecked(i) };
236        let to = unsafe { to_array.value_unchecked(i) };
237        apply_replace(builder, string, from, to, None)
238    })
239}
240
241/// Appends `len` rows to a fresh string builder: a null placeholder for each
242/// null row and `append_row` for each non-null row. The `nulls.is_some()` check
243/// is hoisted out of the loop so the all-non-null case does not depend on LLVM
244/// loop-unswitching heuristics.
245fn build_replaced<O: OffsetSizeTrait>(
246    len: usize,
247    nulls: Option<NullBuffer>,
248    mut append_row: impl FnMut(&mut GenericStringArrayBuilder<O>, usize) -> Result<()>,
249) -> Result<ArrayRef> {
250    let mut builder = GenericStringArrayBuilder::<O>::with_capacity(len, 0);
251    if let Some(nulls_ref) = nulls.as_ref() {
252        for i in 0..len {
253            if nulls_ref.is_null(i) {
254                builder.try_append_placeholder()?;
255            } else {
256                append_row(&mut builder, i)?;
257            }
258        }
259    } else {
260        for i in 0..len {
261            append_row(&mut builder, i)?;
262        }
263    }
264    Ok(Arc::new(builder.finish(nulls)?) as ArrayRef)
265}
266
267#[inline]
268fn apply_replace<O: OffsetSizeTrait>(
269    builder: &mut GenericStringArrayBuilder<O>,
270    string: &str,
271    from: &str,
272    to: &str,
273    finder: Option<&memmem::Finder>,
274) -> Result<()> {
275    // Hot path: single ASCII byte → single ASCII byte. An ASCII byte (< 0x80)
276    // cannot appear inside a multi-byte UTF-8 sequence, so any multi-byte
277    // sequences in `string` pass through unchanged and output stays valid
278    // UTF-8.
279    if let (&[from_byte], &[to_byte]) = (from.as_bytes(), to.as_bytes())
280        && from_byte.is_ascii()
281        && to_byte.is_ascii()
282    {
283        // SAFETY: see the contract above.
284        return unsafe {
285            builder.try_append_byte_map(string.as_bytes(), |b| {
286                if b == from_byte { to_byte } else { b }
287            })
288        };
289    }
290
291    if from.is_empty() {
292        // PostgreSQL returns the input unchanged when `from` is empty (#22253).
293        return builder.try_append_value(string);
294    }
295
296    builder.try_append_with(|w| replace_into_writer(w, string, from, to, finder))
297}
298
299/// Writes `string` into `w` with every non-overlapping occurrence of `from`
300/// replaced by `to`. When `finder` is `Some`, matches are located with the
301/// pre-built finder (the scalar fast path, where `from` is constant across all
302/// rows); otherwise `str::match_indices` builds a searcher per call.
303///
304/// Both `string` and `from` are valid UTF-8, and UTF-8 is self-synchronizing,
305/// so a byte match of `from` can only start on a char boundary of `string`; the
306/// slices below are therefore always valid.
307#[inline]
308fn replace_into_writer<W: StringWriter>(
309    w: &mut W,
310    string: &str,
311    from: &str,
312    to: &str,
313    finder: Option<&memmem::Finder>,
314) {
315    match finder {
316        Some(finder) => write_replaced(
317            w,
318            string,
319            to,
320            from.len(),
321            finder.find_iter(string.as_bytes()),
322        ),
323        None => write_replaced(
324            w,
325            string,
326            to,
327            from.len(),
328            string.match_indices(from).map(|(start, _)| start),
329        ),
330    }
331}
332
333/// Copies `string` into `w`, replacing the `from_len`-byte substring at each
334/// byte offset yielded by `starts` with `to`. `starts` must be ascending and
335/// non-overlapping, as produced by both `memmem::Finder::find_iter` and
336/// `str::match_indices`.
337#[inline]
338fn write_replaced<W: StringWriter>(
339    w: &mut W,
340    string: &str,
341    to: &str,
342    from_len: usize,
343    starts: impl Iterator<Item = usize>,
344) {
345    let mut last_end = 0;
346    for start in starts {
347        w.write_str(&string[last_end..start]);
348        w.write_str(to);
349        last_end = start + from_len;
350    }
351    w.write_str(&string[last_end..]);
352}
353
354/// Fast path for a `from`/`to` pair that is constant across all rows. The
355/// substring finder is built once and reused for every haystack value, which
356/// avoids the per-row searcher construction incurred by `str::match_indices`.
357fn replace_scalar<'a, S, O>(haystack: S, from: &str, to: &str) -> Result<ArrayRef>
358where
359    S: StringArrayType<'a> + Copy,
360    O: OffsetSizeTrait,
361{
362    // `from` and `to` are non-null scalars, so the output nulls are exactly the
363    // haystack's nulls (matching the null union computed by the general path).
364    let nulls = haystack.nulls().cloned();
365    // Built once and reused for every row.
366    let finder = memmem::Finder::new(from.as_bytes());
367    build_replaced::<O>(haystack.len(), nulls, |builder, i| {
368        // SAFETY: build_replaced only calls this for non-null rows.
369        let string = unsafe { haystack.value_unchecked(i) };
370        apply_replace(builder, string, from, to, Some(&finder))
371    })
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377    use crate::utils::test::test_function;
378    use arrow::array::Array;
379    use arrow::array::LargeStringArray;
380    use arrow::array::StringArray;
381    use arrow::datatypes::DataType::{LargeUtf8, Utf8};
382    use datafusion_common::ScalarValue;
383    #[test]
384    fn test_functions() -> Result<()> {
385        test_function!(
386            ReplaceFunc::new(),
387            vec![
388                ColumnarValue::Scalar(ScalarValue::Utf8(Some(String::from("aabbdqcbb")))),
389                ColumnarValue::Scalar(ScalarValue::Utf8(Some(String::from("bb")))),
390                ColumnarValue::Scalar(ScalarValue::Utf8(Some(String::from("ccc")))),
391            ],
392            Ok(Some("aacccdqcccc")),
393            &str,
394            Utf8,
395            StringArray
396        );
397
398        test_function!(
399            ReplaceFunc::new(),
400            vec![
401                ColumnarValue::Scalar(ScalarValue::LargeUtf8(Some(String::from(
402                    "aabbb"
403                )))),
404                ColumnarValue::Scalar(ScalarValue::LargeUtf8(Some(String::from("bbb")))),
405                ColumnarValue::Scalar(ScalarValue::LargeUtf8(Some(String::from("cc")))),
406            ],
407            Ok(Some("aacc")),
408            &str,
409            LargeUtf8,
410            LargeStringArray
411        );
412
413        test_function!(
414            ReplaceFunc::new(),
415            vec![
416                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from(
417                    "aabbbcw"
418                )))),
419                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from("bb")))),
420                ColumnarValue::Scalar(ScalarValue::Utf8View(Some(String::from("cc")))),
421            ],
422            Ok(Some("aaccbcw")),
423            &str,
424            Utf8,
425            StringArray
426        );
427
428        test_function!(
429            ReplaceFunc::new(),
430            vec![
431                ColumnarValue::Scalar(ScalarValue::LargeUtf8(Some(String::from("abc")))),
432                ColumnarValue::Scalar(ScalarValue::LargeUtf8(Some(String::from("")))),
433                ColumnarValue::Scalar(ScalarValue::LargeUtf8(Some(String::from("x")))),
434            ],
435            Ok(Some("abc")),
436            &str,
437            LargeUtf8,
438            LargeStringArray
439        );
440
441        Ok(())
442    }
443
444    /// The scalar-argument fast path must produce output that is bit-identical
445    /// to the general (array-argument) path for every kind of pattern.
446    #[test]
447    fn scalar_fast_path_matches_general() {
448        use arrow::array::{ArrayRef, StringViewArray};
449        use arrow::datatypes::Field;
450        use datafusion_common::config::ConfigOptions;
451        use std::sync::Arc;
452
453        let rows = vec![
454            Some("hello world"),
455            None,
456            Some("aaaa"),
457            Some(""),
458            Some("a.b.c.d"),
459            Some("úñîçödé abcúñ"),
460            Some("mississippi"),
461            Some("  double  spaces  "),
462        ];
463        // Covers byte-map (single ASCII → single ASCII), deletion (empty `to`),
464        // empty `from`, multi-byte `to`, and multi-byte non-ASCII `from`.
465        let cases = [
466            (" ", "_"),
467            ("a", "X"),
468            ("ss", "Z"),
469            ("", "Q"),
470            ("a", "yy"),
471            ("úñ", "A"),
472            (".", ""),
473            ("i", "II"),
474        ];
475
476        let invoke = |haystack: &ArrayRef,
477                      from: ColumnarValue,
478                      to: ColumnarValue|
479         -> ArrayRef {
480            let args = vec![ColumnarValue::Array(Arc::clone(haystack)), from, to];
481            let arg_fields = args
482                .iter()
483                .enumerate()
484                .map(|(i, a)| Field::new(format!("a{i}"), a.data_type(), true).into())
485                .collect();
486            match ReplaceFunc::new()
487                .invoke_with_args(ScalarFunctionArgs {
488                    args,
489                    arg_fields,
490                    number_rows: haystack.len(),
491                    return_field: Field::new("f", Utf8, true).into(),
492                    config_options: Arc::new(ConfigOptions::default()),
493                })
494                .unwrap()
495            {
496                ColumnarValue::Array(a) => a,
497                ColumnarValue::Scalar(s) => s.to_array_of_size(haystack.len()).unwrap(),
498            }
499        };
500
501        for (from, to) in cases {
502            let n = rows.len();
503            for haystack in [
504                Arc::new(StringArray::from(rows.clone())) as ArrayRef,
505                Arc::new(LargeStringArray::from(rows.clone())) as ArrayRef,
506                Arc::new(StringViewArray::from(rows.clone())) as ArrayRef,
507            ] {
508                // scalar `from`/`to` -> new fast path
509                let fast = invoke(
510                    &haystack,
511                    ColumnarValue::Scalar(ScalarValue::Utf8(Some(from.to_string()))),
512                    ColumnarValue::Scalar(ScalarValue::Utf8(Some(to.to_string()))),
513                );
514                // array `from`/`to` -> general path
515                let general = invoke(
516                    &haystack,
517                    ColumnarValue::Array(Arc::new(StringArray::from(vec![from; n]))),
518                    ColumnarValue::Array(Arc::new(StringArray::from(vec![to; n]))),
519                );
520                assert_eq!(
521                    &fast,
522                    &general,
523                    "mismatch for from={from:?} to={to:?} on {:?}",
524                    haystack.data_type()
525                );
526            }
527        }
528    }
529}