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

yiguolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new d24fafc0b34 [fix](like) Fix LIKE matching values with extra characters 
at the end (#68133)
d24fafc0b34 is described below

commit d24fafc0b343bf7a1b23c5d1addbb937e032c8fd
Author: Chenyang Sun <[email protected]>
AuthorDate: Fri Sep 18 13:48:42 2026 +0800

    [fix](like) Fix LIKE matching values with extra characters at the end 
(#68133)
    
    ### What problem does this PR solve?
    
    1. `LIKE 'a_b'` is converted to `^a.b\z`, not `^a.b$`. Hyperscan follows
    PCRE, where `$` also matches right before a newline that ends the value,
    so `^a.b$` wrongly matched 'acb\n'. `\z` is the end of the value in both
    Hyperscan and RE2.
    
    2. `LIKE 'a_b%'` is converted to `^a.b`, not `^a.b.*\z`. A trailing `%`
    matches anything, and so does appending nothing. The anchored form would
    force `.*` to consume the value to its end, and `.*` only matches valid
    UTF-8, so RE2 would not match 'acb\xff' while Hyperscan still would.
    
    3. `LIKE 'a_*'` is converted to `^a.\*\z`, not `^a.\*`. The tail anchor
    used to be skipped whenever the produced regex ended with `*`, a check
    meant to detect the `.*` expanded from a trailing `%`. An escaped
    literal `*` ends the regex with the same character, so the pattern lost
    its anchor entirely and `'ab*xyz'` matched. The decision now comes from
    the position in the pattern instead of from the produced string.
    
    
    Issue Number: close #xxx
    
    Related PR: #xxx
    
    Problem Summary:
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [x] Regression test
        - [x] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 be/src/exprs/function/like.cpp                     | 13 ++--
 be/test/exprs/function/function_like_test.cpp      | 74 ++++++++++++++++++++
 .../test_like_whole_string_match.out               | 74 ++++++++++++++++++++
 .../test_like_whole_string_match.groovy            | 81 ++++++++++++++++++++++
 4 files changed, 237 insertions(+), 5 deletions(-)

diff --git a/be/src/exprs/function/like.cpp b/be/src/exprs/function/like.cpp
index 938ecfb958f..a6403f5684c 100644
--- a/be/src/exprs/function/like.cpp
+++ b/be/src/exprs/function/like.cpp
@@ -781,7 +781,8 @@ void FunctionLike::convert_like_pattern(const 
LikeSearchState* state, const std:
     re_pattern->clear();
 
     if (pattern.empty()) {
-        re_pattern->append("^$");
+        // `\z` is the end of the value in both Hyperscan and RE2
+        re_pattern->append("^\\z");
         return;
     }
 
@@ -809,6 +810,10 @@ void FunctionLike::convert_like_pattern(const 
LikeSearchState* state, const std:
         }
 
         if (c == '%') {
+            if (i + 1 == pattern.size()) {
+                // a trailing `%` matches anything, and so does appending 
nothing
+                return;
+            }
             re_pattern->append(".*");
         } else if (c == '_') {
             re_pattern->append(".");
@@ -823,10 +828,8 @@ void FunctionLike::convert_like_pattern(const 
LikeSearchState* state, const std:
         }
     }
 
-    // add $ to pattern tail to match line tail
-    if (!pattern.empty() && re_pattern->back() != '*') {
-        re_pattern->append("$");
-    }
+    // `\z` is the end of the value in both Hyperscan and RE2
+    re_pattern->append("\\z");
 }
 
 void FunctionLike::remove_escape_character(std::string* search_string) {
diff --git a/be/test/exprs/function/function_like_test.cpp 
b/be/test/exprs/function/function_like_test.cpp
index 62384b3f82b..a6887222251 100644
--- a/be/test/exprs/function/function_like_test.cpp
+++ b/be/test/exprs/function/function_like_test.cpp
@@ -144,6 +144,80 @@ TEST(FunctionLikeTest, like) {
             func_name, const_pattern_input_types, data_set));
 }
 
+TEST(FunctionLikeTest, like_matches_whole_value) {
+    std::string func_name = "like";
+
+    DataSet data_set = {
+            // A trailing newline belongs to the value, so a pattern that is 
anchored at the
+            // tail must not match across it.
+            {{std::string("acb"), std::string("a_b")}, uint8_t(1)},
+            {{std::string("acb\n"), std::string("a_b")}, uint8_t(0)},
+            {{std::string("acb\r\n"), std::string("a_b")}, uint8_t(0)},
+            {{std::string("acb\n\n"), std::string("a_b")}, uint8_t(0)},
+            {{std::string("acbx"), std::string("a_b")}, uint8_t(0)},
+            {{std::string("\nacb"), std::string("a_b")}, uint8_t(0)},
+            {{std::string("abc"), std::string("a%c")}, uint8_t(1)},
+            {{std::string("abc\n"), std::string("a%c")}, uint8_t(0)},
+            {{std::string("abc\n"), std::string("%b%c")}, uint8_t(0)},
+            // The newline is an ordinary character for '_' and '%'.
+            {{std::string("a\nb"), std::string("a_b")}, uint8_t(1)},
+            {{std::string("a\nb"), std::string("a%b")}, uint8_t(1)},
+            {{std::string("acb\n"), std::string("a_b_")}, uint8_t(1)},
+            {{std::string("acb\n"), std::string("a_b%")}, uint8_t(1)},
+            {{std::string("abc\n"), std::string("a_c%")}, uint8_t(1)},
+            // '_' stands for one character, not for one byte.
+            {{std::string("a中b"), std::string("a_b")}, uint8_t(1)},
+            {{std::string("a中b\n"), std::string("a_b")}, uint8_t(0)},
+            // An empty pattern only matches an empty value.
+            {{std::string(""), std::string("")}, uint8_t(1)},
+            {{std::string("\n"), std::string("")}, uint8_t(0)},
+            // The shortcut paths and the regex path must agree on the same 
value.
+            {{std::string("acb\n"), std::string("acb")}, uint8_t(0)},
+            {{std::string("abc\n"), std::string("%c")}, uint8_t(0)},
+            {{std::string("abc\n"), std::string("a%")}, uint8_t(1)},
+
+            // A literal '*' at the tail of the pattern is not the '.*' 
expanded from a
+            // trailing '%', so the pattern stays anchored.
+            {{std::string("ab*"), std::string("a_*")}, uint8_t(1)},
+            {{std::string("ab*xyz"), std::string("a_*")}, uint8_t(0)},
+            {{std::string("ab*\n"), std::string("a_*")}, uint8_t(0)},
+            {{std::string("ab%"), std::string("a_\\%")}, uint8_t(1)},
+            {{std::string("ab%xyz"), std::string("a_\\%")}, uint8_t(0)},
+    };
+
+    InputTypeSet const_pattern_input_types = {PrimitiveType::TYPE_VARCHAR,
+                                              PrimitiveType::TYPE_VARCHAR};
+    check_function_all_arg_comb<DataTypeUInt8, true>(func_name, 
const_pattern_input_types,
+                                                     data_set);
+}
+
+TEST(FunctionLikeTest, convert_like_pattern_shapes) {
+    auto convert = [](const std::string& pattern) {
+        std::string re_pattern;
+        FunctionLike::convert_like_pattern(nullptr, pattern, &re_pattern);
+        return re_pattern;
+    };
+
+    // The tail anchor is `\z`, never `$`: Hyperscan reads `$` the PCRE way 
and would also
+    // match right before a newline that ends the value.
+    EXPECT_EQ(convert(""), "^\\z");
+    EXPECT_EQ(convert("a_b"), "^a.b\\z");
+    EXPECT_EQ(convert("%c%b"), ".*c.*b\\z");
+
+    // A trailing `%` is the only shape left open at the tail, and it needs no 
`.*` either.
+    EXPECT_EQ(convert("abc%"), "^abc");
+    EXPECT_EQ(convert("a_b%%"), "^a.b.*");
+    EXPECT_EQ(convert("%"), "");
+
+    // An escaped literal `*` ends the produced regex with '*' without being a 
wildcard, and an
+    // escaped `%` is a literal: both stay anchored.
+    EXPECT_EQ(convert("a_*"), "^a.\\*\\z");
+    EXPECT_EQ(convert("a_\\%"), "^a.%\\z");
+
+    // A backslash that does not open a LIKE escape is a literal backslash.
+    EXPECT_EQ(convert("a_\\"), "^a.\\\\\\z");
+}
+
 TEST(FunctionLikeTest, regexp) {
     std::string func_name = "regexp";
 
diff --git 
a/regression-test/data/query_p0/sql_functions/string_functions/test_like_whole_string_match.out
 
b/regression-test/data/query_p0/sql_functions/string_functions/test_like_whole_string_match.out
new file mode 100644
index 00000000000..4dbfc4fbc89
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/string_functions/test_like_whole_string_match.out
@@ -0,0 +1,74 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !anchor_underscore --
+1      3       true
+2      4       false
+3      5       false
+4      3       true
+5      4       false
+6      3       false
+7      4       false
+8      6       false
+
+-- !anchor_leading_percent --
+1      3       true
+2      4       false
+3      5       false
+4      3       false
+5      4       false
+6      3       false
+7      4       false
+8      6       false
+
+-- !literal_star_tail --
+1      3       false
+2      4       false
+3      5       false
+4      3       false
+5      4       false
+6      3       true
+7      4       false
+8      6       false
+
+-- !trailing_percent --
+1      3       true
+2      4       true
+3      5       true
+4      3       true
+5      4       true
+6      3       false
+7      4       false
+8      6       false
+
+-- !equals_shortcut --
+1      3       true
+2      4       false
+3      5       false
+4      3       false
+5      4       false
+6      3       false
+7      4       false
+8      6       false
+
+-- !pushdown_underscore --
+1
+4
+
+-- !pushdown_literal_star --
+6
+
+-- !not_literal_star --
+1
+2
+3
+4
+5
+7
+8
+
+-- !pushdown_trailing_percent --
+1
+2
+3
+4
+5
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/string_functions/test_like_whole_string_match.groovy
 
b/regression-test/suites/query_p0/sql_functions/string_functions/test_like_whole_string_match.groovy
new file mode 100644
index 00000000000..89a960e09fb
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/string_functions/test_like_whole_string_match.groovy
@@ -0,0 +1,81 @@
+// 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.
+
+suite("test_like_whole_string_match") {
+    sql "drop table if exists test_like_whole_string_match"
+    sql """
+        create table test_like_whole_string_match (
+            id int,
+            s varchar(64)
+        ) duplicate key(id)
+        distributed by hash(id) buckets 1
+        properties("replication_num" = "1");
+    """
+    sql """
+        insert into test_like_whole_string_match values
+            (1, 'acb'),
+            (2, concat('acb', char(10))),
+            (3, concat('acb', char(13), char(10))),
+            (4, concat('a', char(10), 'b')),
+            (5, 'acbx'),
+            (6, 'ab*'),
+            (7, concat('ab*', char(10))),
+            (8, 'ab*xyz');
+    """
+
+    // a value that ends with a newline is one character longer, so it must 
not match a
+    // pattern that is anchored at the tail
+    qt_anchor_underscore """
+        select id, length(s) as len, s like 'a_b' as r from 
test_like_whole_string_match order by id
+    """
+    qt_anchor_leading_percent """
+        select id, length(s) as len, s like '%c%b' as r from 
test_like_whole_string_match order by id
+    """
+    // a literal '*' at the end of the pattern does not make it end with a 
wildcard
+    qt_literal_star_tail """
+        select id, length(s) as len, s like 'a_*' as r from 
test_like_whole_string_match order by id
+    """
+    // a pattern that really ends with '%' still accepts anything, the newline 
included
+    qt_trailing_percent """
+        select id, length(s) as len, s like 'a_b%' as r from 
test_like_whole_string_match order by id
+    """
+
+    // a pattern without wildcards is rewritten to an equality by 
LIKE_TO_EQUAL, which would
+    // make this block pass with LIKE broken; disable the rule so the shortcut 
path really runs
+    sql "set disable_nereids_expression_rules='LIKE_TO_EQUAL'"
+    qt_equals_shortcut """
+        select id, length(s) as len, s like 'acb' as r from 
test_like_whole_string_match order by id
+    """
+
+    // a LIKE in a filter with function pushdown enabled is evaluated by 
LikeColumnPredicate,
+    // which converts the pattern a second time through 
LikeSearchState::clone()
+    sql "set enable_function_pushdown = true"
+    qt_pushdown_underscore """
+        select id from test_like_whole_string_match where s like 'a_b' order 
by id
+    """
+    qt_pushdown_literal_star """
+        select id from test_like_whole_string_match where s like 'a_*' order 
by id
+    """
+    // NOT LIKE reaches the scan as CompoundPredicate(NOT, LIKE), whose 
children are not a slot,
+    // so it stays a generic expression rather than a LikeColumnPredicate
+    qt_not_literal_star """
+        select id from test_like_whole_string_match where s not like 'a_*' 
order by id
+    """
+    qt_pushdown_trailing_percent """
+        select id from test_like_whole_string_match where s like 'a_b%' order 
by id
+    """
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to