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

zhehu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new d83122b86e [CALCITE-6791] Search pattern during matching in REPLACE 
function should be case insensitive in MSSQL
d83122b86e is described below

commit d83122b86e8030ffb12314ca5e7414a19b95f218
Author: Zhe Hu <[email protected]>
AuthorDate: Tue Jan 21 09:45:51 2025 +0800

    [CALCITE-6791] Search pattern during matching in REPLACE function should be 
case insensitive in MSSQL
---
 .../calcite/adapter/enumerable/RexImpTable.java    | 21 ++++++++++++++++++-
 .../org/apache/calcite/runtime/SqlFunctions.java   |  9 ++++++--
 .../calcite/sql/fun/SqlStdOperatorTable.java       |  8 +++++++-
 .../org/apache/calcite/util/BuiltInMethod.java     |  2 +-
 .../org/apache/calcite/test/CoreQuidemTest.java    |  9 ++++++++
 .../org/apache/calcite/test/SqlFunctionsTest.java  | 14 ++++++++-----
 core/src/test/resources/sql/functions.iq           | 22 ++++++++++++++++++++
 site/_docs/reference.md                            |  2 +-
 .../org/apache/calcite/test/SqlOperatorTest.java   | 24 ++++++++++++++++++++++
 9 files changed, 100 insertions(+), 11 deletions(-)

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 3c071376b8..1a84edc3d6 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
@@ -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;
@@ -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,
@@ -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));
+    }
+  }
 }
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 4b82aca272..e664c1a6f3 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -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
diff --git 
a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
index 977db8ac1f..204900e7f4 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
@@ -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);
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 6f3fbd71c7..8eceedff48 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -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),
diff --git a/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java 
b/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java
index abcd2fd6ef..9d19ec653e 100644
--- a/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java
+++ b/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java
@@ -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,
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 6e55a052d8..439560d9c3 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
@@ -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() {
diff --git a/core/src/test/resources/sql/functions.iq 
b/core/src/test/resources/sql/functions.iq
index b9ba622f33..1d6ea00768 100644
--- a/core/src/test/resources/sql/functions.iq
+++ b/core/src/test/resources/sql/functions.iq
@@ -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
 
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 6d1148e764..48d2d62a97 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -1914,7 +1914,7 @@ #### String
 | {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
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 3512f6a9e4..1fbded454a 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -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");

Reply via email to