This is an automated email from the ASF dual-hosted git repository.

gian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new e10e35aa2c Add REGEXP_REPLACE function. (#14460)
e10e35aa2c is described below

commit e10e35aa2ca953e65f1337a08800e0a29aaefd44
Author: Gian Merlino <[email protected]>
AuthorDate: Thu Jun 29 13:47:57 2023 -0700

    Add REGEXP_REPLACE function. (#14460)
    
    * Add REGEXP_REPLACE function.
    
    Replaces all instances of a pattern with a replacement string.
    
    * Fixes.
    
    * Improve test coverage.
    
    * Adjust behavior.
---
 docs/querying/math-expr.md                         |   1 +
 docs/querying/sql-functions.md                     |   9 +
 docs/querying/sql-scalar.md                        |   1 +
 .../query/expression/RegexpReplaceExprMacro.java   | 157 ++++++++++++++
 .../expression/RegexpReplaceExprMacroTest.java     | 238 +++++++++++++++++++++
 .../druid/query/expression/TestExprMacroTable.java |   2 +
 .../org/apache/druid/guice/ExpressionModule.java   |   2 +
 .../builtin/RegexpReplaceOperatorConversion.java   |  69 ++++++
 .../sql/calcite/planner/DruidOperatorTable.java    |   3 +
 .../sql/calcite/expression/ExpressionsTest.java    |  53 +++++
 website/.spelling                                  |   1 +
 11 files changed, 536 insertions(+)

diff --git a/docs/querying/math-expr.md b/docs/querying/math-expr.md
index 2f50d4102c..8d558f4ceb 100644
--- a/docs/querying/math-expr.md
+++ b/docs/querying/math-expr.md
@@ -84,6 +84,7 @@ The following built-in functions are available.
 |parse_long|parse_long(string[, radix]) parses a string as a long with the 
given radix, or 10 (decimal) if a radix is not provided.|
 |regexp_extract|regexp_extract(expr, pattern[, index]) applies a regular 
expression pattern and extracts a capture group index, or null if there is no 
match. If index is unspecified or zero, returns the substring that matched the 
pattern. The pattern may match anywhere inside `expr`; if you want to match the 
entire string instead, use the `^` and `$` markers at the start and end of your 
pattern.|
 |regexp_like|regexp_like(expr, pattern) returns whether `expr` matches regular 
expression `pattern`. The pattern may match anywhere inside `expr`; if you want 
to match the entire string instead, use the `^` and `$` markers at the start 
and end of your pattern. |
+|regexp_replace|regexp_replace(expr, pattern, replacement) replaces all 
instances of a regular expression pattern with a given replacement string. The 
pattern may match anywhere inside `expr`; if you want to match the entire 
string instead, use the `^` and `$` markers at the start and end of your 
pattern.|
 |contains_string|contains_string(expr, string) returns whether `expr` contains 
`string` as a substring. This method is case-sensitive.|
 |icontains_string|contains_string(expr, string) returns whether `expr` 
contains `string` as a substring. This method is case-insensitive.|
 |replace|replace(expr, pattern, replacement) replaces pattern with replacement|
diff --git a/docs/querying/sql-functions.md b/docs/querying/sql-functions.md
index 80532f2aca..3e4cf711dc 100644
--- a/docs/querying/sql-functions.md
+++ b/docs/querying/sql-functions.md
@@ -1141,6 +1141,15 @@ Applies a regular expression to the string expression 
and returns the _n_th matc
 
 Returns true or false signifying whether the regular expression finds a match 
in the string expression.
 
+## REGEXP_REPLACE
+
+`REGEXP_REPLACE(<CHARACTER>, <CHARACTER>, <CHARACTER>)`
+
+**Function type:** [Scalar, string](sql-scalar.md#string-functions)
+
+Replaces all occurrences of a regular expression in a string expression with a 
replacement string. The replacement
+string may refer to capture groups using `$1`, `$2`, etc.
+
 ## REPEAT
 
 `REPEAT(<CHARACTER>, [<INTEGER>])`
diff --git a/docs/querying/sql-scalar.md b/docs/querying/sql-scalar.md
index ffdac7b7a2..a741c1ff8b 100644
--- a/docs/querying/sql-scalar.md
+++ b/docs/querying/sql-scalar.md
@@ -103,6 +103,7 @@ String functions accept strings, and return a type 
appropriate to the function.
 |`POSITION(needle IN haystack [FROM fromIndex])`|Returns the index of `needle` 
within `haystack`, with indexes starting from 1. The search will begin at 
`fromIndex`, or 1 if `fromIndex` is not specified. If `needle` is not found, 
returns 0.|
 |`REGEXP_EXTRACT(expr, pattern, [index])`|Apply regular expression `pattern` 
to `expr` and extract a capture group, or `NULL` if there is no match. If index 
is unspecified or zero, returns the first substring that matched the pattern. 
The pattern may match anywhere inside `expr`; if you want to match the entire 
string instead, use the `^` and `$` markers at the start and end of your 
pattern. Note: when `druid.generic.useDefaultValueForNull = true`, it is not 
possible to differentiate an  [...]
 |`REGEXP_LIKE(expr, pattern)`|Returns whether `expr` matches regular 
expression `pattern`. The pattern may match anywhere inside `expr`; if you want 
to match the entire string instead, use the `^` and `$` markers at the start 
and end of your pattern. Similar to 
[`LIKE`](sql-operators.md#logical-operators), but uses regexps instead of LIKE 
patterns. Especially useful in WHERE clauses.|
+|`REGEXP_REPLACE(expr, pattern, replacement)`|Replaces all occurrences of 
regular expression `pattern` within `expr` with `replacement`. The replacement 
string may refer to capture groups using `$1`, `$2`, etc. The pattern may match 
anywhere inside `expr`; if you want to match the entire string instead, use the 
`^` and `$` markers at the start and end of your pattern.|
 |`CONTAINS_STRING(expr, str)`|Returns true if the `str` is a substring of 
`expr`.|
 |`ICONTAINS_STRING(expr, str)`|Returns true if the `str` is a substring of 
`expr`. The match is case-insensitive.|
 |`REPLACE(expr, pattern, replacement)`|Replaces pattern with replacement in 
`expr`, and returns the result.|
diff --git 
a/processing/src/main/java/org/apache/druid/query/expression/RegexpReplaceExprMacro.java
 
b/processing/src/main/java/org/apache/druid/query/expression/RegexpReplaceExprMacro.java
new file mode 100644
index 0000000000..cf4ecf83a7
--- /dev/null
+++ 
b/processing/src/main/java/org/apache/druid/query/expression/RegexpReplaceExprMacro.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.query.expression;
+
+import org.apache.druid.common.config.NullHandling;
+import org.apache.druid.math.expr.Expr;
+import org.apache.druid.math.expr.ExprEval;
+import org.apache.druid.math.expr.ExprMacroTable;
+import org.apache.druid.math.expr.ExpressionType;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class RegexpReplaceExprMacro implements ExprMacroTable.ExprMacro
+{
+  private static final String FN_NAME = "regexp_replace";
+
+  @Override
+  public String name()
+  {
+    return FN_NAME;
+  }
+
+  @Override
+  public Expr apply(final List<Expr> args)
+  {
+    validationHelperCheckArgumentCount(args, 3);
+
+    if (args.stream().skip(1).allMatch(Expr::isLiteral)) {
+      return new RegexpReplaceExpr(args);
+    } else {
+      return new RegexpReplaceDynamicExpr(args);
+    }
+  }
+
+  abstract class BaseRegexpReplaceExpr extends 
ExprMacroTable.BaseScalarMacroFunctionExpr
+  {
+    public BaseRegexpReplaceExpr(final List<Expr> args)
+    {
+      super(FN_NAME, args);
+    }
+
+    @Nullable
+    @Override
+    public ExpressionType getOutputType(InputBindingInspector inspector)
+    {
+      return ExpressionType.STRING;
+    }
+
+    @Override
+    public Expr visit(Shuttle shuttle)
+    {
+      return shuttle.visit(apply(shuttle.visitAll(args)));
+    }
+  }
+
+  /**
+   * Expr when pattern and replacement are literals.
+   */
+  class RegexpReplaceExpr extends BaseRegexpReplaceExpr
+  {
+    private final Expr arg;
+    private final Pattern pattern;
+    private final String replacement;
+
+    private RegexpReplaceExpr(List<Expr> args)
+    {
+      super(args);
+
+      final Expr patternExpr = args.get(1);
+      final Expr replacementExpr = args.get(2);
+
+      if (!ExprUtils.isStringLiteral(patternExpr)
+          && !(patternExpr.isLiteral() && patternExpr.getLiteralValue() == 
null)) {
+        throw validationFailed("pattern must be a string literal");
+      }
+
+      if (!ExprUtils.isStringLiteral(replacementExpr)
+          && !(replacementExpr.isLiteral() && 
replacementExpr.getLiteralValue() == null)) {
+        throw validationFailed("replacement must be a string literal");
+      }
+
+      final String patternString = NullHandling.nullToEmptyIfNeeded((String) 
patternExpr.getLiteralValue());
+
+      this.arg = args.get(0);
+      this.pattern = patternString != null ? Pattern.compile(patternString) : 
null;
+      this.replacement = NullHandling.nullToEmptyIfNeeded((String) 
replacementExpr.getLiteralValue());
+    }
+
+    @Nonnull
+    @Override
+    public ExprEval<?> eval(final ObjectBinding bindings)
+    {
+      if (pattern == null || replacement == null) {
+        return ExprEval.of(null);
+      }
+
+      final String s = 
NullHandling.nullToEmptyIfNeeded(arg.eval(bindings).asString());
+
+      if (s == null) {
+        return ExprEval.of(null);
+      } else {
+        final Matcher matcher = pattern.matcher(s);
+        final String retVal = matcher.replaceAll(replacement);
+        return ExprEval.of(retVal);
+      }
+    }
+  }
+
+  /**
+   * Expr when pattern and replacement are dynamic (not literals).
+   */
+  class RegexpReplaceDynamicExpr extends BaseRegexpReplaceExpr
+  {
+    private RegexpReplaceDynamicExpr(List<Expr> args)
+    {
+      super(args);
+    }
+
+    @Nonnull
+    @Override
+    public ExprEval<?> eval(final ObjectBinding bindings)
+    {
+      final String s = 
NullHandling.nullToEmptyIfNeeded(args.get(0).eval(bindings).asString());
+      final String pattern = 
NullHandling.nullToEmptyIfNeeded(args.get(1).eval(bindings).asString());
+      final String replacement = 
NullHandling.nullToEmptyIfNeeded(args.get(2).eval(bindings).asString());
+
+      if (s == null || pattern == null || replacement == null) {
+        return ExprEval.of(null);
+      } else {
+        final Matcher matcher = Pattern.compile(pattern).matcher(s);
+        final String retVal = matcher.replaceAll(replacement);
+        return ExprEval.of(retVal);
+      }
+    }
+  }
+}
diff --git 
a/processing/src/test/java/org/apache/druid/query/expression/RegexpReplaceExprMacroTest.java
 
b/processing/src/test/java/org/apache/druid/query/expression/RegexpReplaceExprMacroTest.java
new file mode 100644
index 0000000000..f0e3f3c843
--- /dev/null
+++ 
b/processing/src/test/java/org/apache/druid/query/expression/RegexpReplaceExprMacroTest.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.query.expression;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.druid.common.config.NullHandling;
+import org.apache.druid.math.expr.ExprEval;
+import org.apache.druid.math.expr.ExpressionType;
+import org.apache.druid.math.expr.InputBindings;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class RegexpReplaceExprMacroTest extends MacroTestBase
+{
+  public RegexpReplaceExprMacroTest()
+  {
+    super(new RegexpReplaceExprMacro());
+  }
+
+  @Test
+  public void testErrorZeroArguments()
+  {
+    expectException(IllegalArgumentException.class, "Function[regexp_replace] 
requires 3 arguments");
+    eval("regexp_replace()", InputBindings.nilBindings());
+  }
+
+  @Test
+  public void testErrorFourArguments()
+  {
+    expectException(IllegalArgumentException.class, "Function[regexp_replace] 
requires 3 arguments");
+    eval("regexp_replace('a', 'b', 'c', 'd')", InputBindings.nilBindings());
+  }
+
+  @Test
+  public void testErrorNonStringPattern()
+  {
+    expectException(IllegalArgumentException.class, "Function[regexp_replace] 
pattern must be a string literal");
+    eval(
+        "regexp_replace(a, 1, 'x')",
+        InputBindings.forInputSupplier("a", ExpressionType.STRING, () -> "foo")
+    );
+  }
+
+  @Test
+  public void testErrorNonStringReplacement()
+  {
+    expectException(IllegalArgumentException.class, "Function[regexp_replace] 
replacement must be a string literal");
+    eval(
+        "regexp_replace(a, 'x', 1)",
+        InputBindings.forInputSupplier("a", ExpressionType.STRING, () -> "foo")
+    );
+  }
+
+  @Test
+  public void testNullPattern()
+  {
+    final ExprEval<?> result = eval(
+        "regexp_replace(a, null, 'x')",
+        InputBindings.forInputSupplier("a", ExpressionType.STRING, () -> "foo")
+    );
+
+    if (NullHandling.sqlCompatible()) {
+      Assert.assertNull(result.value());
+    } else {
+      Assert.assertEquals("xfxoxox", result.value());
+    }
+  }
+
+  @Test
+  public void testNoMatch()
+  {
+    final ExprEval<?> result = eval(
+        "regexp_replace(a, 'f.x', 'beep')",
+        InputBindings.forInputSupplier("a", ExpressionType.STRING, () -> "foo")
+    );
+    Assert.assertEquals("foo", result.value());
+  }
+
+  @Test
+  public void testEmptyStringPattern()
+  {
+    final ExprEval<?> result = eval(
+        "regexp_replace(a, '', 'x')",
+        InputBindings.forInputSupplier("a", ExpressionType.STRING, () -> "foo")
+    );
+    Assert.assertEquals("xfxoxox", result.value());
+  }
+
+  @Test
+  public void testMultiLinePattern()
+  {
+    final ExprEval<?> result = eval(
+        "regexp_replace(a, '^foo\\\\nbar$', 'xxx')",
+        InputBindings.forInputSupplier("a", ExpressionType.STRING, () -> 
"foo\nbar")
+    );
+    Assert.assertEquals("xxx", result.value());
+  }
+
+  @Test
+  public void testMultiLinePatternNoMatch()
+  {
+    final ExprEval<?> result = eval(
+        "regexp_replace(a, '^foo\\\\nbar$', 'xxx')",
+        InputBindings.forInputSupplier("a", ExpressionType.STRING, () -> 
"foo\nbarz")
+    );
+    Assert.assertEquals("foo\nbarz", result.value());
+  }
+
+  @Test
+  public void testNullPatternOnEmptyString()
+  {
+    final ExprEval<?> result = eval(
+        "regexp_replace(a, null, 'x')",
+        InputBindings.forInputSupplier("a", ExpressionType.STRING, () -> "")
+    );
+
+    if (NullHandling.sqlCompatible()) {
+      Assert.assertNull(result.value());
+    } else {
+      Assert.assertEquals("x", result.value());
+    }
+  }
+
+  @Test
+  public void testEmptyStringPatternOnEmptyString()
+  {
+    final ExprEval<?> result = eval(
+        "regexp_replace(a, '', 'x')",
+        InputBindings.forInputSupplier("a", ExpressionType.STRING, () -> "")
+    );
+    Assert.assertEquals("x", result.value());
+  }
+
+  @Test
+  public void testEmptyStringPatternOnEmptyStringDynamic()
+  {
+    final ExprEval<?> result = eval(
+        "regexp_replace(a, pattern, replacement)",
+        InputBindings.forInputSuppliers(
+            ImmutableMap.of(
+                "a", InputBindings.inputSupplier(ExpressionType.STRING, () -> 
""),
+                "pattern", InputBindings.inputSupplier(ExpressionType.STRING, 
() -> ""),
+                "replacement", 
InputBindings.inputSupplier(ExpressionType.STRING, () -> "x")
+            )
+        )
+    );
+    Assert.assertEquals("x", result.value());
+  }
+
+  @Test
+  public void testNullPatternOnNull()
+  {
+    final ExprEval<?> result = eval("regexp_replace(a, null, 'x')", 
InputBindings.nilBindings());
+
+    if (NullHandling.sqlCompatible()) {
+      Assert.assertNull(result.value());
+    } else {
+      Assert.assertEquals("x", result.value());
+    }
+  }
+
+  @Test
+  public void testNullPatternOnNullDynamic()
+  {
+    final ExprEval<?> result = eval(
+        "regexp_replace(a, pattern, replacement)",
+        InputBindings.forInputSuppliers(
+            ImmutableMap.of("replacement", 
InputBindings.inputSupplier(ExpressionType.STRING, () -> "x"))
+        )
+    );
+
+    if (NullHandling.sqlCompatible()) {
+      Assert.assertNull(result.value());
+    } else {
+      Assert.assertEquals("x", result.value());
+    }
+  }
+
+  @Test
+  public void testEmptyStringPatternOnNull()
+  {
+    final ExprEval<?> result = eval("regexp_replace(a, '', 'x')", 
InputBindings.nilBindings());
+
+    if (NullHandling.sqlCompatible()) {
+      Assert.assertNull(result.value());
+    } else {
+      Assert.assertEquals("x", result.value());
+    }
+  }
+
+  @Test
+  public void testUrlIdReplacement()
+  {
+    final ExprEval<?> result = eval(
+        "regexp_replace(regexp_replace(a, '\\\\?(.*)$', ''), 
'/(\\\\w+)(?=/|$)', '/*')",
+        InputBindings.forInputSupplier("a", ExpressionType.STRING, () -> 
"http://example.com/path/to?query";)
+    );
+
+    Assert.assertEquals("http://example.com/*/*";, result.value());
+  }
+
+  @Test
+  public void testUrlIdReplacementDynamic()
+  {
+    final ExprEval<?> result = eval(
+        "regexp_replace(regexp_replace(a, pattern1, replacement1), pattern2, 
replacement2)",
+        InputBindings.forInputSuppliers(
+            ImmutableMap
+                .<String, InputBindings.InputSupplier>builder()
+                .put("a", InputBindings.inputSupplier(ExpressionType.STRING, 
() -> "http://example.com/path/to?query";))
+                .put("pattern1", 
InputBindings.inputSupplier(ExpressionType.STRING, () -> "\\?(.*)$"))
+                .put("pattern2", 
InputBindings.inputSupplier(ExpressionType.STRING, () -> "/(\\w+)(?=/|$)"))
+                .put("replacement1", 
InputBindings.inputSupplier(ExpressionType.STRING, () -> ""))
+                .put("replacement2", 
InputBindings.inputSupplier(ExpressionType.STRING, () -> "/*"))
+                .build()
+        )
+    );
+
+    Assert.assertEquals("http://example.com/*/*";, result.value());
+  }
+}
diff --git 
a/processing/src/test/java/org/apache/druid/query/expression/TestExprMacroTable.java
 
b/processing/src/test/java/org/apache/druid/query/expression/TestExprMacroTable.java
index cadb24dc98..93ba9878f7 100644
--- 
a/processing/src/test/java/org/apache/druid/query/expression/TestExprMacroTable.java
+++ 
b/processing/src/test/java/org/apache/druid/query/expression/TestExprMacroTable.java
@@ -42,7 +42,9 @@ public class TestExprMacroTable extends ExprMacroTable
             new IPv4AddressParseExprMacro(),
             new IPv4AddressStringifyExprMacro(),
             new LikeExprMacro(),
+            new RegexpLikeExprMacro(),
             new RegexpExtractExprMacro(),
+            new RegexpReplaceExprMacro(),
             new TimestampCeilExprMacro(),
             new TimestampExtractExprMacro(),
             new TimestampFloorExprMacro(),
diff --git a/server/src/main/java/org/apache/druid/guice/ExpressionModule.java 
b/server/src/main/java/org/apache/druid/guice/ExpressionModule.java
index 3008ba8fb0..59f9ddb6c8 100644
--- a/server/src/main/java/org/apache/druid/guice/ExpressionModule.java
+++ b/server/src/main/java/org/apache/druid/guice/ExpressionModule.java
@@ -36,6 +36,7 @@ import org.apache.druid.query.expression.LikeExprMacro;
 import org.apache.druid.query.expression.NestedDataExpressions;
 import org.apache.druid.query.expression.RegexpExtractExprMacro;
 import org.apache.druid.query.expression.RegexpLikeExprMacro;
+import org.apache.druid.query.expression.RegexpReplaceExprMacro;
 import org.apache.druid.query.expression.TimestampCeilExprMacro;
 import org.apache.druid.query.expression.TimestampExtractExprMacro;
 import org.apache.druid.query.expression.TimestampFloorExprMacro;
@@ -57,6 +58,7 @@ public class ExpressionModule implements Module
                    .add(LikeExprMacro.class)
                    .add(RegexpExtractExprMacro.class)
                    .add(RegexpLikeExprMacro.class)
+                   .add(RegexpReplaceExprMacro.class)
                    .add(ContainsExprMacro.class)
                    .add(CaseInsensitiveContainsExprMacro.class)
                    .add(TimestampCeilExprMacro.class)
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/RegexpReplaceOperatorConversion.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/RegexpReplaceOperatorConversion.java
new file mode 100644
index 0000000000..134d72e914
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/RegexpReplaceOperatorConversion.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.sql.calcite.expression.builtin;
+
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.segment.column.RowSignature;
+import org.apache.druid.sql.calcite.expression.DruidExpression;
+import org.apache.druid.sql.calcite.expression.OperatorConversions;
+import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
+import org.apache.druid.sql.calcite.planner.Calcites;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
+
+public class RegexpReplaceOperatorConversion implements SqlOperatorConversion
+{
+  private static final SqlFunction SQL_FUNCTION = OperatorConversions
+      .operatorBuilder("REGEXP_REPLACE")
+      .operandTypes(SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER, 
SqlTypeFamily.CHARACTER)
+      .requiredOperands(3)
+      .returnTypeNullable(SqlTypeName.VARCHAR)
+      .functionCategory(SqlFunctionCategory.STRING)
+      .build();
+
+  @Override
+  public SqlFunction calciteOperator()
+  {
+    return SQL_FUNCTION;
+  }
+
+  @Override
+  public DruidExpression toDruidExpression(
+      final PlannerContext plannerContext,
+      final RowSignature rowSignature,
+      final RexNode rexNode
+  )
+  {
+    return OperatorConversions.convertCall(
+        plannerContext,
+        rowSignature,
+        rexNode,
+        arguments -> DruidExpression.ofFunctionCall(
+            Calcites.getColumnTypeForRelDataType(rexNode.getType()),
+            StringUtils.toLowerCase(SQL_FUNCTION.getName()),
+            arguments
+        )
+    );
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
index b373e8a01b..eac61b8b44 100644
--- 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
@@ -100,6 +100,7 @@ import 
org.apache.druid.sql.calcite.expression.builtin.RPadOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.RTrimOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.RegexpExtractOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.RegexpLikeOperatorConversion;
+import 
org.apache.druid.sql.calcite.expression.builtin.RegexpReplaceOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.ReinterpretOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.RepeatOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.ReverseOperatorConversion;
@@ -200,6 +201,7 @@ public class DruidOperatorTable implements SqlOperatorTable
                    .add(new PositionOperatorConversion())
                    .add(new RegexpExtractOperatorConversion())
                    .add(new RegexpLikeOperatorConversion())
+                   .add(new RegexpReplaceOperatorConversion())
                    .add(new RTrimOperatorConversion())
                    .add(new ParseLongOperatorConversion())
                    .add(new StringFormatOperatorConversion())
@@ -533,6 +535,7 @@ public class DruidOperatorTable implements SqlOperatorTable
    * than prefix/suffix/binary syntax as function syntax.
    *
    * @param syntax The SqlSyntax value to be checked.
+   *
    * @return {@code true} if the syntax is valid for a function, {@code false} 
otherwise.
    */
   public static boolean isFunctionSyntax(final SqlSyntax syntax)
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
 
b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
index 3fbd517e1c..6610951f07 100644
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
@@ -50,6 +50,7 @@ import 
org.apache.druid.sql.calcite.expression.builtin.ParseLongOperatorConversi
 import org.apache.druid.sql.calcite.expression.builtin.RPadOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.RegexpExtractOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.RegexpLikeOperatorConversion;
+import 
org.apache.druid.sql.calcite.expression.builtin.RegexpReplaceOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.RepeatOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.ReverseOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.RightOperatorConversion;
@@ -390,6 +391,58 @@ public class ExpressionsTest extends ExpressionTestBase
     );
   }
 
+  @Test
+  public void testRegexpReplace()
+  {
+    testHelper.testExpressionString(
+        new RegexpReplaceOperatorConversion().calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("s"),
+            testHelper.makeLiteral("x(.)"),
+            testHelper.makeLiteral("z")
+        ),
+        makeExpression("regexp_replace(\"s\",'x(.)','z')"),
+        "foo"
+    );
+
+    testHelper.testExpressionString(
+        new RegexpReplaceOperatorConversion().calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("s"),
+            testHelper.makeLiteral("(o)"),
+            testHelper.makeLiteral("z")
+        ),
+        makeExpression("regexp_replace(\"s\",'(o)','z')"),
+        "fzz"
+    );
+
+    testHelper.testExpressionString(
+        new RegexpReplaceOperatorConversion().calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeCall(
+                SqlStdOperatorTable.CONCAT,
+                testHelper.makeLiteral("Z"),
+                testHelper.makeInputRef("s")
+            ),
+            testHelper.makeLiteral("Zf(.)"),
+            testHelper.makeLiteral("z")
+        ),
+        makeExpression("regexp_replace(concat('Z',\"s\"),'Zf(.)','z')"),
+        "zo"
+    );
+
+    testHelper.testExpressionString(
+        new RegexpReplaceOperatorConversion().calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("s"),
+            testHelper.makeLiteral("f(.)"),
+            testHelper.makeLiteral("$1")
+        ),
+        makeExpression("regexp_replace(\"s\",'f(.)','$1')"),
+        "oo"
+    );
+  }
+
   @Test
   public void testRegexpLike()
   {
diff --git a/website/.spelling b/website/.spelling
index 997d387c76..63573f94a8 100644
--- a/website/.spelling
+++ b/website/.spelling
@@ -1474,6 +1474,7 @@ nvl
 parse_long
 regexp_extract
 regexp_like
+regexp_replace
 contains_string
 icontains_string
 result1


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to