From 0c6a3c1face14cf8136a593ec25a7f13631a09df Mon Sep 17 00:00:00 2001 From: James Duong Date: Mon, 27 May 2024 10:53:55 -0700 Subject: [PATCH] [CALCITE-6310] Add REGEXP_REPLACE function (enabled in PostgreSQL library) Reworked the overloading of REGEXP_REPLACE: * There is a two argument version that is only for Amazon Redshift, replacement string is the empty string * There is a three argument version that is for everything except PostgreSQL * PostgreSQL has its own 3 and 4 argument versions * PostgreSQL 3 argument version will only replace first occurrence * The g flag is now support for the 4 argument function in PostgreSQL * BigQuery only supports a 3 argument version REGEXP_REPLACE * BigQuery uses different syntax to refer to capture groups * There is a new REGEXP_REPLACE for BigQuery * No longer need to examine the ConformanceEnum to pick the correct implementation * MySQL and Amazon Redshift have two signatures with 5 arguments * Oracle only has one signature with 5 arguments --- babel/src/test/resources/sql/postgresql.iq | 15 ++ babel/src/test/resources/sql/redshift.iq | 10 ++ .../adapter/enumerable/RexImpTable.java | 52 ++---- .../apache/calcite/runtime/SqlFunctions.java | 29 ++- .../calcite/sql/fun/SqlLibraryOperators.java | 102 ++++++++++- .../sql/fun/SqlRegexpReplaceFunction.java | 75 -------- .../apache/calcite/sql/type/OperandTypes.java | 4 + .../sql/validate/SqlAbstractConformance.java | 4 - .../calcite/sql/validate/SqlConformance.java | 16 -- .../sql/validate/SqlConformanceEnum.java | 9 - .../validate/SqlDelegatingConformance.java | 4 - .../apache/calcite/util/BuiltInMethod.java | 12 +- .../calcite/util/RelToSqlConverterUtil.java | 4 +- .../apache/calcite/test/SqlFunctionsTest.java | 6 + .../apache/calcite/test/SqlValidatorTest.java | 27 +++ site/_docs/reference.md | 2 + .../apache/calcite/test/SqlOperatorTest.java | 169 +++++++++++++++--- 17 files changed, 365 insertions(+), 175 deletions(-) delete mode 100644 core/src/main/java/org/apache/calcite/sql/fun/SqlRegexpReplaceFunction.java diff --git a/babel/src/test/resources/sql/postgresql.iq b/babel/src/test/resources/sql/postgresql.iq index d2e0b532241..dc8a98d7ca7 100644 --- a/babel/src/test/resources/sql/postgresql.iq +++ b/babel/src/test/resources/sql/postgresql.iq @@ -1231,4 +1231,19 @@ X 1 !ok +SELECT regexp_replace('abc def GHI', '[a-z]+', 'X') AS x; +X +X def GHI +!ok + +SELECT regexp_replace('abc def GHI', '[a-z]+', 'X', 'g') AS x; +X +X X GHI +!ok + +SELECT regexp_replace('ABC def GHI', '[a-z]+', 'X', 'i') AS x; +X +X def GHI +!ok + # End postgresql.iq diff --git a/babel/src/test/resources/sql/redshift.iq b/babel/src/test/resources/sql/redshift.iq index ca0ed2fb9ff..6104222746f 100755 --- a/babel/src/test/resources/sql/redshift.iq +++ b/babel/src/test/resources/sql/redshift.iq @@ -2232,6 +2232,16 @@ USER sa !ok +SELECT regexp_replace('abcabc', 'b') AS x; +X +acac +!ok + +SELECT regexp_replace('abc def GHI', '[a-z]+', 'X') AS x; +X +X X GHI +!ok + # VERSION # Returns details about the currently installed release, # with specific Amazon Redshift version information at the end. diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java index e20e4a82f28..55ef175c67c 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java @@ -254,7 +254,15 @@ import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_EXTRACT_ALL; import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_INSTR; import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_LIKE; -import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE_2; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE_3; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE_4; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE_5; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE_5_ORACLE; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE_6; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE_BIG_QUERY_3; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE_PG_3; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE_PG_4; import static org.apache.calcite.sql.fun.SqlLibraryOperators.REPEAT; import static org.apache.calcite.sql.fun.SqlLibraryOperators.REVERSE; import static org.apache.calcite.sql.fun.SqlLibraryOperators.REVERSE_SPARK; @@ -864,7 +872,16 @@ Builder populate2() { NotImplementor.of(insensitiveImplementor)); map.put(NEGATED_POSIX_REGEX_CASE_SENSITIVE, NotImplementor.of(sensitiveImplementor)); - map.put(REGEXP_REPLACE, new RegexpReplaceImplementor()); + defineReflective(REGEXP_REPLACE_2, BuiltInMethod.REGEXP_REPLACE2.method); + defineReflective(REGEXP_REPLACE_3, BuiltInMethod.REGEXP_REPLACE3.method); + defineReflective(REGEXP_REPLACE_4, BuiltInMethod.REGEXP_REPLACE4.method); + defineReflective(REGEXP_REPLACE_5, BuiltInMethod.REGEXP_REPLACE5_OCCURRENCE.method, + BuiltInMethod.REGEXP_REPLACE5_MATCHTYPE.method); + defineReflective(REGEXP_REPLACE_5_ORACLE, BuiltInMethod.REGEXP_REPLACE5_OCCURRENCE.method); + defineReflective(REGEXP_REPLACE_6, BuiltInMethod.REGEXP_REPLACE6.method); + defineReflective(REGEXP_REPLACE_BIG_QUERY_3, BuiltInMethod.REGEXP_REPLACE_BIG_QUERY_3.method); + defineReflective(REGEXP_REPLACE_PG_3, BuiltInMethod.REGEXP_REPLACE_PG_3.method); + defineReflective(REGEXP_REPLACE_PG_4, BuiltInMethod.REGEXP_REPLACE_PG_4.method); // Multisets & arrays @@ -2512,37 +2529,6 @@ private static class ContainsSubstrImplementor extends AbstractRexCallImplemento } } - /** Implementor for the {@code REGEXP_REPLACE} function. */ - private static class RegexpReplaceImplementor extends AbstractRexCallImplementor { - RegexpReplaceImplementor() { - super("regexp_replace", NullPolicy.STRICT, false); - } - - @Override Expression implementSafe(final RexToLixTranslator translator, - final RexCall call, final List argValueList) { - // Boolean indicating if dialect uses default $-based indexing for - // regex capturing group (false means double-backslash-based indexing) - final boolean dollarIndexed = - translator.conformance.isRegexReplaceCaptureGroupDollarIndexed(); - - // Standard REGEXP_REPLACE implementation for default indexing. - if (dollarIndexed) { - final ReflectiveImplementor implementor = - new ReflectiveImplementor( - ImmutableList.of(BuiltInMethod.REGEXP_REPLACE3.method, - BuiltInMethod.REGEXP_REPLACE4.method, - BuiltInMethod.REGEXP_REPLACE5.method, - BuiltInMethod.REGEXP_REPLACE6.method)); - return implementor.implementSafe(translator, call, argValueList); - } - - // Custom regexp replace method to preprocess double-backslashes into $-based indices. - return Expressions.call(Expressions.new_(SqlFunctions.RegexFunction.class), - "regexpReplaceNonDollarIndexed", - argValueList); - } - } - /** Implementor for the {@code MONTHNAME} and {@code DAYNAME} functions. * Each takes a {@link java.util.Locale} argument. */ private static class PeriodNameImplementor extends AbstractRexCallImplementor { diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java index 65bc1a0142d..ff51fc6b66a 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -632,6 +632,11 @@ public int regexpInstr(String value, String regex, int position, return matchIndex; } + /** SQL {@code REGEXP_REPLACE} function with 2 arguments. */ + public String regexpReplace(String s, String regex) { + return regexpReplace(s, regex, "", 1, 0, null); + } + /** SQL {@code REGEXP_REPLACE} function with 3 arguments. */ public String regexpReplace(String s, String regex, String replacement) { @@ -644,12 +649,18 @@ public String regexpReplace(String s, String regex, String replacement, return regexpReplace(s, regex, replacement, pos, 0, null); } - /** SQL {@code REGEXP_REPLACE} function with 5 arguments. */ + /** SQL {@code REGEXP_REPLACE} function with 5 arguments. Last argument is occurrence. */ public String regexpReplace(String s, String regex, String replacement, int pos, int occurrence) { return regexpReplace(s, regex, replacement, pos, occurrence, null); } + /** SQL {@code REGEXP_REPLACE} function with 5 arguments. Last argument is match type */ + public String regexpReplace(String s, String regex, String replacement, + int pos, String matchType) { + return regexpReplace(s, regex, replacement, pos, 0, matchType); + } + /** SQL {@code REGEXP_REPLACE} function with 6 arguments. */ public String regexpReplace(String s, String regex, String replacement, int pos, int occurrence, @Nullable String matchType) { @@ -663,6 +674,18 @@ public String regexpReplace(String s, String regex, String replacement, return Unsafe.regexpReplace(s, pattern, replacement, pos, occurrence); } + /** SQL {@code REGEXP_REPLACE} function for PostgreSQL with 3 arguments. */ + public String regexpReplacePg(String s, String regex, String replacement) { + return regexpReplace(s, regex, replacement, 1, 1, null); + } + + /** SQL {@code REGEXP_REPLACE} function for PostgreSQL with 4 arguments. */ + public String regexpReplacePg(String s, String regex, String replacement, String matchType) { + // Translate g flag to occurrence + final int occurrence = matchType.contains("g") ? 0 : 1; + return regexpReplace(s, regex, replacement, 1, occurrence, matchType); + } + /** SQL {@code REGEXP_REPLACE} function with 3 arguments with * {@code \\} based indexing for capturing groups. */ public String regexpReplaceNonDollarIndexed(String s, String regex, @@ -706,6 +729,10 @@ private static int makeRegexpFlags(String stringFlags) { // for consistency. flags &= ~Pattern.DOTALL; break; + case 'g': + // This flag is in PostgreSQL but doesn't apply to other libraries. Skip here since + // this is actually occurrence. + break; default: throw RESOURCE.invalidInputForRegexpReplace(stringFlags).ex(); } diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java index df3dcbd002f..a3069cec11c 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java @@ -565,11 +565,107 @@ static RelDataType deriveTypeSplit(SqlOperatorBinding operatorBinding, OperandTypes.STRING_STRING_OPTIONAL_INTEGER_OPTIONAL_INTEGER_OPTIONAL_INTEGER, SqlFunctionCategory.STRING); - /** The "REGEXP_REPLACE(value, regexp, rep [, pos [, occurrence [, matchType]]])" + /** The "REGEXP_REPLACE(value, regexp)" * function. Replaces all substrings of value that match regexp with * {@code rep} and returns modified value. */ - @LibraryOperator(libraries = {BIG_QUERY, MYSQL, ORACLE, REDSHIFT}) - public static final SqlFunction REGEXP_REPLACE = new SqlRegexpReplaceFunction(); + @LibraryOperator(libraries = {REDSHIFT}) + public static final SqlFunction REGEXP_REPLACE_2 = + new SqlBasicFunction("REGEXP_REPLACE", SqlKind.OTHER_FUNCTION, + SqlSyntax.FUNCTION, true, ReturnTypes.VARCHAR_NULLABLE, null, + OperandHandlers.DEFAULT, OperandTypes.STRING_STRING, 0, + SqlFunctionCategory.STRING, call -> SqlMonotonicity.NOT_MONOTONIC, false) { }; + + /** The "REGEXP_REPLACE(value, regexp, rep)" + * function. Replaces all substrings of value that match regexp with + * {@code rep} and returns modified value. */ + @LibraryOperator(libraries = {MYSQL, ORACLE, REDSHIFT}) + public static final SqlFunction REGEXP_REPLACE_3 = + SqlBasicFunction.create("REGEXP_REPLACE", ReturnTypes.VARCHAR_NULLABLE, + OperandTypes.STRING_STRING_STRING, SqlFunctionCategory.STRING); + + /** The "REGEXP_REPLACE(value, regexp, rep, pos)" + * function. Replaces all substrings of value that match regexp with + * {@code rep} and returns modified value. Start searching value from character position + * pos. */ + @LibraryOperator(libraries = {MYSQL, ORACLE, REDSHIFT}) + public static final SqlFunction REGEXP_REPLACE_4 = + new SqlBasicFunction("REGEXP_REPLACE", SqlKind.OTHER_FUNCTION, + SqlSyntax.FUNCTION, true, ReturnTypes.VARCHAR_NULLABLE, null, + OperandHandlers.DEFAULT, OperandTypes.family(SqlTypeFamily.STRING, SqlTypeFamily.STRING, + SqlTypeFamily.STRING, SqlTypeFamily.INTEGER), + 0, SqlFunctionCategory.STRING, call -> SqlMonotonicity.NOT_MONOTONIC, false) { }; + + /** The "REGEXP_REPLACE(value, regexp, rep, pos, [ occurrence | matchType ])" + * function. Replaces all substrings of value that match regexp with + * {@code rep} and returns modified value. Start searching value from character position + * pos. Replace only the occurrence match or all matches if occurrence is 0. matchType + * is a string of flags to apply to the search. */ + @LibraryOperator(libraries = {MYSQL, REDSHIFT}) + public static final SqlFunction REGEXP_REPLACE_5 = + new SqlBasicFunction("REGEXP_REPLACE", SqlKind.OTHER_FUNCTION, + SqlSyntax.FUNCTION, true, ReturnTypes.VARCHAR_NULLABLE, null, + OperandHandlers.DEFAULT, + OperandTypes.or( + OperandTypes.family(SqlTypeFamily.STRING, SqlTypeFamily.STRING, + SqlTypeFamily.STRING, SqlTypeFamily.INTEGER, SqlTypeFamily.INTEGER), + OperandTypes.family(SqlTypeFamily.STRING, SqlTypeFamily.STRING, + SqlTypeFamily.STRING, SqlTypeFamily.INTEGER, SqlTypeFamily.STRING)), + 0, SqlFunctionCategory.STRING, call -> SqlMonotonicity.NOT_MONOTONIC, false) { }; + + /** The "REGEXP_REPLACE(value, regexp, rep, pos, matchType)" + * function. Replaces all substrings of value that match regexp with + * {@code rep} and returns modified value. Start searching value from character position + * pos. Replace only the occurrence match or all matches if occurrence is 0. */ + @LibraryOperator(libraries = {ORACLE}) + public static final SqlFunction REGEXP_REPLACE_5_ORACLE = + new SqlBasicFunction("REGEXP_REPLACE", SqlKind.OTHER_FUNCTION, + SqlSyntax.FUNCTION, true, ReturnTypes.VARCHAR_NULLABLE, null, + OperandHandlers.DEFAULT, OperandTypes.family(SqlTypeFamily.STRING, SqlTypeFamily.STRING, + SqlTypeFamily.STRING, SqlTypeFamily.INTEGER, SqlTypeFamily.INTEGER), + 0, SqlFunctionCategory.STRING, call -> SqlMonotonicity.NOT_MONOTONIC, false) { }; + + /** The "REGEXP_REPLACE(value, regexp, rep, pos, occurrence, matchType)" + * function. Replaces all substrings of value that match regexp with + * {@code rep} and returns modified value. Start searching value from character position + * pos. Replace only the occurrence match or all matches if occurrence is 0. matchType + * is a string of flags to apply to the search. */ + @LibraryOperator(libraries = {MYSQL, ORACLE, REDSHIFT}) + public static final SqlFunction REGEXP_REPLACE_6 = + new SqlBasicFunction("REGEXP_REPLACE", SqlKind.OTHER_FUNCTION, + SqlSyntax.FUNCTION, true, ReturnTypes.VARCHAR_NULLABLE, null, + OperandHandlers.DEFAULT, OperandTypes.family(SqlTypeFamily.STRING, SqlTypeFamily.STRING, + SqlTypeFamily.STRING, SqlTypeFamily.INTEGER, SqlTypeFamily.INTEGER, SqlTypeFamily.STRING), + 0, SqlFunctionCategory.STRING, call -> SqlMonotonicity.NOT_MONOTONIC, false) { }; + + /** The "REGEXP_REPLACE(value, regexp, rep)" + * function. Replaces all substrings of value that match regexp with + * {@code rep} and returns modified value. */ + @LibraryOperator(libraries = {BIG_QUERY}) + public static final SqlFunction REGEXP_REPLACE_BIG_QUERY_3 = + new SqlBasicFunction("REGEXP_REPLACE", SqlKind.OTHER_FUNCTION, + SqlSyntax.FUNCTION, true, ReturnTypes.VARCHAR_NULLABLE, null, + OperandHandlers.DEFAULT, OperandTypes.STRING_STRING_STRING, 0, + SqlFunctionCategory.STRING, call -> SqlMonotonicity.NOT_MONOTONIC, false) { }; + + /** The "REGEXP_REPLACE(value, regexp, rep)" + * function. Replaces all substrings of value that match regexp with + * {@code rep} and returns modified value. */ + @LibraryOperator(libraries = {POSTGRESQL}, exceptLibraries = REDSHIFT) + public static final SqlFunction REGEXP_REPLACE_PG_3 = + new SqlBasicFunction("REGEXP_REPLACE", SqlKind.OTHER_FUNCTION, + SqlSyntax.FUNCTION, true, ReturnTypes.VARCHAR_NULLABLE, null, + OperandHandlers.DEFAULT, OperandTypes.STRING_STRING_STRING, 0, + SqlFunctionCategory.STRING, call -> SqlMonotonicity.NOT_MONOTONIC, false) { }; + + /** The "REGEXP_REPLACE(value, regexp, rep, flags)" + * function. Replaces all substrings of value that match regexp with + * {@code rep} and returns modified value. flags are applied to the search. */ + @LibraryOperator(libraries = {POSTGRESQL}, exceptLibraries = REDSHIFT) + public static final SqlFunction REGEXP_REPLACE_PG_4 = + new SqlBasicFunction("REGEXP_REPLACE", SqlKind.OTHER_FUNCTION, + SqlSyntax.FUNCTION, true, ReturnTypes.VARCHAR_NULLABLE, null, + OperandHandlers.DEFAULT, OperandTypes.STRING_STRING_STRING_STRING, 0, + SqlFunctionCategory.STRING, call -> SqlMonotonicity.NOT_MONOTONIC, false) { }; /** The "REGEXP_SUBSTR(value, regexp[, position[, occurrence]])" function. * Returns the substring in value that matches the regexp. Returns NULL if there is no match. */ diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlRegexpReplaceFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlRegexpReplaceFunction.java deleted file mode 100644 index e56e74e2eca..00000000000 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlRegexpReplaceFunction.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.calcite.sql.fun; - -import org.apache.calcite.sql.SqlCallBinding; -import org.apache.calcite.sql.SqlFunction; -import org.apache.calcite.sql.SqlFunctionCategory; -import org.apache.calcite.sql.SqlKind; -import org.apache.calcite.sql.SqlOperandCountRange; -import org.apache.calcite.sql.type.OperandTypes; -import org.apache.calcite.sql.type.ReturnTypes; -import org.apache.calcite.sql.type.SqlOperandCountRanges; -import org.apache.calcite.sql.type.SqlTypeFamily; - -import java.util.ArrayList; -import java.util.List; - -/** - * The REGEXP_REPLACE(source_string, pattern, replacement [, pos, occurrence, match_type]) - * searches for a regular expression pattern and replaces every occurrence of the pattern - * with the specified string. - * */ -public class SqlRegexpReplaceFunction extends SqlFunction { - - public SqlRegexpReplaceFunction() { - super("REGEXP_REPLACE", SqlKind.OTHER_FUNCTION, - ReturnTypes.VARCHAR_NULLABLE, - null, null, SqlFunctionCategory.STRING); - } - - @Override public SqlOperandCountRange getOperandCountRange() { - return SqlOperandCountRanges.between(3, 6); - } - - @Override public boolean checkOperandTypes(SqlCallBinding callBinding, - boolean throwOnFailure) { - final int operandCount = callBinding.getOperandCount(); - assert operandCount >= 3; - if (operandCount == 3) { - return OperandTypes.STRING_STRING_STRING - .checkOperandTypes(callBinding, throwOnFailure); - } - final List families = new ArrayList<>(); - families.add(SqlTypeFamily.STRING); - families.add(SqlTypeFamily.STRING); - families.add(SqlTypeFamily.STRING); - for (int i = 3; i < operandCount; i++) { - if (i == 3) { - families.add(SqlTypeFamily.INTEGER); - } - if (i == 4) { - families.add(SqlTypeFamily.INTEGER); - } - if (i == 5) { - families.add(SqlTypeFamily.STRING); - } - } - return OperandTypes.family(families.toArray(new SqlTypeFamily[0])) - .checkOperandTypes(callBinding, throwOnFailure); - } -} diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java index 1bdc66f420d..5ba35b889b6 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java +++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java @@ -465,6 +465,10 @@ public static SqlOperandTypeChecker variadic( // Second and Third operand both are optional (operand index 0, 1, 2) number -> number == 1 || number == 2); + public static final FamilyOperandTypeChecker STRING_STRING_STRING_STRING = + family(SqlTypeFamily.STRING, SqlTypeFamily.STRING, SqlTypeFamily.STRING, + SqlTypeFamily.STRING); + public static final FamilyOperandTypeChecker STRING_NUMERIC_OPTIONAL_STRING = family( ImmutableList.of(SqlTypeFamily.STRING, SqlTypeFamily.NUMERIC, diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java index 14c2c73e35a..3205a48b409 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java @@ -105,10 +105,6 @@ public abstract class SqlAbstractConformance implements SqlConformance { return SqlConformanceEnum.DEFAULT.isOffsetLimitAllowed(); } - @Override public boolean isRegexReplaceCaptureGroupDollarIndexed() { - return SqlConformanceEnum.DEFAULT.isRegexReplaceCaptureGroupDollarIndexed(); - } - @Override public boolean isPercentRemainderAllowed() { return SqlConformanceEnum.DEFAULT.isPercentRemainderAllowed(); } diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java index 896db041e87..b8501f02f14 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java @@ -256,22 +256,6 @@ public interface SqlConformance { */ boolean isMinusAllowed(); - /** - * Whether this dialect uses {@code $} (dollar) for indexing capturing groups - * in the replacement string of regular expression functions such as - * {@code REGEXP_REPLACE}. If false, the dialect uses {@code \\} (backslash) - * for indexing capturing groups. - * - *

For example, {@code REGEXP_REPLACE("abc", "a(.)c", "X\\1")} in BigQuery - * is equivalent to {@code REGEXP_REPLACE("abc", "a(.)c", "X$1")} in MySQL; - * both produce the result "Xb". - * - *

Among the built-in conformance levels, false in - * {@link SqlConformanceEnum#BIG_QUERY}; - * true otherwise. - */ - boolean isRegexReplaceCaptureGroupDollarIndexed(); - /** * Whether {@code CROSS APPLY} and {@code OUTER APPLY} operators are allowed * in the parser. diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java index 1b7bd1a811d..a610f19f169 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java @@ -240,15 +240,6 @@ public enum SqlConformanceEnum implements SqlConformance { } } - @Override public boolean isRegexReplaceCaptureGroupDollarIndexed() { - switch (this) { - case BIG_QUERY: - return false; - default: - return true; - } - } - @Override public boolean isPercentRemainderAllowed() { switch (this) { case BABEL: diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java index 7b60bb4445b..c8e2f7cdd2e 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java @@ -87,10 +87,6 @@ protected SqlDelegatingConformance(SqlConformance delegate) { return delegate.isMinusAllowed(); } - @Override public boolean isRegexReplaceCaptureGroupDollarIndexed() { - return delegate.isRegexReplaceCaptureGroupDollarIndexed(); - } - @Override public boolean isApplyAllowed() { return delegate.isApplyAllowed(); } diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java index 3dd78e8772c..bc5ca1d61ab 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -586,15 +586,25 @@ public enum BuiltInMethod { String.class, String.class, int.class, int.class, int.class), REGEXP_LIKE3(SqlFunctions.RegexFunction.class, "regexpLike", String.class, String.class, String.class), + REGEXP_REPLACE2(SqlFunctions.RegexFunction.class, "regexpReplace", + String.class, String.class), REGEXP_REPLACE3(SqlFunctions.RegexFunction.class, "regexpReplace", String.class, String.class, String.class), REGEXP_REPLACE4(SqlFunctions.RegexFunction.class, "regexpReplace", String.class, String.class, String.class, int.class), - REGEXP_REPLACE5(SqlFunctions.RegexFunction.class, "regexpReplace", + REGEXP_REPLACE5_OCCURRENCE(SqlFunctions.RegexFunction.class, "regexpReplace", String.class, String.class, String.class, int.class, int.class), + REGEXP_REPLACE5_MATCHTYPE(SqlFunctions.RegexFunction.class, "regexpReplace", + String.class, String.class, String.class, int.class, String.class), REGEXP_REPLACE6(SqlFunctions.RegexFunction.class, "regexpReplace", String.class, String.class, String.class, int.class, int.class, String.class), + REGEXP_REPLACE_BIG_QUERY_3(SqlFunctions.RegexFunction.class, "regexpReplaceNonDollarIndexed", + String.class, String.class, String.class), + REGEXP_REPLACE_PG_3(SqlFunctions.RegexFunction.class, "regexpReplacePg", + String.class, String.class, String.class), + REGEXP_REPLACE_PG_4(SqlFunctions.RegexFunction.class, "regexpReplacePg", + String.class, String.class, String.class, String.class), IS_TRUE(SqlFunctions.class, "isTrue", Boolean.class), IS_NOT_FALSE(SqlFunctions.class, "isNotFalse", Boolean.class), NOT(SqlFunctions.class, "not", Boolean.class), diff --git a/core/src/main/java/org/apache/calcite/util/RelToSqlConverterUtil.java b/core/src/main/java/org/apache/calcite/util/RelToSqlConverterUtil.java index 36816a2bfac..6b510e41417 100644 --- a/core/src/main/java/org/apache/calcite/util/RelToSqlConverterUtil.java +++ b/core/src/main/java/org/apache/calcite/util/RelToSqlConverterUtil.java @@ -26,7 +26,7 @@ import org.apache.calcite.sql.fun.SqlTrimFunction; import org.apache.calcite.sql.parser.SqlParserPos; -import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE_3; import static java.util.Objects.requireNonNull; @@ -58,7 +58,7 @@ public static void unparseHiveTrim( final SqlCharStringLiteral blankLiteral = SqlLiteral.createCharString("", call.getParserPosition()); final SqlNode[] trimOperands = new SqlNode[] { call.operand(2), regexNode, blankLiteral }; - final SqlCall regexReplaceCall = REGEXP_REPLACE.createCall(SqlParserPos.ZERO, trimOperands); + final SqlCall regexReplaceCall = REGEXP_REPLACE_3.createCall(SqlParserPos.ZERO, trimOperands); regexReplaceCall.unparse(writer, leftPrec, rightPrec); } } diff --git a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java index c2dab833048..0f9f9049ed9 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java @@ -495,6 +495,7 @@ static List list() { @Test void testRegexpReplace() { final SqlFunctions.RegexFunction f = new SqlFunctions.RegexFunction(); + assertThat(f.regexpReplace("abc", "b"), is("ac")); assertThat(f.regexpReplace("a b c", "b", "X"), is("a X c")); assertThat(f.regexpReplace("abc def ghi", "[g-z]+", "X"), is("abc def X")); assertThat(f.regexpReplace("abc def ghi", "[a-z]+", "X"), is("X X X")); @@ -513,6 +514,11 @@ static List list() { is("abc def GHI")); assertThat(f.regexpReplace("abc def GHI", "[a-z]+", "X", 1, 3, "i"), is("abc def X")); + assertThat(f.regexpReplacePg("abc def GHI", "[a-z]+", "X"), is("X def GHI")); + assertThat(f.regexpReplacePg("abc def GHI", "[a-z]+", "X", "g"), + is("X X GHI")); + assertThat(f.regexpReplacePg("ABC def GHI", "[a-z]+", "X", "i"), + is("X def GHI")); try { f.regexpReplace("abc def ghi", "[a-z]+", "X", 0); diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java index 927274d21c6..2ff73fb2edc 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -11946,6 +11946,33 @@ private void checkCustomColumnResolving(String table) { .columnType("VARCHAR NOT NULL"); } + @Test void testPgRegexpReplace() { + final SqlOperatorTable opTable = operatorTableFor(SqlLibrary.POSTGRESQL); + + expr("REGEXP_REPLACE('a b c', 'a', 'X')") + .withOperatorTable(opTable) + .columnType("VARCHAR NOT NULL"); + expr("REGEXP_REPLACE('abc def ghi', '[a-z]+', 'X')") + .withOperatorTable(opTable) + .columnType("VARCHAR NOT NULL"); + expr("REGEXP_REPLACE('abc def ghi', '[a-z]+', 'X')") + .withOperatorTable(opTable) + .columnType("VARCHAR NOT NULL"); + expr("REGEXP_REPLACE('abc def GHI', '[a-z]+', 'X', 'c')") + .withOperatorTable(opTable) + .columnType("VARCHAR NOT NULL"); + // Implicit type coercion. + expr("REGEXP_REPLACE(null, '(-)', '###')") + .withOperatorTable(opTable) + .columnType("VARCHAR"); + expr("REGEXP_REPLACE('100-200', null, '###')") + .withOperatorTable(opTable) + .columnType("VARCHAR"); + expr("REGEXP_REPLACE('100-200', '(-)', null)") + .withOperatorTable(opTable) + .columnType("VARCHAR"); + } + @Test void testInvalidFunctionCall() { final SqlOperatorTable operatorTable = MockSqlOperatorTable.standard().extend(); diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 180c3aab51d..b3565bc1fa2 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -2854,7 +2854,9 @@ In the following: | b | REGEXP_EXTRACT_ALL(string, regexp) | Returns an array of all substrings in *string* that matches the *regexp*. Returns an empty array if there is no match | b | REGEXP_INSTR(string, regexp [, position [, occurrence [, occurrence_position]]]) | Returns the lowest 1-based position of the substring in *string* that matches the *regexp*, starting search at *position* (default 1), and until locating the nth *occurrence* (default 1). Setting occurrence_position (default 0) to 1 returns the end position of substring + 1. Returns 0 if there is no match | m o p r s | REGEXP_LIKE(string, regexp [, flags]) | Equivalent to `string1 RLIKE string2` with an optional parameter for search flags. Supported flags are:

  • i: case-insensitive matching
  • c: case-sensitive matching
  • n: newline-sensitive matching
  • s: non-newline-sensitive matching
  • m: multi-line
+| r | REGEXP_REPLACE(string, regexp) | Replaces all substrings of *string* that match *regexp* with the empty string | b m o r | REGEXP_REPLACE(string, regexp, rep [, pos [, occurrence [, matchType]]]) | Replaces all substrings of *string* that match *regexp* with *rep* at the starting *pos* in expr (if omitted, the default is 1), *occurrence* specifies which occurrence of a match to search for (if omitted, the default is 1), *matchType* specifies how to perform matching +| p | REGEXP_REPLACE(string, regexp, rep [, matchType]) | Replaces substrings of *string* that match *regexp* with *rep* at the starting *pos* in expr, *matchType* specifies how to perform matching and whether to only replace first match or all | b | REGEXP_SUBSTR(string, regexp [, position [, occurrence]]) | Synonym for REGEXP_EXTRACT | b m p r s | REPEAT(string, integer) | Returns a string consisting of *string* repeated of *integer* times; returns an empty string if *integer* is less than 1 | b m | REVERSE(string) | Returns *string* with the order of the characters reversed diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java index 4427dcd83fe..7e880e94c62 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -5829,10 +5829,32 @@ private static void checkIf(SqlOperatorFixture f) { f.checkQuery("select regexp_instr('a9cadca5c4aecghi', 'a[0-9]c', 1, 3)"); } - @Test void testRegexpReplaceFunc() { + @Test void testRegexpReplace2Func() { final SqlOperatorFixture f0 = fixture(); final Consumer consumer = f -> { - f.setFor(SqlLibraryOperators.REGEXP_REPLACE); + f.setFor(SqlLibraryOperators.REGEXP_REPLACE_2); + + // Tests for regexp replace generic functionality + f.checkString("regexp_replace('a b c', 'b')", "a c", + "VARCHAR NOT NULL"); + f.checkString("regexp_replace('abc1 def2 ghi3', '[a-z]+')", "1 2 3", + "VARCHAR NOT NULL"); + f.checkString("regexp_replace('100-200', '(\\d+)')", "-", + "VARCHAR NOT NULL"); + f.checkString("regexp_replace('100-200', '(-)')", "100200", + "VARCHAR NOT NULL"); + + f.checkQuery("select regexp_replace('a b c', 'b')"); + }; + final List libraries = + list(SqlLibrary.REDSHIFT); + f0.forEachLibrary(libraries, consumer); + } + + @Test void testRegexpReplace3Func() { + final SqlOperatorFixture f0 = fixture(); + final Consumer consumer = f -> { + f.setFor(SqlLibraryOperators.REGEXP_REPLACE_3); // Tests for regexp replace generic functionality f.checkString("regexp_replace('a b c', 'b', 'X')", "a X c", @@ -5846,16 +5868,6 @@ private static void checkIf(SqlOperatorFixture f) { f.checkNull("regexp_replace(cast(null as varchar), '(-)', '###')"); f.checkNull("regexp_replace('100-200', cast(null as varchar), '###')"); f.checkNull("regexp_replace('100-200', '(-)', cast(null as varchar))"); - f.checkString("regexp_replace('abc def ghi', '[a-z]+', 'X', 2)", "aX X X", - "VARCHAR NOT NULL"); - f.checkString("regexp_replace('abc def ghi', '[a-z]+', 'X', 1, 3)", "abc def X", - "VARCHAR NOT NULL"); - f.checkString("regexp_replace('abc def GHI', '[a-z]+', 'X', 1, 3, 'c')", "abc def GHI", - "VARCHAR NOT NULL"); - f.checkString("regexp_replace('abc def GHI', '[a-z]+', 'X', 1, 3, 'i')", "abc def X", - "VARCHAR NOT NULL"); - f.checkString("regexp_replace('abc def GHI', '[a-z]+', 'X', 1, 3, 'i')", "abc def X", - "VARCHAR NOT NULL"); f.checkString("regexp_replace('abc\t\ndef\t\nghi', '\t', '+')", "abc+\ndef+\nghi", "VARCHAR NOT NULL"); f.checkString("regexp_replace('abc\t\ndef\t\nghi', '\t\n', '+')", "abc+def+ghi", @@ -5864,38 +5876,141 @@ private static void checkIf(SqlOperatorFixture f) { "VARCHAR NOT NULL"); f.checkQuery("select regexp_replace('a b c', 'b', 'X')"); + }; + final List libraries = + list(SqlLibrary.BIG_QUERY, SqlLibrary.MYSQL, SqlLibrary.ORACLE, SqlLibrary.REDSHIFT); + f0.forEachLibrary(libraries, consumer); + + // Tests to verify double-backslashes are ignored for indexing in other dialects + final SqlOperatorFixture f2 = + f0.withLibrary(SqlLibrary.MYSQL).withConformance(SqlConformanceEnum.MYSQL_5); + f2.checkString("regexp_replace('abc16', 'b(.*)(\\d)', '\\\\2\\\\1X')", "a\\2\\1X", + "VARCHAR NOT NULL"); + f2.checkString("regexp_replace('abcdefghijabc', 'abc(.)', '\\\\-11x')", "\\-11xefghijabc", + "VARCHAR NOT NULL"); + f2.checkString("regexp_replace('abcdefghijabc', 'abc(.)', '$1x')", "dxefghijabc", + "VARCHAR NOT NULL"); + } + + @Test void testRegexpReplace4Func() { + final SqlOperatorFixture f0 = fixture(); + final Consumer consumer = f -> { + f.setFor(SqlLibraryOperators.REGEXP_REPLACE_4); + + // Tests for regexp replace generic functionality + f.checkString("regexp_replace('abc def ghi', '[a-z]+', 'X', 2)", "aX X X", + "VARCHAR NOT NULL"); f.checkQuery("select regexp_replace('a b c', 'b', 'X', 1)"); + }; + final List libraries = + list(SqlLibrary.MYSQL, SqlLibrary.ORACLE, SqlLibrary.REDSHIFT); + f0.forEachLibrary(libraries, consumer); + } + + @Test void testRegexpReplace5Func() { + final SqlOperatorFixture f0 = fixture(); + final Consumer consumer = f -> { + f.setFor(SqlLibraryOperators.REGEXP_REPLACE_5); + + // Tests for regexp replace generic functionality + f.checkString("regexp_replace('abc def ghi', '[a-z]+', 'X', 1, 3)", "abc def X", + "VARCHAR NOT NULL"); + f.checkString("regexp_replace('ABC def ghi', '[a-z]+', 'X', 1, 'i')", "X X X", + "VARCHAR NOT NULL"); f.checkQuery("select regexp_replace('a b c', 'b', 'X', 1, 3)"); + f.checkQuery("select regexp_replace('a b c', 'b', 'X', 1, 'i')"); + }; + final List libraries = + list(SqlLibrary.MYSQL, SqlLibrary.REDSHIFT); + f0.forEachLibrary(libraries, consumer); + } + + @Test void testRegexpReplace5OracleFunc() { + final SqlOperatorFixture f = fixture().withLibrary(SqlLibrary.ORACLE); + f.setFor(SqlLibraryOperators.REGEXP_REPLACE_5_ORACLE); + + // Tests for regexp replace generic functionality + f.checkString("regexp_replace('abc def ghi', '[a-z]+', 'X', 1, 3)", "abc def X", + "VARCHAR NOT NULL"); + f.checkQuery("select regexp_replace('a b c', 'b', 'X', 1, 1)"); + } + + @Test void testRegexpReplace6Func() { + final SqlOperatorFixture f0 = fixture(); + final Consumer consumer = f -> { + f.setFor(SqlLibraryOperators.REGEXP_REPLACE_6); + + // Tests for regexp replace generic functionality + f.checkString("regexp_replace('abc def GHI', '[a-z]+', 'X', 1, 3, 'c')", "abc def GHI", + "VARCHAR NOT NULL"); + f.checkString("regexp_replace('abc def GHI', '[a-z]+', 'X', 1, 3, 'i')", "abc def X", + "VARCHAR NOT NULL"); + f.checkQuery("select regexp_replace('a b c', 'b', 'X', 1, 3, 'i')"); }; final List libraries = - list(SqlLibrary.BIG_QUERY, SqlLibrary.MYSQL, SqlLibrary.ORACLE, - SqlLibrary.REDSHIFT); + list(SqlLibrary.MYSQL, SqlLibrary.ORACLE, SqlLibrary.REDSHIFT); f0.forEachLibrary(libraries, consumer); + } + + @Test void testRegexpReplaceBigQuery3Func() { + final SqlOperatorFixture f = fixture().setFor(SqlLibraryOperators.REGEXP_REPLACE_BIG_QUERY_3) + .withLibrary(SqlLibrary.BIG_QUERY); // Tests for double-backslash indexed capturing groups for regexp_replace in BQ - final SqlOperatorFixture f1 = - f0.withLibrary(SqlLibrary.BIG_QUERY).withConformance(SqlConformanceEnum.BIG_QUERY); - f1.checkString("regexp_replace('abc16', 'b(.*)(\\d)', '\\\\2\\\\1X')", "a6c1X", + f.checkString("regexp_replace('abc16', 'b(.*)(\\d)', '\\\\2\\\\1X')", "a6c1X", "VARCHAR NOT NULL"); - f1.checkString("regexp_replace('a\\bc56a\\bc37', 'b(.)(\\d)', '\\\\2\\\\0X')", + f.checkString("regexp_replace('a\\bc56a\\bc37', 'b(.)(\\d)', '\\\\2\\\\0X')", "a\\5bc5X6a\\3bc3X7", "VARCHAR NOT NULL"); - f1.checkString("regexp_replace('abcdefghijabc', 'abc(.)', '\\\\\\\\123xyz')", + f.checkString("regexp_replace('abcdefghijabc', 'abc(.)', '\\\\\\\\123xyz')", "\\123xyzefghijabc", "VARCHAR NOT NULL"); - f1.checkString("regexp_replace('abcdefghijabc', 'abc(.)', '$1xy')", + f.checkString("regexp_replace('abcdefghijabc', 'abc(.)', '$1xy')", "$1xyefghijabc", "VARCHAR NOT NULL"); - f1.checkString("regexp_replace('abc123', 'b(.*)(\\d)', '\\\\\\\\$ $\\\\\\\\')", + f.checkString("regexp_replace('abc123', 'b(.*)(\\d)', '\\\\\\\\$ $\\\\\\\\')", "a\\$ $\\", "VARCHAR NOT NULL"); - // Tests to verify double-backslashes are ignored for indexing in other dialects - final SqlOperatorFixture f2 = - f0.withLibrary(SqlLibrary.MYSQL).withConformance(SqlConformanceEnum.MYSQL_5); - f2.checkString("regexp_replace('abc16', 'b(.*)(\\d)', '\\\\2\\\\1X')", "a\\2\\1X", + f.checkQuery("select regexp_replace('a b c', 'b', 'X')"); + } + + @Test void testRegexpReplacePg3Func() { + final SqlOperatorFixture f = fixture().setFor(SqlLibraryOperators.REGEXP_REPLACE_PG_3) + .withLibrary(SqlLibrary.POSTGRESQL); + + // Tests for regexp replace generic functionality + f.checkString("regexp_replace('a b c', 'b', 'X')", "a X c", "VARCHAR NOT NULL"); - f2.checkString("regexp_replace('abcdefghijabc', 'abc(.)', '\\\\-11x')", "\\-11xefghijabc", + f.checkString("regexp_replace('abc def ghi', '[a-z]+', 'X')", "X def ghi", "VARCHAR NOT NULL"); - f2.checkString("regexp_replace('abcdefghijabc', 'abc(.)', '$1x')", "dxefghijabc", + f.checkString("regexp_replace('100-200', '(\\d+)', 'num')", "num-200", "VARCHAR NOT NULL"); + f.checkString("regexp_replace('100-200', '(-)', '###')", "100###200", + "VARCHAR NOT NULL"); + f.checkNull("regexp_replace(cast(null as varchar), '(-)', '###')"); + f.checkNull("regexp_replace('100-200', cast(null as varchar), '###')"); + f.checkNull("regexp_replace('100-200', '(-)', cast(null as varchar))"); + f.checkString("regexp_replace('abc\t\ndef\t\nghi', '\t', '+')", "abc+\ndef\t\nghi", + "VARCHAR NOT NULL"); + f.checkString("regexp_replace('abc\t\ndef\t\nghi', '\t\n', '+')", "abc+def\t\nghi", + "VARCHAR NOT NULL"); + f.checkString("regexp_replace('abc\t\ndef\t\nghi', '\\w+', '+')", "+\t\ndef\t\nghi", + "VARCHAR NOT NULL"); + + f.checkQuery("select regexp_replace('a b c', 'b', 'X')"); + } + + @Test void testRegexpReplacePg4Func() { + final SqlOperatorFixture f = fixture().setFor(SqlLibraryOperators.REGEXP_REPLACE_PG_4) + .withLibrary(SqlLibrary.POSTGRESQL); + + // Tests for regexp replace generic functionality + f.checkString("regexp_replace('abc def GHI', '[a-z]+', 'X', 'c')", "X def GHI", + "VARCHAR NOT NULL"); + f.checkString("regexp_replace('ABC def GHI', '[a-z]+', 'X', 'i')", "X def GHI", + "VARCHAR NOT NULL"); + f.checkString("regexp_replace('abc def ghi', '[a-z]+', 'X', 'g')", "X X X", + "VARCHAR NOT NULL"); + + f.checkQuery("select regexp_replace('a b c', 'b', 'X', 'i')"); } @Test void testRegexpExtractFunc() {