From 2bdf709f2fd2f042437f5077c30a9e74df13b060 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Thu, 6 Aug 2026 14:25:03 +0800 Subject: [PATCH 1/2] [CALCITE-7693] Move MongoDB LIKE-to-regex conversion into runtime.Like for consistency --- .../java/org/apache/calcite/runtime/Like.java | 65 +++++++++++++++ .../org/apache/calcite/runtime/LikeTest.java | 50 ++++++++++++ .../calcite/adapter/mongodb/MongoFilter.java | 80 ++----------------- 3 files changed, 123 insertions(+), 72 deletions(-) create mode 100644 core/src/test/java/org/apache/calcite/runtime/LikeTest.java 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 ac074afa3da7..bfec1234923a 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,71 @@ static String sqlToRegexLike( return javaPattern.toString(); } + /** + * Translates a SQL LIKE pattern to a MongoDB regular expression, with an + * optional escape string. + * + *
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. + */ + public static String sqlToRegexMongo( + 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 sqlToRegexMongo(sqlPattern, escapeChar); + } + + /** + * Translates a SQL LIKE pattern to a MongoDB regular expression. + */ + public static String sqlToRegexMongo( + 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 000000000000..12316c8d18a5 --- /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.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** Unit tests for {@link Like}. */ +class LikeTest { + + /** Test case for + * [CALCITE-7693] + * Move MongoDB LIKE-to-regex conversion into runtime.Like for consistency. */ + @Test void testSqlToRegexMongo() { + assertThat(Like.sqlToRegexMongo("", null), is("^$")); + assertThat(Like.sqlToRegexMongo("abc", null), is("^abc$")); + assertThat(Like.sqlToRegexMongo("A%", null), is("^A.*$")); + assertThat(Like.sqlToRegexMongo("A_", null), is("^A.$")); + assertThat(Like.sqlToRegexMongo("%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.sqlToRegexMongo("A.B%", null), is("^A\\.B.*$")); + } + + @Test void testSqlToRegexMongoWithEscape() { + // '\' escapes the wildcards, making them literal. + assertThat(Like.sqlToRegexMongo("A\\_B\\%C%", "\\"), is("^A_B%C.*$")); + assertThat(Like.sqlToRegexMongo("BROOKLYN\\%", "\\"), is("^BROOKLYN%$")); + // A custom escape character. + assertThat(Like.sqlToRegexMongo("BROOKLYN!%", "!"), is("^BROOKLYN%$")); + // The escape character followed by itself is a literal escape character. + assertThat(Like.sqlToRegexMongo("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 c3d12a84be0b..df0821abafdb 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 escapeChar = escapeChar(call); + final String finalRegex = Like.sqlToRegexMongo(sqlPattern, escapeChar); switch (left.getKind()) { case INPUT_REF: @@ -364,8 +365,8 @@ private Void translateNotLike(RexCall call, List