1use 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 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
206fn 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 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
241fn 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 if let (&[from_byte], &[to_byte]) = (from.as_bytes(), to.as_bytes())
280 && from_byte.is_ascii()
281 && to_byte.is_ascii()
282 {
283 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 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#[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#[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
354fn replace_scalar<'a, S, O>(haystack: S, from: &str, to: &str) -> Result<ArrayRef>
358where
359 S: StringArrayType<'a> + Copy,
360 O: OffsetSizeTrait,
361{
362 let nulls = haystack.nulls().cloned();
365 let finder = memmem::Finder::new(from.as_bytes());
367 build_replaced::<O>(haystack.len(), nulls, |builder, i| {
368 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 #[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 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 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 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}