Skip to main content

datafusion_functions/regex/
regexpreplace.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
18//! Regex expressions
19use memchr::memchr;
20
21use arrow::array::ArrayDataBuilder;
22use arrow::array::BufferBuilder;
23use arrow::array::GenericStringArray;
24use arrow::array::StringViewBuilder;
25use arrow::array::{Array, ArrayRef, OffsetSizeTrait};
26use arrow::array::{ArrayAccessor, StringViewArray};
27use arrow::array::{ArrayIter, AsArray, new_null_array};
28use arrow::datatypes::DataType;
29use datafusion_common::ScalarValue;
30use datafusion_common::cast::{
31    as_large_string_array, as_string_array, as_string_view_array,
32};
33use datafusion_common::exec_err;
34use datafusion_common::plan_err;
35use datafusion_common::{
36    DataFusionError, Result, cast::as_generic_string_array, internal_err,
37};
38use datafusion_expr::ColumnarValue;
39use datafusion_expr::TypeSignature;
40use datafusion_expr::function::Hint;
41use datafusion_expr::{
42    Documentation, ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility,
43};
44use datafusion_macros::user_doc;
45use regex::{CaptureLocations, Regex};
46use std::borrow::Cow;
47use std::collections::HashMap;
48use std::sync::{Arc, LazyLock};
49
50#[user_doc(
51    doc_section(label = "Regular Expression Functions"),
52    description = "Replaces substrings in a string that match a [regular expression](https://docs.rs/regex/latest/regex/#syntax).",
53    syntax_example = "regexp_replace(str, regexp, replacement[, flags])",
54    sql_example = r#"```sql
55> select regexp_replace('foobarbaz', 'b(..)', 'X\\1Y', 'g');
56+------------------------------------------------------------------------+
57| regexp_replace(Utf8("foobarbaz"),Utf8("b(..)"),Utf8("X\1Y"),Utf8("g")) |
58+------------------------------------------------------------------------+
59| fooXarYXazY                                                            |
60+------------------------------------------------------------------------+
61SELECT regexp_replace('aBc', '(b|d)', 'Ab\\1a', 'i');
62+-------------------------------------------------------------------+
63| regexp_replace(Utf8("aBc"),Utf8("(b|d)"),Utf8("Ab\1a"),Utf8("i")) |
64+-------------------------------------------------------------------+
65| aAbBac                                                            |
66+-------------------------------------------------------------------+
67```
68Additional examples can be found [here](https://github.com/apache/datafusion/blob/main/datafusion-examples/examples/builtin_functions/regexp.rs)
69"#,
70    standard_argument(name = "str", prefix = "String"),
71    argument(
72        name = "regexp",
73        description = "Regular expression to match against.
74  Can be a constant, column, or function."
75    ),
76    argument(
77        name = "replacement",
78        description = "Replacement string expression to operate on. Can be a constant, column, or function, and any combination of operators."
79    ),
80    argument(
81        name = "flags",
82        description = r#"Optional regular expression flags that control the behavior of the regular expression. The following flags are supported:
83- **g**: (global) Search globally and don't return after the first match
84- **i**: case-insensitive: letters match both upper and lower case
85- **m**: multi-line mode: ^ and $ match begin/end of line
86- **s**: allow . to match \n
87- **R**: enables CRLF mode: when multi-line mode is enabled, \r\n is used
88- **U**: swap the meaning of x* and x*?"#
89    )
90)]
91#[derive(Debug, PartialEq, Eq, Hash)]
92pub struct RegexpReplaceFunc {
93    signature: Signature,
94}
95impl Default for RegexpReplaceFunc {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101impl RegexpReplaceFunc {
102    pub fn new() -> Self {
103        use DataType::*;
104        use TypeSignature::*;
105        Self {
106            signature: Signature::one_of(
107                vec![
108                    Uniform(3, vec![Utf8View, LargeUtf8, Utf8]),
109                    Uniform(4, vec![Utf8View, LargeUtf8, Utf8]),
110                ],
111                Volatility::Immutable,
112            ),
113        }
114    }
115}
116
117impl ScalarUDFImpl for RegexpReplaceFunc {
118    fn name(&self) -> &str {
119        "regexp_replace"
120    }
121
122    fn signature(&self) -> &Signature {
123        &self.signature
124    }
125
126    fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
127        use DataType::*;
128        Ok(match &arg_types[0] {
129            LargeUtf8 | LargeBinary => LargeUtf8,
130            Utf8 | Binary => Utf8,
131            Utf8View | BinaryView => Utf8View,
132            Null => Null,
133            Dictionary(_, t) => match **t {
134                LargeUtf8 | LargeBinary => LargeUtf8,
135                Utf8 | Binary => Utf8,
136                Null => Null,
137                _ => {
138                    return plan_err!(
139                        "the regexp_replace can only accept strings but got {:?}",
140                        **t
141                    );
142                }
143            },
144            other => {
145                return plan_err!(
146                    "The regexp_replace function can only accept strings. Got {other}"
147                );
148            }
149        })
150    }
151
152    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
153        let args = &args.args;
154
155        let len = args
156            .iter()
157            .fold(Option::<usize>::None, |acc, arg| match arg {
158                ColumnarValue::Scalar(_) => acc,
159                ColumnarValue::Array(a) => Some(a.len()),
160            });
161
162        let is_scalar = len.is_none();
163        let result = regexp_replace_func(args);
164        if is_scalar {
165            // If all inputs are scalar, keeps output as scalar
166            let result = result.and_then(|arr| ScalarValue::try_from_array(&arr, 0));
167            result.map(ColumnarValue::Scalar)
168        } else {
169            result.map(ColumnarValue::Array)
170        }
171    }
172
173    fn documentation(&self) -> Option<&Documentation> {
174        self.doc()
175    }
176}
177
178fn regexp_replace_func(args: &[ColumnarValue]) -> Result<ArrayRef> {
179    match args[0].data_type() {
180        DataType::Utf8 => specialize_regexp_replace::<i32>(args),
181        DataType::LargeUtf8 => specialize_regexp_replace::<i64>(args),
182        DataType::Utf8View => specialize_regexp_replace::<i32>(args),
183        other => {
184            internal_err!("Unsupported data type {other:?} for function regexp_replace")
185        }
186    }
187}
188
189/// replace POSIX capture groups (like \1 or \\1) with Rust Regex group (like ${1})
190/// used by regexp_replace
191/// Handles both single backslash (\1) and double backslash (\\1) which can occur
192/// when SQL strings with escaped backslashes are passed through
193///
194/// Note: \0 is converted to ${0}, which in Rust's regex replacement syntax
195/// substitutes the entire match. This is consistent with POSIX behavior where
196/// \0 (or &) refers to the entire matched string.
197fn regex_replace_posix_groups(replacement: &str) -> String {
198    static CAPTURE_GROUPS_RE_LOCK: LazyLock<Regex> =
199        LazyLock::new(|| Regex::new(r"\\{1,2}(\d+)").unwrap());
200    CAPTURE_GROUPS_RE_LOCK
201        .replace_all(replacement, "$${$1}")
202        .into_owned()
203}
204
205struct ShortRegex {
206    /// Shortened anchored regex used to extract capture group 1 directly.
207    /// See [`try_build_short_extract_regex`] for details.
208    short_re: Regex,
209    /// Reusable capture locations for `short_re` to avoid per-row allocation.
210    locs: CaptureLocations,
211}
212
213/// Holds the normal compiled regex together with the optional fast path used
214/// for `regexp_replace(str, '^...(capture)...*$', '\1')`.
215struct OptimizedRegex {
216    /// Full regex used for the normal replacement path and as a correctness fallback.
217    re: Regex,
218    /// Precomputed state for the direct-extraction fast path, when applicable.
219    short_re: Option<ShortRegex>,
220}
221
222impl OptimizedRegex {
223    /// Builds any reusable state needed by the extraction fast path.
224    ///
225    /// The fast path is only enabled for single replacements where the pattern
226    /// and replacement satisfy [`try_build_short_extract_regex`].
227    fn new(re: Regex, limit: usize, pattern: &str, replacement: &str) -> Self {
228        let short_re = if limit == 1 {
229            try_build_short_extract_regex(pattern, replacement)
230        } else {
231            None
232        };
233
234        let short_re = short_re.map(|short_re| {
235            let locs = short_re.capture_locations();
236            ShortRegex { short_re, locs }
237        });
238
239        Self { re, short_re }
240    }
241
242    /// Applies the direct-extraction fast path when it preserves the result of
243    /// `Regex::replacen`; otherwise falls back to the full regex replacement.
244    fn replacen<'a>(
245        &mut self,
246        val: &'a str,
247        limit: usize,
248        replacement: &str,
249    ) -> Cow<'a, str> {
250        // If this pattern is not eligible for direct extraction, use the full regex.
251        let Some(ShortRegex { short_re, locs }) = self.short_re.as_mut() else {
252            return self.re.replacen(val, limit, replacement);
253        };
254
255        // If the shortened regex does not match, the original anchored regex would
256        // also leave the input unchanged.
257        if short_re.captures_read(locs, val).is_none() {
258            return Cow::Borrowed(val);
259        };
260
261        // `captures_read` succeeded, so the overall shortened match is present.
262        let match_end = locs.get(0).unwrap().1;
263        if memchr(b'\n', &val.as_bytes()[match_end..]).is_some() {
264            // If there is a newline after the match, we can't use the short
265            // regex since it won't match across lines. Fall back to the full
266            // regex replacement.
267            return self.re.replacen(val, limit, replacement);
268        };
269        // The fast path only applies to `${1}` replacements, so the result is
270        // either capture group 1 or the empty string if that group did not match.
271        if let Some((start, end)) = locs.get(1) {
272            Cow::Borrowed(&val[start..end])
273        } else {
274            Cow::Borrowed("")
275        }
276    }
277}
278
279/// For anchored patterns like `^...(capture)....*$` where the replacement
280/// is `\1`, build a shorter regex (stripping trailing `.*$`) and use
281/// `captures_read` with `CaptureLocations` for direct extraction — no
282/// `expand()`, no `String` allocation.
283/// This pattern appears in ClickBench Q28: which uses a regexp like
284/// `^https?://(?:www\.)?([^/]+)/.*$`
285fn try_build_short_extract_regex(pattern: &str, replacement: &str) -> Option<Regex> {
286    if replacement != "${1}" || !pattern.starts_with('^') || !pattern.ends_with(".*$") {
287        return None;
288    }
289    let short = &pattern[..pattern.len() - 3];
290    let re = Regex::new(short).ok()?;
291    if re.captures_len() != 2 {
292        return None;
293    }
294    Some(re)
295}
296
297/// Replaces substring(s) matching a PCRE-like regular expression.
298///
299/// The full list of supported features and syntax can be found at
300/// <https://docs.rs/regex/latest/regex/#syntax>
301///
302/// Supported flags with the addition of 'g' can be found at
303/// <https://docs.rs/regex/latest/regex/#grouping-and-flags>
304///
305/// # Examples
306///
307/// ```ignore
308/// # use datafusion::prelude::*;
309/// # use datafusion::error::Result;
310/// # #[tokio::main]
311/// # async fn main() -> Result<()> {
312/// let ctx = SessionContext::new();
313/// let df = ctx.read_csv("tests/data/regex.csv", CsvReadOptions::new()).await?;
314///
315/// // use the regexp_replace function to replace substring(s) without flags
316/// let df = df.with_column(
317///     "a",
318///     regexp_replace(vec![col("values"), col("patterns"), col("replacement")])
319/// )?;
320/// // use the regexp_replace function to replace substring(s) with flags
321/// let df = df.with_column(
322///     "b",
323///     regexp_replace(vec![col("values"), col("patterns"), col("replacement"), col("flags")]),
324/// )?;
325///
326/// // literals can be used as well
327/// let df = df.with_column(
328///     "c",
329///     regexp_replace(vec![lit("foobarbequebaz"), lit("(bar)(beque)"), lit(r"\2")]),
330/// )?;
331///
332/// df.show().await?;
333///
334/// # Ok(())
335/// # }
336/// ```
337pub fn regexp_replace<'a, T: OffsetSizeTrait, U>(
338    string_array: U,
339    pattern_array: U,
340    replacement_array: U,
341    flags_array: Option<U>,
342) -> Result<ArrayRef>
343where
344    U: ArrayAccessor<Item = &'a str>,
345{
346    // Default implementation for regexp_replace, assumes all args are arrays
347    // and args is a sequence of 3 or 4 elements.
348
349    // creating Regex is expensive so create hashmap for memoization
350    let mut patterns: HashMap<String, Regex> = HashMap::new();
351
352    let datatype = string_array.data_type().to_owned();
353
354    let string_array_iter = ArrayIter::new(string_array);
355    let pattern_array_iter = ArrayIter::new(pattern_array);
356    let replacement_array_iter = ArrayIter::new(replacement_array);
357
358    match flags_array {
359        None => {
360            let result_iter = string_array_iter
361                .zip(pattern_array_iter)
362                .zip(replacement_array_iter)
363                .map(|((string, pattern), replacement)| {
364                    match (string, pattern, replacement) {
365                        (Some(string), Some(pattern), Some(replacement)) => {
366                            let replacement = regex_replace_posix_groups(replacement);
367                            // if patterns hashmap already has regexp then use else create and return
368                            let re = match patterns.get(pattern) {
369                                Some(re) => Ok(re),
370                                None => match Regex::new(pattern) {
371                                    Ok(re) => {
372                                        patterns.insert(pattern.to_string(), re);
373                                        Ok(patterns.get(pattern).unwrap())
374                                    }
375                                    Err(err) => {
376                                        Err(DataFusionError::External(Box::new(err)))
377                                    }
378                                },
379                            };
380
381                            Some(re.map(|re| re.replace(string, replacement.as_str())))
382                                .transpose()
383                        }
384                        _ => Ok(None),
385                    }
386                });
387
388            match datatype {
389                DataType::Utf8 | DataType::LargeUtf8 => {
390                    let result =
391                        result_iter.collect::<Result<GenericStringArray<T>>>()?;
392                    Ok(Arc::new(result) as ArrayRef)
393                }
394                DataType::Utf8View => {
395                    let result = result_iter.collect::<Result<StringViewArray>>()?;
396                    Ok(Arc::new(result) as ArrayRef)
397                }
398                other => {
399                    exec_err!(
400                        "Unsupported data type {other:?} for function regex_replace"
401                    )
402                }
403            }
404        }
405        Some(flags_array) => {
406            let flags_array_iter = ArrayIter::new(flags_array);
407
408            let result_iter = string_array_iter
409                .zip(pattern_array_iter)
410                .zip(replacement_array_iter)
411                .zip(flags_array_iter)
412                .map(|(((string, pattern), replacement), flags)| {
413                    match (string, pattern, replacement, flags) {
414                        (Some(string), Some(pattern), Some(replacement), Some(flags)) => {
415                            let replacement = regex_replace_posix_groups(replacement);
416
417                            // format flags into rust pattern
418                            let (pattern, replace_all) = if flags == "g" {
419                                (pattern.to_string(), true)
420                            } else if flags.contains('g') {
421                                (
422                                    format!(
423                                        "(?{}){}",
424                                        flags.to_string().replace('g', ""),
425                                        pattern
426                                    ),
427                                    true,
428                                )
429                            } else {
430                                (format!("(?{flags}){pattern}"), false)
431                            };
432
433                            // if patterns hashmap already has regexp then use else create and return
434                            let re = match patterns.get(&pattern) {
435                                Some(re) => Ok(re),
436                                None => match Regex::new(pattern.as_str()) {
437                                    Ok(re) => {
438                                        patterns.insert(pattern.clone(), re);
439                                        Ok(patterns.get(&pattern).unwrap())
440                                    }
441                                    Err(err) => {
442                                        Err(DataFusionError::External(Box::new(err)))
443                                    }
444                                },
445                            };
446
447                            Some(re.map(|re| {
448                                if replace_all {
449                                    re.replace_all(string, replacement.as_str())
450                                } else {
451                                    re.replace(string, replacement.as_str())
452                                }
453                            }))
454                            .transpose()
455                        }
456                        _ => Ok(None),
457                    }
458                });
459
460            match datatype {
461                DataType::Utf8 | DataType::LargeUtf8 => {
462                    let result =
463                        result_iter.collect::<Result<GenericStringArray<T>>>()?;
464                    Ok(Arc::new(result) as ArrayRef)
465                }
466                DataType::Utf8View => {
467                    let result = result_iter.collect::<Result<StringViewArray>>()?;
468                    Ok(Arc::new(result) as ArrayRef)
469                }
470                other => {
471                    exec_err!(
472                        "Unsupported data type {other:?} for function regex_replace"
473                    )
474                }
475            }
476        }
477    }
478}
479
480/// Get the first argument from the given string array.
481///
482/// Note: If the array is empty or the first argument is null,
483/// then aborts early.
484macro_rules! fetch_string_arg {
485    ($ARG:expr, $NAME:expr, $ARRAY_SIZE:expr) => {{
486        let string_array_type = ($ARG).data_type();
487        match string_array_type {
488            dt if $ARG.len() == 0 || $ARG.is_null(0) => {
489                // Mimicking the existing behavior of regexp_replace, if any of the scalar arguments
490                // are actually null, then the result will be an array of the same size as the first argument with all nulls.
491                //
492                // Also acts like an early abort mechanism when the input array is empty.
493                return Ok(new_null_array(dt, $ARRAY_SIZE));
494            }
495            DataType::Utf8 => {
496                let array = as_string_array($ARG)?;
497                array.value(0)
498            }
499            DataType::LargeUtf8 => {
500                let array = as_large_string_array($ARG)?;
501                array.value(0)
502            }
503            DataType::Utf8View => {
504                let array = as_string_view_array($ARG)?;
505                array.value(0)
506            }
507            _ => unreachable!(
508                "Invalid data type for regexp_replace: {}",
509                string_array_type
510            ),
511        }
512    }};
513}
514/// Special cased regex_replace implementation for the scenario where
515/// the pattern, replacement and flags are static (arrays that are derived
516/// from scalars). This means we can skip regex caching system and basically
517/// hold a single Regex object for the replace operation. This also speeds
518/// up the pre-processing time of the replacement string, since it only
519/// needs to processed once.
520fn regexp_replace_static_pattern_replace<T: OffsetSizeTrait>(
521    args: &[ArrayRef],
522) -> Result<ArrayRef> {
523    let array_size = args[0].len();
524    let pattern = fetch_string_arg!(&args[1], "pattern", array_size);
525    let replacement = fetch_string_arg!(&args[2], "replacement", array_size);
526    let flags = match args.len() {
527        3 => None,
528        4 => Some(fetch_string_arg!(&args[3], "flags", array_size)),
529        other => {
530            return exec_err!(
531                "regexp_replace was called with {other} arguments. It requires at least 3 and at most 4."
532            );
533        }
534    };
535
536    // Embed the flag (if it exists) into the pattern. Limit will determine
537    // whether this is a global match (as in replace all) or just a single
538    // replace operation.
539    let (pattern, limit) = match flags {
540        Some("g") => (pattern.to_string(), 0),
541        Some(flags) => (
542            format!("(?{}){}", flags.to_string().replace('g', ""), pattern),
543            !flags.contains('g') as usize,
544        ),
545        None => (pattern.to_string(), 1),
546    };
547
548    let re =
549        Regex::new(&pattern).map_err(|err| DataFusionError::External(Box::new(err)))?;
550
551    // Replaces the posix groups in the replacement string
552    // with rust ones.
553    let replacement = regex_replace_posix_groups(replacement);
554
555    let mut opt_re = OptimizedRegex::new(re, limit, &pattern, &replacement);
556
557    let string_array_type = args[0].data_type();
558    match string_array_type {
559        DataType::Utf8 | DataType::LargeUtf8 => {
560            let string_array = as_generic_string_array::<T>(&args[0])?;
561
562            // We are going to create the underlying string buffer from its parts
563            // to be able to re-use the existing null buffer for sparse arrays.
564            let mut vals = BufferBuilder::<u8>::new({
565                let offsets = string_array.value_offsets();
566                (offsets[string_array.len()] - offsets[0])
567                    .to_usize()
568                    .unwrap()
569            });
570            let mut new_offsets = BufferBuilder::<T>::new(string_array.len() + 1);
571            new_offsets.append(T::zero());
572
573            string_array.iter().for_each(|val| {
574                if let Some(val) = val {
575                    let result = opt_re.replacen(val, limit, replacement.as_str());
576                    vals.append_slice(result.as_bytes());
577                }
578                new_offsets.append(T::from_usize(vals.len()).unwrap());
579            });
580
581            let data = ArrayDataBuilder::new(GenericStringArray::<T>::DATA_TYPE)
582                .len(string_array.len())
583                .nulls(string_array.nulls().cloned())
584                .buffers(vec![new_offsets.finish(), vals.finish()])
585                .build()?;
586            let result_array = GenericStringArray::<T>::from(data);
587            Ok(Arc::new(result_array) as ArrayRef)
588        }
589        DataType::Utf8View => {
590            let string_view_array = as_string_view_array(&args[0])?;
591
592            let mut builder = StringViewBuilder::with_capacity(string_view_array.len());
593
594            for val in string_view_array.iter() {
595                if let Some(val) = val {
596                    let result = opt_re.replacen(val, limit, replacement.as_str());
597                    builder.append_value(result.as_ref());
598                } else {
599                    builder.append_null();
600                }
601            }
602
603            let result = builder.finish();
604            Ok(Arc::new(result) as ArrayRef)
605        }
606        _ => unreachable!(
607            "Invalid data type for regexp_replace: {}",
608            string_array_type
609        ),
610    }
611}
612
613/// Determine which implementation of the regexp_replace to use based
614/// on the given set of arguments.
615fn specialize_regexp_replace<T: OffsetSizeTrait>(
616    args: &[ColumnarValue],
617) -> Result<ArrayRef> {
618    // This will serve as a dispatch table where we can
619    // leverage it in order to determine whether the scalarity
620    // of the given set of arguments fits a better specialized
621    // function.
622    let (is_source_scalar, is_pattern_scalar, is_replacement_scalar, is_flags_scalar) = (
623        matches!(args[0], ColumnarValue::Scalar(_)),
624        matches!(args[1], ColumnarValue::Scalar(_)),
625        matches!(args[2], ColumnarValue::Scalar(_)),
626        // The forth argument (flags) is optional; so in the event that
627        // it is not available, we'll claim that it is scalar.
628        matches!(args.get(3), Some(ColumnarValue::Scalar(_)) | None),
629    );
630    let len = args
631        .iter()
632        .fold(Option::<usize>::None, |acc, arg| match arg {
633            ColumnarValue::Scalar(_) => acc,
634            ColumnarValue::Array(a) => Some(a.len()),
635        });
636    let inferred_length = len.unwrap_or(1);
637    match (
638        is_source_scalar,
639        is_pattern_scalar,
640        is_replacement_scalar,
641        is_flags_scalar,
642    ) {
643        // This represents a very hot path for the case where the there is
644        // a single pattern that is being matched against and a single replacement.
645        // This is extremely important to specialize on since it removes the overhead
646        // of DF's in-house regex pattern cache (since there will be at most a single
647        // pattern) and the pre-processing of the same replacement pattern at each
648        // query.
649        //
650        // The flags needs to be a scalar as well since each pattern is actually
651        // constructed with the flags embedded into the pattern itself. This means
652        // even if the pattern itself is scalar, if the flags are an array then
653        // we will create many regexes and it is best to use the implementation
654        // that caches it. If there are no flags, we can simply ignore it here,
655        // and let the specialized function handle it.
656        (_, true, true, true) => {
657            let hints = [
658                Hint::Pad,
659                Hint::AcceptsSingular,
660                Hint::AcceptsSingular,
661                Hint::AcceptsSingular,
662            ];
663            let args = args
664                .iter()
665                .zip(hints.iter().chain(std::iter::repeat(&Hint::Pad)))
666                .map(|(arg, hint)| {
667                    // Decide on the length to expand this scalar to depending
668                    // on the given hints.
669                    let expansion_len = match hint {
670                        Hint::AcceptsSingular => 1,
671                        Hint::Pad => inferred_length,
672                    };
673                    arg.to_array(expansion_len)
674                })
675                .collect::<Result<Vec<_>>>()?;
676            regexp_replace_static_pattern_replace::<T>(&args)
677        }
678
679        // If there are no specialized implementations, we'll fall back to the
680        // generic implementation.
681        (_, _, _, _) => {
682            let args = args
683                .iter()
684                .map(|arg| arg.to_array(inferred_length))
685                .collect::<Result<Vec<_>>>()?;
686
687            match (
688                args[0].data_type(),
689                args[1].data_type(),
690                args[2].data_type(),
691                args.get(3).map(|a| a.data_type()),
692            ) {
693                (
694                    DataType::Utf8,
695                    DataType::Utf8,
696                    DataType::Utf8,
697                    Some(DataType::Utf8) | None,
698                ) => {
699                    let string_array = args[0].as_string::<i32>();
700                    let pattern_array = args[1].as_string::<i32>();
701                    let replacement_array = args[2].as_string::<i32>();
702                    let flags_array = args.get(3).map(|a| a.as_string::<i32>());
703                    regexp_replace::<i32, _>(
704                        string_array,
705                        pattern_array,
706                        replacement_array,
707                        flags_array,
708                    )
709                }
710                (
711                    DataType::Utf8View,
712                    DataType::Utf8View,
713                    DataType::Utf8View,
714                    Some(DataType::Utf8View) | None,
715                ) => {
716                    let string_array = args[0].as_string_view();
717                    let pattern_array = args[1].as_string_view();
718                    let replacement_array = args[2].as_string_view();
719                    let flags_array = args.get(3).map(|a| a.as_string_view());
720                    regexp_replace::<i32, _>(
721                        string_array,
722                        pattern_array,
723                        replacement_array,
724                        flags_array,
725                    )
726                }
727                (
728                    DataType::LargeUtf8,
729                    DataType::LargeUtf8,
730                    DataType::LargeUtf8,
731                    Some(DataType::LargeUtf8) | None,
732                ) => {
733                    let string_array = args[0].as_string::<i64>();
734                    let pattern_array = args[1].as_string::<i64>();
735                    let replacement_array = args[2].as_string::<i64>();
736                    let flags_array = args.get(3).map(|a| a.as_string::<i64>());
737                    regexp_replace::<i64, _>(
738                        string_array,
739                        pattern_array,
740                        replacement_array,
741                        flags_array,
742                    )
743                }
744                other => {
745                    exec_err!(
746                        "Unsupported data type {other:?} for function regex_replace"
747                    )
748                }
749            }
750        }
751    }
752}
753#[cfg(test)]
754mod tests {
755    use arrow::array::*;
756
757    use super::*;
758
759    #[test]
760    fn test_regex_replace_posix_groups() {
761        // Test that \1, \2, etc. are replaced with ${1}, ${2}, etc.
762        assert_eq!(regex_replace_posix_groups(r"\1"), "${1}");
763        assert_eq!(regex_replace_posix_groups(r"\12"), "${12}");
764        assert_eq!(regex_replace_posix_groups(r"X\1Y"), "X${1}Y");
765        assert_eq!(regex_replace_posix_groups(r"\1\2"), "${1}${2}");
766
767        // Test double backslash (from SQL escaped strings like '\\1')
768        assert_eq!(regex_replace_posix_groups(r"\\1"), "${1}");
769        assert_eq!(regex_replace_posix_groups(r"X\\1Y"), "X${1}Y");
770        assert_eq!(regex_replace_posix_groups(r"\\1\\2"), "${1}${2}");
771
772        // Test 3 or 4 backslashes before digits to document expected behavior
773        assert_eq!(regex_replace_posix_groups(r"\\\1"), r"\${1}");
774        assert_eq!(regex_replace_posix_groups(r"\\\\1"), r"\\${1}");
775        assert_eq!(regex_replace_posix_groups(r"\\\1\\\\2"), r"\${1}\\${2}");
776
777        // Test that a lone backslash is NOT replaced (requires at least one digit)
778        assert_eq!(regex_replace_posix_groups(r"\"), r"\");
779        assert_eq!(regex_replace_posix_groups(r"foo\bar"), r"foo\bar");
780
781        // Test that backslash followed by non-digit is preserved
782        assert_eq!(regex_replace_posix_groups(r"\n"), r"\n");
783        assert_eq!(regex_replace_posix_groups(r"\t"), r"\t");
784
785        // Test \0 behavior: \0 is converted to ${0}, which in Rust's regex
786        // replacement syntax substitutes the entire match. This is consistent
787        // with POSIX behavior where \0 (or &) refers to the entire matched string.
788        assert_eq!(regex_replace_posix_groups(r"\0"), "${0}");
789        assert_eq!(
790            regex_replace_posix_groups(r"prefix\0suffix"),
791            "prefix${0}suffix"
792        );
793    }
794
795    macro_rules! static_pattern_regexp_replace {
796        ($name:ident, $T:ty, $O:ty) => {
797            #[test]
798            fn $name() {
799                let values = vec!["abc", "acd", "abcd1234567890123", "123456789012abc"];
800                let patterns = vec!["b"; 4];
801                let replacement = vec!["foo"; 4];
802                let expected =
803                    vec!["afooc", "acd", "afoocd1234567890123", "123456789012afooc"];
804
805                let values = <$T>::from(values);
806                let patterns = <$T>::from(patterns);
807                let replacements = <$T>::from(replacement);
808                let expected = <$T>::from(expected);
809
810                let re = regexp_replace_static_pattern_replace::<$O>(&[
811                    Arc::new(values),
812                    Arc::new(patterns),
813                    Arc::new(replacements),
814                ])
815                .unwrap();
816
817                assert_eq!(re.as_ref(), &expected);
818            }
819        };
820    }
821
822    static_pattern_regexp_replace!(string_array, StringArray, i32);
823    static_pattern_regexp_replace!(string_view_array, StringViewArray, i32);
824    static_pattern_regexp_replace!(large_string_array, LargeStringArray, i64);
825
826    macro_rules! static_pattern_regexp_replace_with_flags {
827        ($name:ident, $T:ty, $O: ty) => {
828            #[test]
829            fn $name() {
830                let values = vec![
831                    "abc",
832                    "aBc",
833                    "acd",
834                    "abcd1234567890123",
835                    "aBcd1234567890123",
836                    "123456789012abc",
837                    "123456789012aBc",
838                ];
839                let expected = vec![
840                    "afooc",
841                    "afooc",
842                    "acd",
843                    "afoocd1234567890123",
844                    "afoocd1234567890123",
845                    "123456789012afooc",
846                    "123456789012afooc",
847                ];
848
849                let values = <$T>::from(values);
850                let patterns = StringArray::from(vec!["b"; 7]);
851                let replacements = StringArray::from(vec!["foo"; 7]);
852                let flags = StringArray::from(vec!["i"; 5]);
853                let expected = <$T>::from(expected);
854
855                let re = regexp_replace_static_pattern_replace::<$O>(&[
856                    Arc::new(values),
857                    Arc::new(patterns),
858                    Arc::new(replacements),
859                    Arc::new(flags),
860                ])
861                .unwrap();
862
863                assert_eq!(re.as_ref(), &expected);
864            }
865        };
866    }
867
868    static_pattern_regexp_replace_with_flags!(string_array_with_flags, StringArray, i32);
869    static_pattern_regexp_replace_with_flags!(
870        string_view_array_with_flags,
871        StringViewArray,
872        i32
873    );
874    static_pattern_regexp_replace_with_flags!(
875        large_string_array_with_flags,
876        LargeStringArray,
877        i64
878    );
879
880    #[test]
881    fn test_static_pattern_regexp_replace_early_abort() {
882        let values = StringArray::from(vec!["abc"; 5]);
883        let patterns = StringArray::from(vec![None::<&str>; 5]);
884        let replacements = StringArray::from(vec!["foo"; 5]);
885        let expected = StringArray::from(vec![None::<&str>; 5]);
886
887        let re = regexp_replace_static_pattern_replace::<i32>(&[
888            Arc::new(values),
889            Arc::new(patterns),
890            Arc::new(replacements),
891        ])
892        .unwrap();
893
894        assert_eq!(re.as_ref(), &expected);
895    }
896
897    #[test]
898    fn test_static_pattern_regexp_replace_early_abort_when_empty() {
899        let values = StringArray::from(Vec::<Option<&str>>::new());
900        let patterns = StringArray::from(Vec::<Option<&str>>::new());
901        let replacements = StringArray::from(Vec::<Option<&str>>::new());
902        let expected = StringArray::from(Vec::<Option<&str>>::new());
903
904        let re = regexp_replace_static_pattern_replace::<i32>(&[
905            Arc::new(values),
906            Arc::new(patterns),
907            Arc::new(replacements),
908        ])
909        .unwrap();
910
911        assert_eq!(re.as_ref(), &expected);
912    }
913
914    #[test]
915    fn test_static_pattern_regexp_replace_early_abort_flags() {
916        let values = StringArray::from(vec!["abc"; 5]);
917        let patterns = StringArray::from(vec!["a"; 5]);
918        let replacements = StringArray::from(vec!["foo"; 5]);
919        let flags = StringArray::from(vec![None::<&str>; 5]);
920        let expected = StringArray::from(vec![None::<&str>; 5]);
921
922        let re = regexp_replace_static_pattern_replace::<i32>(&[
923            Arc::new(values),
924            Arc::new(patterns),
925            Arc::new(replacements),
926            Arc::new(flags),
927        ])
928        .unwrap();
929
930        assert_eq!(re.as_ref(), &expected);
931    }
932
933    #[test]
934    fn test_static_pattern_regexp_replace_pattern_error() {
935        let values = StringArray::from(vec!["abc"; 5]);
936        // Deliberately using an invalid pattern to see how the single pattern
937        // error is propagated on regexp_replace.
938        let patterns = StringArray::from(vec!["["; 5]);
939        let replacements = StringArray::from(vec!["foo"; 5]);
940
941        let re = regexp_replace_static_pattern_replace::<i32>(&[
942            Arc::new(values),
943            Arc::new(patterns),
944            Arc::new(replacements),
945        ]);
946        let pattern_err = re.expect_err("broken pattern should have failed");
947        assert_eq!(
948            pattern_err.strip_backtrace(),
949            "External error: regex parse error:\n    [\n    ^\nerror: unclosed character class"
950        );
951    }
952
953    #[test]
954    fn test_static_pattern_regexp_replace_with_null_buffers() {
955        let values = StringArray::from(vec![
956            Some("a"),
957            None,
958            Some("b"),
959            None,
960            Some("a"),
961            None,
962            None,
963            Some("c"),
964        ]);
965        let patterns = StringArray::from(vec!["a"; 1]);
966        let replacements = StringArray::from(vec!["foo"; 1]);
967        let expected = StringArray::from(vec![
968            Some("foo"),
969            None,
970            Some("b"),
971            None,
972            Some("foo"),
973            None,
974            None,
975            Some("c"),
976        ]);
977
978        let re = regexp_replace_static_pattern_replace::<i32>(&[
979            Arc::new(values),
980            Arc::new(patterns),
981            Arc::new(replacements),
982        ])
983        .unwrap();
984
985        assert_eq!(re.as_ref(), &expected);
986        assert_eq!(re.null_count(), 4);
987    }
988
989    #[test]
990    fn test_static_pattern_regexp_replace_with_sliced_null_buffer() {
991        let values = StringArray::from(vec![
992            Some("a"),
993            None,
994            Some("b"),
995            None,
996            Some("a"),
997            None,
998            None,
999            Some("c"),
1000        ]);
1001        let values = values.slice(2, 5);
1002        let patterns = StringArray::from(vec!["a"; 1]);
1003        let replacements = StringArray::from(vec!["foo"; 1]);
1004        let expected = StringArray::from(vec![Some("b"), None, Some("foo"), None, None]);
1005
1006        let re = regexp_replace_static_pattern_replace::<i32>(&[
1007            Arc::new(values),
1008            Arc::new(patterns),
1009            Arc::new(replacements),
1010        ])
1011        .unwrap();
1012        assert_eq!(re.as_ref(), &expected);
1013        assert_eq!(re.null_count(), 3);
1014    }
1015}