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

xuzifu666 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 738a0d7b1c [CALCITE-7693] Move MongoDB LIKE-to-regex conversion into 
runtime.Like for consistency
738a0d7b1c is described below

commit 738a0d7b1c9acdad7d2c450ef7a9cdd2c3923183
Author: Yu Xu <[email protected]>
AuthorDate: Thu Aug 6 14:25:03 2026 +0800

    [CALCITE-7693] Move MongoDB LIKE-to-regex conversion into runtime.Like for 
consistency
---
 .../main/java/org/apache/calcite/runtime/Like.java | 66 ++++++++++++++++++
 .../java/org/apache/calcite/runtime/LikeTest.java  | 50 ++++++++++++++
 .../calcite/adapter/mongodb/MongoFilter.java       | 80 +++-------------------
 3 files changed, 124 insertions(+), 72 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/runtime/Like.java 
b/core/src/main/java/org/apache/calcite/runtime/Like.java
index ac074afa3d..376e7ca58d 100644
--- a/core/src/main/java/org/apache/calcite/runtime/Like.java
+++ b/core/src/main/java/org/apache/calcite/runtime/Like.java
@@ -110,6 +110,72 @@ static String sqlToRegexLike(
     return javaPattern.toString();
   }
 
+  /**
+   * Translates a SQL LIKE pattern to an anchored regular expression, with an
+   * optional escape string.
+   *
+   * <p>Similar to {@link #sqlToRegexLike}, except that the result is anchored
+   * with {@code ^} and {@code $} so that the entire value must match, as SQL
+   * LIKE requires. The translation is not specific to any dialect; it is used,
+   * for example, by the MongoDB adapter.
+   */
+  public static String sqlToRegexAnchored(
+      String sqlPattern,
+      @Nullable CharSequence escapeStr) {
+    final char escapeChar;
+    if (escapeStr != null) {
+      if (escapeStr.length() != 1) {
+        throw invalidEscapeCharacter(escapeStr.toString());
+      }
+      escapeChar = escapeStr.charAt(0);
+    } else {
+      escapeChar = 0;
+    }
+    return sqlToRegexAnchored(sqlPattern, escapeChar);
+  }
+
+  /**
+   * Translates a SQL LIKE pattern to an anchored regular expression.
+   */
+  public static String sqlToRegexAnchored(
+      String sqlPattern,
+      char escapeChar) {
+    final int len = sqlPattern.length();
+    final StringBuilder javaPattern = new StringBuilder(len + len);
+    javaPattern.append('^');
+    for (int i = 0; i < len; i++) {
+      char c = sqlPattern.charAt(i);
+      if (c == escapeChar) {
+        if (i == (sqlPattern.length() - 1)) {
+          throw invalidEscapeSequence(sqlPattern, i);
+        }
+        char nextChar = sqlPattern.charAt(i + 1);
+        if ((nextChar == '_')
+            || (nextChar == '%')
+            || (nextChar == escapeChar)) {
+          if (JAVA_REGEX_SPECIALS.indexOf(nextChar) >= 0) {
+            javaPattern.append('\\');
+          }
+          javaPattern.append(nextChar);
+          i++;
+        } else {
+          throw invalidEscapeSequence(sqlPattern, i);
+        }
+      } else if (c == '_') {
+        javaPattern.append('.');
+      } else if (c == '%') {
+        javaPattern.append(".*");
+      } else {
+        if (JAVA_REGEX_SPECIALS.indexOf(c) >= 0) {
+          javaPattern.append('\\');
+        }
+        javaPattern.append(c);
+      }
+    }
+    javaPattern.append('$');
+    return javaPattern.toString();
+  }
+
   private static RuntimeException invalidEscapeCharacter(String s) {
     return new RuntimeException(
         "Invalid escape character '" + s + "'");
diff --git a/core/src/test/java/org/apache/calcite/runtime/LikeTest.java 
b/core/src/test/java/org/apache/calcite/runtime/LikeTest.java
new file mode 100644
index 0000000000..549ddc3571
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/runtime/LikeTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.runtime;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/** Unit tests for {@link Like}. */
+class LikeTest {
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7693";>[CALCITE-7693]
+   * Move MongoDB LIKE-to-regex conversion into runtime.Like for 
consistency</a>. */
+  @Test void testSqlToRegexAnchored() {
+    assertThat(Like.sqlToRegexAnchored("", null), is("^$"));
+    assertThat(Like.sqlToRegexAnchored("abc", null), is("^abc$"));
+    assertThat(Like.sqlToRegexAnchored("A%", null), is("^A.*$"));
+    assertThat(Like.sqlToRegexAnchored("A_", null), is("^A.$"));
+    assertThat(Like.sqlToRegexAnchored("%abc%", null), is("^.*abc.*$"));
+    // '.' is an ordinary SQL LIKE character; it must be escaped so that it is
+    // literal in the generated regex.
+    assertThat(Like.sqlToRegexAnchored("A.B%", null), is("^A\\.B.*$"));
+  }
+
+  @Test void testSqlToRegexAnchoredWithEscape() {
+    // '\' escapes the wildcards, making them literal.
+    assertThat(Like.sqlToRegexAnchored("A\\_B\\%C%", "\\"), is("^A_B%C.*$"));
+    assertThat(Like.sqlToRegexAnchored("BROOKLYN\\%", "\\"), 
is("^BROOKLYN%$"));
+    // A custom escape character.
+    assertThat(Like.sqlToRegexAnchored("BROOKLYN!%", "!"), is("^BROOKLYN%$"));
+    // The escape character followed by itself is a literal escape character.
+    assertThat(Like.sqlToRegexAnchored("A\\\\B", "\\"), is("^A\\\\B$"));
+  }
+}
diff --git 
a/mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoFilter.java 
b/mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoFilter.java
index c3d12a84be..93f96d79b0 100644
--- a/mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoFilter.java
+++ b/mongodb/src/main/java/org/apache/calcite/adapter/mongodb/MongoFilter.java
@@ -31,6 +31,7 @@
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.runtime.Like;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.util.JsonBuilder;
@@ -321,8 +322,8 @@ private Void translateLike(RexCall call,
       final RexLiteral patternLiteral = (RexLiteral) right;
       final String sqlPattern = patternLiteral.getValue2().toString();
 
-      final @Nullable Character escapeChar = escapeChar(call);
-      final String finalRegex = sqlLikeToMongoRegex(sqlPattern, escapeChar);
+      final @Nullable String escapeStr = escapeStr(call);
+      final String finalRegex = Like.sqlToRegexAnchored(sqlPattern, escapeStr);
 
       switch (left.getKind()) {
       case INPUT_REF:
@@ -364,8 +365,8 @@ private Void translateNotLike(RexCall call, 
List<Map<String, Object>> orMapList)
       final RexLiteral patternLiteral = (RexLiteral) right;
       final String sqlPattern = patternLiteral.getValue2().toString();
 
-      final @Nullable Character escapeChar = escapeChar(call);
-      final String finalRegex = sqlLikeToMongoRegex(sqlPattern, escapeChar);
+      final @Nullable String escapeStr = escapeStr(call);
+      final String finalRegex = Like.sqlToRegexAnchored(sqlPattern, escapeStr);
 
       final String name;
       switch (left.getKind()) {
@@ -404,8 +405,8 @@ private static RexNode stripCast(RexNode node) {
       return node;
     }
 
-    /** Returns the escape character declared in a LIKE expression, or null. */
-    private static @Nullable Character escapeChar(RexCall call) {
+    /** Returns the escape string declared in a LIKE expression, or null. */
+    private static @Nullable String escapeStr(RexCall call) {
       if (call.operands.size() != 3) {
         return null;
       }
@@ -417,72 +418,7 @@ private static RexNode stripCast(RexNode node) {
       if (escape.length() != 1) {
         throw new AssertionError("cannot translate LIKE with multi-character 
escape: " + call);
       }
-      return escape.charAt(0);
-    }
-
-    /**
-     * Converts SQL LIKE pattern to MongoDB regex pattern.
-     *
-     * <p>SQL: {@code %} matches zero or more characters, {@code _} matches a 
single
-     * character. MongoDB: {@code .*} matches zero or more characters, {@code 
.}
-     * matches a single character.
-     *
-     * <p>We add {@code ^} and {@code $} anchors so that the entire string 
matches
-     * the pattern, just as SQL LIKE does.
-     */
-    private static String sqlLikeToMongoRegex(String sqlPattern, @Nullable 
Character escapeChar) {
-      final StringBuilder regex = new StringBuilder(sqlPattern.length() * 2);
-      regex.append("^");
-      for (int i = 0; i < sqlPattern.length(); i++) {
-        char c = sqlPattern.charAt(i);
-        if (escapeChar != null && c == escapeChar) {
-          if (i == sqlPattern.length() - 1) {
-            throw new AssertionError("Invalid escape sequence at end of LIKE 
pattern: "
-                + sqlPattern);
-          }
-          final char nextChar = sqlPattern.charAt(i + 1);
-          if (nextChar == '%' || nextChar == '_' || nextChar == escapeChar) {
-            regex.append(escapeRegexChar(nextChar));
-            i++;
-          } else {
-            throw new AssertionError("Invalid escape sequence in LIKE pattern: 
" + sqlPattern);
-          }
-        } else if (c == '%') {
-          regex.append(".*");
-        } else if (c == '_') {
-          regex.append('.');
-        } else {
-          regex.append(escapeRegexChar(c));
-        }
-      }
-      regex.append("$");
-      return regex.toString();
-    }
-
-    /**
-     * Escapes a character for use in a MongoDB regex if it's a special regex 
character.
-     */
-    private static String escapeRegexChar(char c) {
-      // MongoDB regex special characters that need escaping
-      switch (c) {
-      case '\\':
-      case '^':
-      case '$':
-      case '.':
-      case '|':
-      case '?':
-      case '*':
-      case '+':
-      case '(':
-      case ')':
-      case '[':
-      case ']':
-      case '{':
-      case '}':
-        return "\\" + c;
-      default:
-        return String.valueOf(c);
-      }
+      return escape;
     }
   }
 }

Reply via email to