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;
}
}
}