Skip to content

Commit

Permalink
[CALCITE-6791] Search pattern during matching in REPLACE function sho…
Browse files Browse the repository at this point in the history
…uld be case insensitive in MSSQL
  • Loading branch information
ILuffZhe committed Jan 23, 2025
1 parent 3fce658 commit d83122b
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
import org.apache.calcite.sql.fun.SqlTrimFunction;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.sql.validate.SqlConformanceEnum;
import org.apache.calcite.sql.validate.SqlUserDefinedAggFunction;
import org.apache.calcite.sql.validate.SqlUserDefinedFunction;
import org.apache.calcite.sql.validate.SqlUserDefinedTableFunction;
Expand Down Expand Up @@ -699,7 +700,7 @@ void populate1() {
defineMethod(RPAD, BuiltInMethod.RPAD.method, NullPolicy.STRICT);
defineMethod(STARTS_WITH, BuiltInMethod.STARTS_WITH.method, NullPolicy.STRICT);
defineMethod(ENDS_WITH, BuiltInMethod.ENDS_WITH.method, NullPolicy.STRICT);
defineMethod(REPLACE, BuiltInMethod.REPLACE.method, NullPolicy.STRICT);
define(REPLACE, new ReplaceImplementor());
defineMethod(TRANSLATE3, BuiltInMethod.TRANSLATE3.method, NullPolicy.STRICT);
defineMethod(CHR, BuiltInMethod.CHAR_FROM_UTF8.method, NullPolicy.STRICT);
defineMethod(CHARACTER_LENGTH, BuiltInMethod.CHAR_LENGTH.method,
Expand Down Expand Up @@ -4967,4 +4968,22 @@ private static class ToTimestampPgImplementor extends AbstractRexCallImplementor
return Expressions.call(target, method, operand0, operand1);
}
}

/** Implementor for the {@code REPLACE} function for Oracle, PostgreSQL and
* Microsoft SQL Server. And search pattern for SQL Server is case-insensitive. */
private static class ReplaceImplementor extends AbstractRexCallImplementor {
ReplaceImplementor() {
super("replace", NullPolicy.STRICT, false);
}

@Override Expression implementSafe(RexToLixTranslator translator, RexCall call,
List<Expression> argValueList) {
boolean isCaseSensitive = translator.conformance != SqlConformanceEnum.SQL_SERVER_2008;
final Expression operand0 = argValueList.get(0);
final Expression operand1 = argValueList.get(1);
final Expression operand2 = argValueList.get(2);
return Expressions.call(BuiltInMethod.REPLACE.method,
operand0, operand1, operand2, Expressions.constant(isCaseSensitive));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5781,11 +5781,16 @@ public static String translate3(String s, String search, String replacement) {
}

/** SQL {@code REPLACE(string, search, replacement)} function. */
public static String replace(String s, String search, String replacement) {
public static String replace(String s, String search, String replacement,
boolean isCaseSensitive) {
if (search.isEmpty()) {
return s;
}
return s.replace(search, replacement);
if (isCaseSensitive) {
return s.replace(search, replacement);
}
// for MSSQL's REPLACE function, search pattern is case-insensitive during matching
return org.apache.commons.lang3.StringUtils.replaceIgnoreCase(s, search, replacement);
}

/** Helper for "array element reference". Caller has already ensured that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1697,7 +1697,13 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
public static final SqlFunction SUBSTRING = new SqlSubstringFunction();

/** The {@code REPLACE(string, search, replace)} function. Not standard SQL,
* but in Oracle, PostgreSQL and Microsoft SQL Server. */
* but in Oracle, PostgreSQL and Microsoft SQL Server.
*
* <p>REPLACE behaves a little different in Microsoft SQL Server,
* whose search pattern is case-insensitive during matching.
*
* <p>For example, {@code REPLACE(('ciAao', 'a', 'ciao'))} returns "ciAciaoo" in both
* Oracle and PostgreSQL, but returns "ciciaociaoo" in Microsoft SQL Server. */
public static final SqlFunction REPLACE =
SqlBasicFunction.create("REPLACE", ReturnTypes.VARCHAR_NULLABLE,
OperandTypes.STRING_STRING_STRING, SqlFunctionCategory.STRING);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ public enum BuiltInMethod {
TRIM(SqlFunctions.class, "trim", boolean.class, boolean.class, String.class,
String.class, boolean.class),
REPLACE(SqlFunctions.class, "replace", String.class, String.class,
String.class),
String.class, boolean.class),
TRANSLATE_WITH_CHARSET(SqlFunctions.class, "translateWithCharset", String.class, String.class),
TRANSLATE3(SqlFunctions.class, "translate3", String.class, String.class, String.class),
LTRIM(SqlFunctions.class, "ltrim", String.class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ public static void main(String[] args) throws Exception {
SqlConformanceEnum.ORACLE_10)
.with(CalciteAssert.Config.SCOTT)
.connect();
case "scott-mssql":
// Same as "scott", but uses SQL_SERVER_2008 conformance.
return CalciteAssert.that()
.with(CalciteConnectionProperty.PARSER_FACTORY,
ExtensionDdlExecutor.class.getName() + "#PARSER_FACTORY")
.with(CalciteConnectionProperty.CONFORMANCE,
SqlConformanceEnum.SQL_SERVER_2008)
.with(CalciteAssert.Config.SCOTT)
.connect();
case "steelwheels":
return CalciteAssert.that()
.with(CalciteConnectionProperty.PARSER_FACTORY,
Expand Down
14 changes: 9 additions & 5 deletions core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -560,11 +560,15 @@ static <E> List<E> list() {
}

@Test void testReplace() {
assertThat(replace("", "ciao", "ci"), is(""));
assertThat(replace("ciao", "ciao", ""), is(""));
assertThat(replace("ciao", "", "ciao"), is("ciao"));
assertThat(replace("ci ao", " ", "ciao"), is("ciciaoao"));
assertThat(replace("hello world", "o", ""), is("hell wrld"));
assertThat(replace("", "ciao", "ci", true), is(""));
assertThat(replace("ciao", "ciao", "", true), is(""));
assertThat(replace("ciao", "", "ciao", true), is("ciao"));
assertThat(replace("ci ao", " ", "ciao", true), is("ciciaoao"));
assertThat(replace("ciAao", "a", "ciao", true), is("ciAciaoo"));
assertThat(replace("ciAao", "A", "ciao", true), is("ciciaoao"));
assertThat(replace("ciAao", "a", "ciao", false), is("ciciaociaoo"));
assertThat(replace("ciAao", "A", "ciao", false), is("ciciaociaoo"));
assertThat(replace("hello world", "o", "", true), is("hell wrld"));
}

@Test void testRegexpReplace() {
Expand Down
22 changes: 22 additions & 0 deletions core/src/test/resources/sql/functions.iq
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,28 @@ from t;

!ok

# [CALCITE-6791] Search pattern during matching in REPLACE function should be case insensitive in MSSQL
!use scott-mssql
select replace('ciAao', 'a', 'ciao');
+-------------+
| EXPR$0 |
+-------------+
| ciciaociaoo |
+-------------+
(1 row)

!ok

select replace('ciAao', 'A', 'ciao');
+-------------+
| EXPR$0 |
+-------------+
| ciciaociaoo |
+-------------+
(1 row)

!ok

# concat in BigQuery
!use post-big-query

Expand Down
2 changes: 1 addition & 1 deletion site/_docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1914,7 +1914,7 @@ period:
| {fn LOCATE(string1, string2 [, integer])} | Returns the position in *string2* of the first occurrence of *string1*. Searches from the beginning of *string2*, unless *integer* is specified.
| {fn LEFT(string, length)} | Returns the leftmost *length* characters from *string*
| {fn LTRIM(string)} | Returns *string* with leading space characters removed
| {fn REPLACE(string, search, replacement)} | Returns a string in which all the occurrences of *search* in *string* are replaced with *replacement*; returns unchanged *string* if *search* is an empty string(''); if *replacement* is the empty string, the occurrences of *search* are removed
| {fn REPLACE(string, search, replacement)} | Returns a string in which all the occurrences of *search* in *string* are replaced with *replacement*; returns unchanged *string* if *search* is an empty string(''); if *replacement* is the empty string, the occurrences of *search* are removed. Matching between *search* and *string* is case-insensitive under SQL Server semantics
| {fn REVERSE(string)} | Returns *string* with the order of the characters reversed
| {fn RIGHT(string, length)} | Returns the rightmost *length* characters from *string*
| {fn RTRIM(string)} | Returns *string* with trailing space characters removed
Expand Down
24 changes: 24 additions & 0 deletions testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4832,6 +4832,30 @@ static void checkRlikeFails(SqlOperatorFixture f) {
* REPLACE function returns wrong result when search pattern is an empty string</a>. */
@Test void testReplaceFunc() {
final SqlOperatorFixture f = fixture();
checkReplaceFunc(f);
// case-sensitive
f.checkString("REPLACE('ciAao', 'a', 'ciao')", "ciAciaoo",
"VARCHAR NOT NULL");
f.checkString("REPLACE('ciAao', 'A', 'ciao')", "ciciaoao",
"VARCHAR NOT NULL");
}

/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-6791">[CALCITE-6791]
* Search pattern during matching in REPLACE function should be case insensitive
* in MSSQL</a>. */
@Test void testReplaceMSSQLFunc() {
final SqlOperatorFixture f = fixture();
checkReplaceFunc(f);
// case-insensitive
SqlOperatorFixture f1 = f.withConformance(SqlConformanceEnum.SQL_SERVER_2008);
f1.checkString("REPLACE('ciAao', 'a', 'ciao')", "ciciaociaoo",
"VARCHAR NOT NULL");
f1.checkString("REPLACE('ciAao', 'A', 'ciao')", "ciciaociaoo",
"VARCHAR NOT NULL");
}

private static void checkReplaceFunc(SqlOperatorFixture f) {
f.setFor(SqlStdOperatorTable.REPLACE, VmName.EXPAND);
f.checkString("REPLACE('ciao', 'ciao', '')", "",
"VARCHAR NOT NULL");
Expand Down

0 comments on commit d83122b

Please sign in to comment.