Skip to main content

datafusion_functions/regex/
regexplike.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
19
20use arrow::array::{Array, ArrayRef, AsArray, BooleanArray, GenericStringArray};
21use arrow::compute::kernels::regexp;
22use arrow::datatypes::DataType;
23use arrow::datatypes::DataType::{LargeUtf8, Utf8, Utf8View};
24use datafusion_common::types::logical_string;
25use datafusion_common::{
26    Result, ScalarValue, arrow_datafusion_err, exec_err, internal_err, plan_err,
27};
28use datafusion_expr::{
29    Coercion, ColumnarValue, Documentation, Expr, ScalarFunctionArgs, ScalarUDFImpl,
30    Signature, TypeSignature, TypeSignatureClass, Volatility, binary_expr, cast,
31};
32use datafusion_macros::user_doc;
33
34use datafusion_expr::simplify::{ExprSimplifyResult, SimplifyContext};
35use datafusion_expr_common::operator::Operator;
36use datafusion_expr_common::type_coercion::binary::BinaryTypeCoercer;
37use regex::Regex;
38use std::sync::Arc;
39
40#[user_doc(
41    doc_section(label = "Regular Expression Functions"),
42    description = "Returns true if a [regular expression](https://docs.rs/regex/latest/regex/#syntax) has at least one match in a string, false otherwise.",
43    syntax_example = "regexp_like(str, regexp[, flags])",
44    sql_example = r#"```sql
45select regexp_like('Köln', '[a-zA-Z]ö[a-zA-Z]{2}');
46+--------------------------------------------------------+
47| regexp_like(Utf8("Köln"),Utf8("[a-zA-Z]ö[a-zA-Z]{2}")) |
48+--------------------------------------------------------+
49| true                                                   |
50+--------------------------------------------------------+
51SELECT regexp_like('aBc', '(b|d)', 'i');
52+--------------------------------------------------+
53| regexp_like(Utf8("aBc"),Utf8("(b|d)"),Utf8("i")) |
54+--------------------------------------------------+
55| true                                             |
56+--------------------------------------------------+
57```
58Additional examples can be found [here](https://github.com/apache/datafusion/blob/main/datafusion-examples/examples/builtin_functions/regexp.rs)
59"#,
60    standard_argument(name = "str", prefix = "String"),
61    standard_argument(name = "regexp", prefix = "Regular"),
62    argument(
63        name = "flags",
64        description = r#"Optional regular expression flags that control the behavior of the regular expression. The following flags are supported:
65  - **i**: case-insensitive: letters match both upper and lower case
66  - **m**: multi-line mode: ^ and $ match begin/end of line
67  - **s**: allow . to match \n
68  - **R**: enables CRLF mode: when multi-line mode is enabled, \r\n is used
69  - **U**: swap the meaning of x* and x*?"#
70    )
71)]
72#[derive(Debug, PartialEq, Eq, Hash)]
73pub struct RegexpLikeFunc {
74    signature: Signature,
75}
76
77impl Default for RegexpLikeFunc {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl RegexpLikeFunc {
84    pub fn new() -> Self {
85        Self {
86            signature: Signature::one_of(
87                vec![
88                    TypeSignature::Coercible(vec![
89                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
90                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
91                    ]),
92                    TypeSignature::Coercible(vec![
93                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
94                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
95                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
96                    ]),
97                ],
98                Volatility::Immutable,
99            ),
100        }
101    }
102}
103
104impl ScalarUDFImpl for RegexpLikeFunc {
105    fn name(&self) -> &str {
106        "regexp_like"
107    }
108
109    fn signature(&self) -> &Signature {
110        &self.signature
111    }
112
113    fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
114        use DataType::*;
115
116        Ok(match &arg_types[0] {
117            Null => Null,
118            // Type coercion is done by DataFusion based on signature, so if we
119            // get here, the first argument is always a string
120            _ => Boolean,
121        })
122    }
123
124    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
125        let args = &args.args;
126        match args.as_slice() {
127            [ColumnarValue::Scalar(value), ColumnarValue::Scalar(pattern)] => {
128                let value = scalar_string(value)?;
129                let pattern = scalar_string(pattern)?;
130                regexp_like_scalar(value, pattern, None)
131            }
132            [
133                ColumnarValue::Scalar(value),
134                ColumnarValue::Scalar(pattern),
135                ColumnarValue::Scalar(flags),
136            ] => {
137                let value = scalar_string(value)?;
138                let pattern = scalar_string(pattern)?;
139                let flags = scalar_string(flags)?;
140                regexp_like_scalar(value, pattern, flags)
141            }
142            [ColumnarValue::Array(values), ColumnarValue::Scalar(pattern)] => {
143                let pattern = scalar_string(pattern)?;
144                let array = regexp_like_array_scalar(values, pattern, None)?;
145                Ok(ColumnarValue::Array(array))
146            }
147            [
148                ColumnarValue::Array(values),
149                ColumnarValue::Scalar(pattern),
150                ColumnarValue::Scalar(flags),
151            ] => {
152                let flags = scalar_string(flags)?;
153                if flags.is_some_and(|flagz| flagz.contains('g')) {
154                    plan_err!("regexp_like() does not support the \"global\" option")
155                } else {
156                    let pattern = scalar_string(pattern)?;
157                    let array = regexp_like_array_scalar(values, pattern, flags)?;
158                    Ok(ColumnarValue::Array(array))
159                }
160            }
161            _ => {
162                let args = ColumnarValue::values_to_arrays(args)?;
163                regexp_like(&args).map(ColumnarValue::Array)
164            }
165        }
166    }
167
168    fn simplify(
169        &self,
170        mut args: Vec<Expr>,
171        info: &SimplifyContext,
172    ) -> Result<ExprSimplifyResult> {
173        // Try to simplify regexp_like usage to one of the builtin operators since those have
174        // optimized code paths for the case where the regular expression pattern is a scalar.
175        // Additionally, the expression simplification optimization pass will attempt to further
176        // simplify regular expression patterns used in operator expressions.
177        let Some(op) = derive_operator(&args) else {
178            return Ok(ExprSimplifyResult::Original(args));
179        };
180
181        let string_type = info.get_data_type(&args[0])?;
182        let regexp_type = info.get_data_type(&args[1])?;
183        let binary_type_coercer = BinaryTypeCoercer::new(&string_type, &op, &regexp_type);
184        let Ok((coerced_string_type, coerced_regexp_type)) =
185            binary_type_coercer.get_input_types()
186        else {
187            return Ok(ExprSimplifyResult::Original(args));
188        };
189
190        // regexp_like(str, regexp [, flags])
191        let regexp = args.swap_remove(1);
192        let string = args.swap_remove(0);
193
194        Ok(ExprSimplifyResult::Simplified(binary_expr(
195            if string_type != coerced_string_type {
196                cast(string, coerced_string_type)
197            } else {
198                string
199            },
200            op,
201            if regexp_type != coerced_regexp_type {
202                cast(regexp, coerced_regexp_type)
203            } else {
204                regexp
205            },
206        )))
207    }
208
209    fn documentation(&self) -> Option<&Documentation> {
210        self.doc()
211    }
212}
213
214fn derive_operator(args: &[Expr]) -> Option<Operator> {
215    match args.len() {
216        // regexp_like(str, regexp, flags)
217        3 => {
218            match &args[2] {
219                Expr::Literal(ScalarValue::Utf8(Some(flags)), _) => {
220                    match flags.as_str() {
221                        "i" => Some(Operator::RegexIMatch),
222                        "" => Some(Operator::RegexMatch),
223                        // Any flags besides 'i' have no operator equivalent
224                        _ => None,
225                    }
226                }
227                // `flags` is not a literal, so we can't derive the correct operator statically
228                _ => None,
229            }
230        }
231        // regexp_like(str, regexp)
232        2 => Some(Operator::RegexMatch),
233        // Should never happen, but just in case
234        _ => None,
235    }
236}
237
238/// Tests a string using a regular expression returning true if at
239/// least one match, false otherwise.
240///
241/// The full list of supported features and syntax can be found at
242/// <https://docs.rs/regex/latest/regex/#syntax>
243///
244/// Supported flags can be found at
245/// <https://docs.rs/regex/latest/regex/#grouping-and-flags>
246///
247/// # Examples
248///
249/// ```ignore
250/// # use datafusion::prelude::*;
251/// # use datafusion::error::Result;
252/// # #[tokio::main]
253/// # async fn main() -> Result<()> {
254/// let ctx = SessionContext::new();
255/// let df = ctx.read_csv("tests/data/regex.csv", CsvReadOptions::new()).await?;
256///
257/// // use the regexp_like function to test col 'values',
258/// // against patterns in col 'patterns' without flags
259/// let df = df.with_column(
260///     "a",
261///     regexp_like(vec![col("values"), col("patterns")])
262/// )?;
263/// // use the regexp_like function to test col 'values',
264/// // against patterns in col 'patterns' with flags
265/// let df = df.with_column(
266///     "b",
267///     regexp_like(vec![col("values"), col("patterns"), col("flags")])
268/// )?;
269/// // literals can be used as well with dataframe calls
270/// let df = df.with_column(
271///     "c",
272///     regexp_like(vec![lit("foobarbequebaz"), lit("(bar)(beque)")])
273/// )?;
274///
275/// df.show().await?;
276///
277/// # Ok(())
278/// # }
279/// ```
280pub fn regexp_like(args: &[ArrayRef]) -> Result<ArrayRef> {
281    match args.len() {
282        2 => handle_regexp_like(&args[0], &args[1], None),
283        3 => {
284            let flags = match args[2].data_type() {
285                Utf8 => args[2].as_string::<i32>(),
286                LargeUtf8 => {
287                    let large_string_array = args[2].as_string::<i64>();
288                    let string_vec: Vec<Option<&str>> = (0..large_string_array.len())
289                        .map(|i| {
290                            if large_string_array.is_null(i) {
291                                None
292                            } else {
293                                Some(large_string_array.value(i))
294                            }
295                        })
296                        .collect();
297
298                    &GenericStringArray::<i32>::from(string_vec)
299                }
300                _ => {
301                    let string_view_array = args[2].as_string_view();
302                    let string_vec: Vec<Option<String>> = (0..string_view_array.len())
303                        .map(|i| {
304                            if string_view_array.is_null(i) {
305                                None
306                            } else {
307                                Some(string_view_array.value(i).to_string())
308                            }
309                        })
310                        .collect();
311                    &GenericStringArray::<i32>::from(string_vec)
312                }
313            };
314
315            if flags
316                .iter()
317                .any(|s| s.is_some_and(|flagz| flagz.contains('g')))
318            {
319                return plan_err!("regexp_like() does not support the \"global\" option");
320            }
321
322            handle_regexp_like(&args[0], &args[1], Some(flags))
323        }
324        other => exec_err!(
325            "`regexp_like` was called with {other} arguments. It requires at least 2 and at most 3."
326        ),
327    }
328}
329
330fn scalar_string(value: &ScalarValue) -> Result<Option<&str>> {
331    match value.try_as_str() {
332        Some(v) => Ok(v),
333        None => internal_err!(
334            "Unsupported data type {:?} for function `regexp_like`",
335            value.data_type()
336        ),
337    }
338}
339
340fn regexp_like_array_scalar(
341    values: &ArrayRef,
342    pattern: Option<&str>,
343    flags: Option<&str>,
344) -> Result<ArrayRef> {
345    use DataType::*;
346
347    let Some(pattern) = pattern else {
348        return Ok(Arc::new(BooleanArray::new_null(values.len())));
349    };
350    let array = match values.data_type() {
351        Utf8 => {
352            let array = values.as_string::<i32>();
353            regexp::regexp_is_match_scalar(array, pattern, flags)?
354        }
355        Utf8View => {
356            let array = values.as_string_view();
357            regexp::regexp_is_match_scalar(array, pattern, flags)?
358        }
359        LargeUtf8 => {
360            let array = values.as_string::<i64>();
361            regexp::regexp_is_match_scalar(array, pattern, flags)?
362        }
363        other => {
364            return internal_err!(
365                "Unsupported data type {other:?} for function `regexp_like`"
366            );
367        }
368    };
369
370    Ok(Arc::new(array))
371}
372
373fn regexp_like_scalar(
374    value: Option<&str>,
375    pattern: Option<&str>,
376    flags: Option<&str>,
377) -> Result<ColumnarValue> {
378    if flags.is_some_and(|flagz| flagz.contains('g')) {
379        return plan_err!("regexp_like() does not support the \"global\" option");
380    }
381
382    if value.is_none() || pattern.is_none() {
383        return Ok(ColumnarValue::Scalar(ScalarValue::Boolean(None)));
384    }
385
386    let value = value.unwrap();
387    let pattern = pattern.unwrap();
388    let pattern = match flags {
389        Some(flagz) => format!("(?{flagz}){pattern}"),
390        None => pattern.to_string(),
391    };
392
393    let result = if pattern.is_empty() {
394        true
395    } else {
396        let re = Regex::new(pattern.as_str()).map_err(|e| {
397            datafusion_common::DataFusionError::Execution(format!(
398                "Regular expression did not compile: {e:?}"
399            ))
400        })?;
401        re.is_match(value)
402    };
403
404    Ok(ColumnarValue::Scalar(ScalarValue::Boolean(Some(result))))
405}
406
407fn handle_regexp_like(
408    values: &ArrayRef,
409    patterns: &ArrayRef,
410    flags: Option<&GenericStringArray<i32>>,
411) -> Result<ArrayRef> {
412    let array = match (values.data_type(), patterns.data_type()) {
413        (Utf8View, Utf8) => {
414            let value = values.as_string_view();
415            let pattern = patterns.as_string::<i32>();
416
417            regexp::regexp_is_match(value, pattern, flags)
418                .map_err(|e| arrow_datafusion_err!(e))?
419        }
420        (Utf8View, Utf8View) => {
421            let value = values.as_string_view();
422            let pattern = patterns.as_string_view();
423
424            regexp::regexp_is_match(value, pattern, flags)
425                .map_err(|e| arrow_datafusion_err!(e))?
426        }
427        (Utf8View, LargeUtf8) => {
428            let value = values.as_string_view();
429            let pattern = patterns.as_string::<i64>();
430
431            regexp::regexp_is_match(value, pattern, flags)
432                .map_err(|e| arrow_datafusion_err!(e))?
433        }
434        (Utf8, Utf8) => {
435            let value = values.as_string::<i32>();
436            let pattern = patterns.as_string::<i32>();
437
438            regexp::regexp_is_match(value, pattern, flags)
439                .map_err(|e| arrow_datafusion_err!(e))?
440        }
441        (Utf8, Utf8View) => {
442            let value = values.as_string::<i32>();
443            let pattern = patterns.as_string_view();
444
445            regexp::regexp_is_match(value, pattern, flags)
446                .map_err(|e| arrow_datafusion_err!(e))?
447        }
448        (Utf8, LargeUtf8) => {
449            let value = values.as_string::<i32>();
450            let pattern = patterns.as_string::<i64>();
451
452            regexp::regexp_is_match(value, pattern, flags)
453                .map_err(|e| arrow_datafusion_err!(e))?
454        }
455        (LargeUtf8, Utf8) => {
456            let value = values.as_string::<i64>();
457            let pattern = patterns.as_string::<i32>();
458
459            regexp::regexp_is_match(value, pattern, flags)
460                .map_err(|e| arrow_datafusion_err!(e))?
461        }
462        (LargeUtf8, Utf8View) => {
463            let value = values.as_string::<i64>();
464            let pattern = patterns.as_string_view();
465
466            regexp::regexp_is_match(value, pattern, flags)
467                .map_err(|e| arrow_datafusion_err!(e))?
468        }
469        (LargeUtf8, LargeUtf8) => {
470            let value = values.as_string::<i64>();
471            let pattern = patterns.as_string::<i64>();
472
473            regexp::regexp_is_match(value, pattern, flags)
474                .map_err(|e| arrow_datafusion_err!(e))?
475        }
476        other => {
477            return internal_err!(
478                "Unsupported data type {other:?} for function `regexp_like`"
479            );
480        }
481    };
482
483    Ok(Arc::new(array) as ArrayRef)
484}
485
486#[cfg(test)]
487mod tests {
488    use std::sync::Arc;
489
490    use arrow::array::StringArray;
491    use arrow::array::{BooleanBuilder, StringViewArray};
492    use arrow::datatypes::{DataType, Field};
493    use datafusion_common::config::ConfigOptions;
494    use datafusion_common::{Result, ScalarValue};
495    use datafusion_expr::{ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl};
496
497    use crate::regex::regexplike::{RegexpLikeFunc, regexp_like};
498
499    fn invoke_regexp_like(args: Vec<ColumnarValue>) -> Result<ColumnarValue> {
500        let number_rows = args
501            .iter()
502            .find_map(|arg| match arg {
503                ColumnarValue::Array(array) => Some(array.len()),
504                _ => None,
505            })
506            .unwrap_or(1);
507        let arg_fields = args
508            .iter()
509            .enumerate()
510            .map(|(idx, arg)| {
511                Arc::new(Field::new(format!("arg_{idx}"), arg.data_type(), true))
512            })
513            .collect::<Vec<_>>();
514
515        RegexpLikeFunc::new().invoke_with_args(ScalarFunctionArgs {
516            args,
517            arg_fields,
518            number_rows,
519            return_field: Arc::new(Field::new("f", DataType::Boolean, true)),
520            config_options: Arc::new(ConfigOptions::default()),
521        })
522    }
523
524    #[test]
525    fn test_case_sensitive_regexp_like_utf8() {
526        let values = StringArray::from(vec!["abc"; 5]);
527
528        let patterns =
529            StringArray::from(vec!["^(a)", "^(A)", "(b|d)", "(B|D)", "^(b|c)"]);
530
531        let mut expected_builder: BooleanBuilder = BooleanBuilder::new();
532        expected_builder.append_value(true);
533        expected_builder.append_value(false);
534        expected_builder.append_value(true);
535        expected_builder.append_value(false);
536        expected_builder.append_value(false);
537        let expected = expected_builder.finish();
538
539        let re = regexp_like(&[Arc::new(values), Arc::new(patterns)]).unwrap();
540
541        assert_eq!(re.as_ref(), &expected);
542    }
543
544    #[test]
545    fn test_case_sensitive_regexp_like_utf8view() {
546        let values = StringViewArray::from(vec!["abc"; 5]);
547
548        let patterns =
549            StringArray::from(vec!["^(a)", "^(A)", "(b|d)", "(B|D)", "^(b|c)"]);
550
551        let mut expected_builder: BooleanBuilder = BooleanBuilder::new();
552        expected_builder.append_value(true);
553        expected_builder.append_value(false);
554        expected_builder.append_value(true);
555        expected_builder.append_value(false);
556        expected_builder.append_value(false);
557        let expected = expected_builder.finish();
558
559        let re = regexp_like(&[Arc::new(values), Arc::new(patterns)]).unwrap();
560
561        assert_eq!(re.as_ref(), &expected);
562    }
563
564    #[test]
565    fn test_case_insensitive_regexp_like_utf8() {
566        let values = StringArray::from(vec!["abc"; 5]);
567        let patterns =
568            StringArray::from(vec!["^(a)", "^(A)", "(b|d)", "(B|D)", "^(b|c)"]);
569        let flags = StringArray::from(vec!["i"; 5]);
570
571        let mut expected_builder: BooleanBuilder = BooleanBuilder::new();
572        expected_builder.append_value(true);
573        expected_builder.append_value(true);
574        expected_builder.append_value(true);
575        expected_builder.append_value(true);
576        expected_builder.append_value(false);
577        let expected = expected_builder.finish();
578
579        let re = regexp_like(&[Arc::new(values), Arc::new(patterns), Arc::new(flags)])
580            .unwrap();
581
582        assert_eq!(re.as_ref(), &expected);
583    }
584
585    #[test]
586    fn test_case_insensitive_regexp_like_utf8view() {
587        let values = StringViewArray::from(vec!["abc"; 5]);
588        let patterns =
589            StringViewArray::from(vec!["^(a)", "^(A)", "(b|d)", "(B|D)", "^(b|c)"]);
590        let flags = StringArray::from(vec!["i"; 5]);
591
592        let mut expected_builder: BooleanBuilder = BooleanBuilder::new();
593        expected_builder.append_value(true);
594        expected_builder.append_value(true);
595        expected_builder.append_value(true);
596        expected_builder.append_value(true);
597        expected_builder.append_value(false);
598        let expected = expected_builder.finish();
599
600        let re = regexp_like(&[Arc::new(values), Arc::new(patterns), Arc::new(flags)])
601            .unwrap();
602
603        assert_eq!(re.as_ref(), &expected);
604    }
605
606    #[test]
607    fn test_unsupported_global_flag_regexp_like() {
608        let values = StringArray::from(vec!["abc"]);
609        let patterns = StringArray::from(vec!["^(a)"]);
610        let flags = StringArray::from(vec!["g"]);
611
612        let re_err =
613            regexp_like(&[Arc::new(values), Arc::new(patterns), Arc::new(flags)])
614                .expect_err("unsupported flag should have failed");
615
616        assert_eq!(
617            re_err.strip_backtrace(),
618            "Error during planning: regexp_like() does not support the \"global\" option"
619        );
620    }
621
622    #[test]
623    fn test_regexp_like_scalar_invoke() {
624        let args = vec![
625            ColumnarValue::Scalar(ScalarValue::Utf8(Some("foobarbequebaz".to_string()))),
626            ColumnarValue::Scalar(ScalarValue::Utf8(Some("(bar)(beque)".to_string()))),
627        ];
628        let result = invoke_regexp_like(args).unwrap();
629        match result {
630            ColumnarValue::Scalar(ScalarValue::Boolean(Some(true))) => {}
631            other => panic!("Unexpected result {other:?}"),
632        }
633    }
634
635    #[test]
636    fn test_regexp_like_array_scalar_invoke() {
637        let values = Arc::new(StringArray::from(vec!["abc", "xyz"]));
638        let args = vec![
639            ColumnarValue::Array(values),
640            ColumnarValue::Scalar(ScalarValue::Utf8(Some("^(a)".to_string()))),
641        ];
642        let result = invoke_regexp_like(args).unwrap();
643        let mut expected_builder = BooleanBuilder::new();
644        expected_builder.append_value(true);
645        expected_builder.append_value(false);
646        let expected = expected_builder.finish();
647        match result {
648            ColumnarValue::Array(array) => {
649                assert_eq!(array.as_ref(), &expected);
650            }
651            other => panic!("Unexpected result {other:?}"),
652        }
653    }
654
655    #[test]
656    fn test_regexp_like_scalar_flags_with_global() {
657        let args = vec![
658            ColumnarValue::Scalar(ScalarValue::Utf8(Some("abc".to_string()))),
659            ColumnarValue::Scalar(ScalarValue::Utf8(Some("^(a)".to_string()))),
660            ColumnarValue::Scalar(ScalarValue::Utf8(Some("ig".to_string()))),
661        ];
662        let err = invoke_regexp_like(args).expect_err("global flag should be rejected");
663        assert_eq!(
664            err.strip_backtrace(),
665            "Error during planning: regexp_like() does not support the \"global\" option"
666        );
667    }
668
669    #[test]
670    fn test_regexp_like_array_scalar_flags_with_global() {
671        let values = Arc::new(StringArray::from(vec!["abc", "xyz"]));
672        let args = vec![
673            ColumnarValue::Array(values),
674            ColumnarValue::Scalar(ScalarValue::Utf8(Some("^(a)".to_string()))),
675            ColumnarValue::Scalar(ScalarValue::Utf8(Some("ig".to_string()))),
676        ];
677        let err = invoke_regexp_like(args).expect_err("global flag should be rejected");
678        assert_eq!(
679            err.strip_backtrace(),
680            "Error during planning: regexp_like() does not support the \"global\" option"
681        );
682    }
683}