Coverage Report

Created: 2026-09-28 21:56

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exprs/function/like.h
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#pragma once
19
20
#include <hs/hs_common.h>
21
#include <hs/hs_runtime.h>
22
#include <re2/re2.h>
23
24
#include <algorithm>
25
#include <boost/iterator/iterator_facade.hpp>
26
#include <boost/regex.hpp>
27
#include <cstddef>
28
#include <cstdint>
29
#include <functional>
30
#include <memory>
31
#include <string>
32
#include <string_view>
33
34
#include "common/status.h"
35
#include "core/block/column_numbers.h"
36
#include "core/column/column_string.h"
37
#include "core/data_type/data_type_number.h"
38
#include "core/data_type/define_primitive_type.h"
39
#include "core/string_ref.h"
40
#include "core/types.h"
41
#include "exprs/aggregate/aggregate_function.h"
42
#include "exprs/function/function.h"
43
#include "exprs/function_context.h"
44
#include "util/string_search.hpp"
45
46
namespace doris {
47
class Block;
48
49
// FastPath types for LIKE pattern matching optimization
50
// This allows per-row pattern analysis to avoid regex when possible
51
enum class LikeFastPath {
52
    ALLPASS,     // Pattern is just '%' or '%%...' - matches everything
53
    EQUALS,      // No wildcards - exact string match
54
    STARTS_WITH, // Pattern ends with '%' only - prefix match
55
    ENDS_WITH,   // Pattern starts with '%' only - suffix match
56
    SUBSTRING,   // Pattern is '%xxx%' - substring search
57
    REGEX        // Contains '_' or multiple '%' - needs regex
58
};
59
60
// Lightweight pattern analysis without RE2
61
// Returns the fast path type and extracts the search string (without wildcards)
62
// Correctly handles escape sequences: backslash-% -> literal %, backslash-_ -> literal _
63
inline LikeFastPath extract_like_fast_path(const char* pattern, size_t len,
64
256
                                           std::string& search_string) {
65
256
    search_string.clear();
66
256
    if (len == 0) {
67
16
        return LikeFastPath::EQUALS;
68
16
    }
69
70
    // Returns true if the character is NOT escaped (even number of preceding backslashes)
71
240
    auto is_unescaped = [&pattern](size_t pos) -> bool {
72
126
        size_t backslash_count = 0;
73
147
        while (pos > 0 && pattern[pos - 1] == '\\') {
74
21
            backslash_count++;
75
21
            pos--;
76
21
        }
77
126
        return (backslash_count % 2 == 0);
78
126
    };
79
80
240
    bool starts_with_percent = (pattern[0] == '%');
81
240
    bool ends_with_percent = (pattern[len - 1] == '%' && is_unescaped(len - 1));
82
83
    // Quick check: if starts or ends with unescaped '_', need regex
84
240
    if (pattern[0] == '_') {
85
19
        return LikeFastPath::REGEX;
86
19
    }
87
221
    if (pattern[len - 1] == '_' && is_unescaped(len - 1)) {
88
28
        return LikeFastPath::REGEX;
89
28
    }
90
91
    // Helper lambda: check if character is a wildcard that needs escaping
92
193
    auto is_wildcard = [](char c) { return c == '%' || c == '_' || c == '\\'; };
93
94
193
    size_t i = 0;
95
    // Skip leading '%' characters (unescaped)
96
255
    while (i < len && pattern[i] == '%') {
97
62
        i++;
98
62
    }
99
    // If pattern is all '%', it's ALLPASS
100
193
    if (i >= len) {
101
6
        return LikeFastPath::ALLPASS;
102
6
    }
103
104
187
    search_string.reserve(len);
105
662
    while (i < len) {
106
605
        char c = pattern[i];
107
        // Escaped character - add the literal
108
605
        if (c == '\\' && i + 1 < len && is_wildcard(pattern[i + 1])) {
109
31
            search_string.push_back(pattern[i + 1]);
110
31
            i += 2;
111
31
            continue;
112
31
        }
113
114
        // Unescaped '_' requires regex
115
574
        if (c == '_') {
116
65
            return LikeFastPath::REGEX;
117
65
        }
118
119
        // Check for trailing '%' or middle '%' (which needs regex)
120
509
        if (c == '%') {
121
            // Check if this is a trailing '%' sequence
122
65
            size_t j = i;
123
130
            while (j < len && pattern[j] == '%') {
124
65
                j++;
125
65
            }
126
65
            if (j >= len) {
127
                // All remaining chars are '%', we're done parsing
128
45
                break;
129
45
            }
130
            // '%' in the middle with more content after - need regex
131
20
            return LikeFastPath::REGEX;
132
65
        }
133
134
444
        search_string.push_back(c);
135
444
        i++;
136
444
    }
137
138
    // Determine the pattern type based on '%' positions
139
102
    if (starts_with_percent && ends_with_percent) {
140
28
        return LikeFastPath::SUBSTRING;
141
74
    } else if (starts_with_percent) {
142
11
        return LikeFastPath::ENDS_WITH;
143
63
    } else if (ends_with_percent) {
144
17
        return LikeFastPath::STARTS_WITH;
145
46
    } else {
146
46
        return LikeFastPath::EQUALS;
147
46
    }
148
102
}
149
150
8
inline std::string replace_pattern_by_escape(const StringRef& pattern, char escape_char) {
151
8
    std::string result;
152
8
    result.reserve(pattern.size);
153
59
    for (size_t i = 0; i < pattern.size; ++i) {
154
51
        if (i + 1 < pattern.size && pattern.data[i] == escape_char &&
155
51
            (pattern.data[i + 1] == escape_char || pattern.data[i + 1] == '%' ||
156
13
             pattern.data[i + 1] == '_')) {
157
            // "^^" -> "^"
158
            // "^%" -> "\%"
159
            // "^_" -> "\_"
160
10
            if ((pattern.data[i + 1] == '%' || pattern.data[i + 1] == '_')) {
161
4
                result.push_back('\\');
162
4
            }
163
10
            result.push_back(pattern.data[i + 1]);
164
10
            ++i; // skip next char
165
41
        } else if (pattern.data[i] == '\\') {
166
            // "\" -> "\\"
167
1
            result.append("\\\\");
168
40
        } else {
169
40
            result.push_back(pattern.data[i]);
170
40
        }
171
51
    }
172
8
    return result;
173
8
}
174
175
// TODO: replace with std::string_view when `LikeSearchState.substring_pattern` can
176
// construct from std::string_view.
177
struct LikeSearchState {
178
    static constexpr char escape_char = '\\';
179
180
    /// Holds the string the StringRef points to and is set any time StringRef is
181
    /// used.
182
    std::string search_string;
183
184
    std::string pattern_str;
185
186
    bool enable_hyperscan_fallback = true;
187
188
    /// Whether a pattern rejected by RE2 may fall back to Boost.Regex, mirrors the
189
    /// `enable_extended_regex` session variable.
190
    bool enable_extended_regex = false;
191
192
    /// Used for LIKE predicates if the pattern is a constant argument, and is either a
193
    /// constant string or has a constant string at the beginning or end of the pattern.
194
    /// This will be set in order to check for that pattern in the corresponding part of
195
    /// the string.
196
    StringRef search_string_sv;
197
198
    /// Used for LIKE predicates if the pattern is a constant argument and has a constant
199
    /// string in the middle of it. This will be use in order to check for the substring
200
    /// in the value.
201
    doris::StringSearch substring_pattern;
202
203
    /// Used for RLIKE and REGEXP predicates if the pattern is a constant argument.
204
    std::unique_ptr<re2::RE2> regex;
205
206
    /// Used for REGEXP predicates when RE2 doesn't support the pattern (e.g., zero-width assertions like `?=`, `?!`, `?<=`, `?<!`)
207
    std::unique_ptr<boost::regex> boost_regex;
208
209
    template <typename Deleter, Deleter deleter>
210
    struct HyperscanDeleter {
211
        template <typename T>
212
352
        void operator()(T* ptr) const {
213
352
            deleter(ptr);
214
352
        }
_ZNK5doris15LikeSearchState16HyperscanDeleterIPFiP10hs_scratchEXadL_Z15hs_free_scratchEEEclIS2_EEvPT_
Line
Count
Source
212
176
        void operator()(T* ptr) const {
213
176
            deleter(ptr);
214
176
        }
_ZNK5doris15LikeSearchState16HyperscanDeleterIPFiP11hs_databaseEXadL_Z16hs_free_databaseEEEclIS2_EEvPT_
Line
Count
Source
212
176
        void operator()(T* ptr) const {
213
176
            deleter(ptr);
214
176
        }
215
    };
216
217
    // hyperscan compiled pattern database and scratch space, reused for performance
218
    std::unique_ptr<hs_database_t, HyperscanDeleter<decltype(&hs_free_database), &hs_free_database>>
219
            hs_database;
220
    std::unique_ptr<hs_scratch_t, HyperscanDeleter<decltype(&hs_free_scratch), &hs_free_scratch>>
221
            hs_scratch;
222
223
    // hyperscan match callback
224
    static int hs_match_handler(unsigned int /* from */,       // NOLINT
225
                                unsigned long long /* from */, // NOLINT
226
                                unsigned long long /* to */,   // NOLINT
227
95
                                unsigned int /* flags */, void* ctx) {
228
        // set result to 1 for matched row
229
95
        *((unsigned char*)ctx) = 1;
230
        /// return non-zero to indicate hyperscan stop after first matched
231
95
        return 1;
232
95
    }
233
234
729
    LikeSearchState() = default;
235
236
    Status clone(LikeSearchState& cloned) const;
237
238
292
    void set_search_string(const std::string& search_string_arg) {
239
292
        search_string = search_string_arg;
240
292
        search_string_sv = StringRef(search_string);
241
292
        substring_pattern.set_pattern(&search_string_sv);
242
292
    }
243
};
244
245
using LikeFn = std::function<doris::Status(const LikeSearchState*, const ColumnString&,
246
                                           const StringRef&, ColumnUInt8::Container&)>;
247
248
using ScalarLikeFn = std::function<doris::Status(const LikeSearchState*, const StringRef&,
249
                                                 const StringRef&, unsigned char*)>;
250
251
using VectorLikeFn = std::function<doris::Status(const ColumnString&, const ColumnString&,
252
                                                 ColumnUInt8::Container&)>;
253
254
struct LikeState {
255
    bool is_like_pattern;
256
    bool has_custom_escape = false;
257
    char escape_char = {};
258
    LikeSearchState search_state;
259
    LikeFn function;
260
    ScalarLikeFn scalar_function;
261
};
262
263
struct VectorPatternSearchState {
264
    MutableColumnPtr _search_strings;
265
    std::string _search_string;
266
    VectorLikeFn _vector_function;
267
    bool _pattern_matched;
268
269
    VectorPatternSearchState(VectorLikeFn vector_function)
270
2.01k
            : _search_strings(ColumnString::create()),
271
2.01k
              _vector_function(vector_function),
272
2.01k
              _pattern_matched(true) {}
273
274
2.01k
    virtual ~VectorPatternSearchState() = default;
275
276
    virtual void like_pattern_match(const std::string& pattern_str) = 0;
277
278
    virtual void regexp_pattern_match(const std::string& pattern_str) = 0;
279
};
280
281
using VPatternSearchStateSPtr = std::shared_ptr<VectorPatternSearchState>;
282
283
class FunctionLikeBase : public IFunction {
284
public:
285
0
    size_t get_number_of_arguments() const override { return 0; }
286
691
    bool is_variadic() const override { return true; }
287
288
689
    DataTypePtr get_return_type_impl(const DataTypes& /*arguments*/) const override {
289
689
        return std::make_shared<DataTypeUInt8>();
290
689
    }
291
292
    Status execute_impl(FunctionContext* context, Block& block, const ColumnNumbers& arguments,
293
                        uint32_t result, size_t /*input_rows_count*/) const override;
294
295
    friend struct VectorAllpassSearchState;
296
    friend struct VectorEqualSearchState;
297
    friend struct VectorSubStringSearchState;
298
    friend struct VectorStartsWithSearchState;
299
    friend struct VectorEndsWithSearchState;
300
301
protected:
302
    static bool should_fallback_to_re2(std::string_view regexp);
303
304
    Status vector_const(const ColumnString& values, const StringRef* pattern_val,
305
                        ColumnUInt8::Container& result, const LikeFn& function,
306
                        LikeSearchState* search_state) const;
307
308
    Status vector_non_const(const ColumnString& values, const ColumnString& patterns,
309
                            ColumnUInt8::Container& result, LikeState* state,
310
                            size_t input_rows_count) const;
311
312
    template <bool LIKE_PATTERN>
313
    static VPatternSearchStateSPtr pattern_type_recognition(const ColumnString& patterns);
314
315
    static Status constant_allpass_fn(const LikeSearchState* state, const ColumnString& val,
316
                                      const StringRef& pattern, ColumnUInt8::Container& result);
317
318
    static Status constant_allpass_fn_scalar(const LikeSearchState* state, const StringRef& val,
319
                                             const StringRef& pattern, unsigned char* result);
320
321
    static Status vector_allpass_fn(const ColumnString& vals, const ColumnString& search_strings,
322
                                    ColumnUInt8::Container& result);
323
324
    static Status constant_starts_with_fn(const LikeSearchState* state, const ColumnString& val,
325
                                          const StringRef& pattern, ColumnUInt8::Container& result);
326
327
    static Status constant_starts_with_fn_scalar(const LikeSearchState* state, const StringRef& val,
328
                                                 const StringRef& pattern, unsigned char* result);
329
330
    static Status vector_starts_with_fn(const ColumnString& vals,
331
                                        const ColumnString& search_strings,
332
                                        ColumnUInt8::Container& result);
333
334
    static Status constant_ends_with_fn(const LikeSearchState* state, const ColumnString& val,
335
                                        const StringRef& pattern, ColumnUInt8::Container& result);
336
337
    static Status constant_ends_with_fn_scalar(const LikeSearchState* state, const StringRef& val,
338
                                               const StringRef& pattern, unsigned char* result);
339
340
    static Status vector_ends_with_fn(const ColumnString& vals, const ColumnString& search_strings,
341
                                      ColumnUInt8::Container& result);
342
343
    static Status constant_equals_fn(const LikeSearchState* state, const ColumnString& val,
344
                                     const StringRef& pattern, ColumnUInt8::Container& result);
345
346
    static Status constant_equals_fn_scalar(const LikeSearchState* state, const StringRef& val,
347
                                            const StringRef& pattern, unsigned char* result);
348
349
    static Status vector_equals_fn(const ColumnString& vals, const ColumnString& search_strings,
350
                                   ColumnUInt8::Container& result);
351
352
    static Status constant_substring_fn(const LikeSearchState* state, const ColumnString& val,
353
                                        const StringRef& pattern, ColumnUInt8::Container& result);
354
355
    static Status constant_substring_fn_scalar(const LikeSearchState* state, const StringRef& val,
356
                                               const StringRef& pattern, unsigned char* result);
357
358
    static Status vector_substring_fn(const ColumnString& vals, const ColumnString& search_strings,
359
                                      ColumnUInt8::Container& result);
360
361
    static Status constant_regex_fn(const LikeSearchState* state, const ColumnString& val,
362
                                    const StringRef& pattern, ColumnUInt8::Container& result);
363
364
    static Status constant_regex_fn_scalar(const LikeSearchState* state, const StringRef& val,
365
                                           const StringRef& pattern, unsigned char* result);
366
367
    static Status regexp_fn(const LikeSearchState* state, const ColumnString& val,
368
                            const StringRef& pattern, ColumnUInt8::Container& result);
369
370
    static Status regexp_fn_scalar(const LikeSearchState* state, const StringRef& val,
371
                                   const StringRef& pattern, unsigned char* result);
372
373
    // Compile `pattern` with RE2. When RE2 rejects it and extended regex is enabled, fall back
374
    // to Boost.Regex, which supports zero-width assertions such as `(?=...)` and `(?<=...)`.
375
    // Exactly one of `regex` / `boost_regex` is set on success.
376
    static Status compile_regex(std::string_view pattern, bool enable_extended_regex,
377
                                std::unique_ptr<re2::RE2>* regex,
378
                                std::unique_ptr<boost::regex>* boost_regex);
379
380
    // Match `val` with whichever engine `compile_regex` produced. Boost.Regex can also fail while
381
    // matching (e.g. the backtracking budget of a pathological pattern is exhausted), which is
382
    // reported as a non-OK Status instead of an exception.
383
    static Status regex_search(const re2::RE2* regex, const boost::regex* boost_regex,
384
                               const StringRef& val, unsigned char* result);
385
386
    // hyperscan compile expression to database and allocate scratch space
387
    static Status hs_prepare(FunctionContext* context, const char* expression,
388
                             hs_database_t** database, hs_scratch_t** scratch);
389
};
390
391
class FunctionLike : public FunctionLikeBase {
392
public:
393
    static constexpr auto name = "like";
394
395
619
    static FunctionPtr create() { return std::make_shared<FunctionLike>(); }
396
397
0
    String get_name() const override { return name; }
398
399
    Status open(FunctionContext* context, FunctionContext::FunctionStateScope scope) override;
400
401
    static Status construct_like_const_state(FunctionContext* ctx, const StringRef& pattern,
402
                                             std::shared_ptr<LikeState>& state,
403
                                             bool try_hyperscan = true);
404
405
    friend struct LikeSearchState;
406
    friend struct VectorAllpassSearchState;
407
    friend struct VectorEqualSearchState;
408
    friend struct VectorSubStringSearchState;
409
    friend struct VectorStartsWithSearchState;
410
    friend struct VectorEndsWithSearchState;
411
412
private:
413
    static Status like_fn(const LikeSearchState* state, const ColumnString& val,
414
                          const StringRef& pattern, ColumnUInt8::Container& result);
415
416
    static Status like_fn_scalar(const LikeSearchState* state, const StringRef& val,
417
                                 const StringRef& pattern, unsigned char* result);
418
419
    static void convert_like_pattern(const LikeSearchState* state, const std::string& pattern,
420
                                     std::string* re_pattern);
421
422
    static void remove_escape_character(std::string* search_string);
423
};
424
425
class FunctionRegexpLike : public FunctionLikeBase {
426
public:
427
    static constexpr auto name = "regexp";
428
    static constexpr auto alias = "rlike";
429
430
74
    static FunctionPtr create() { return std::make_shared<FunctionRegexpLike>(); }
431
432
0
    String get_name() const override { return name; }
433
434
    Status open(FunctionContext* context, FunctionContext::FunctionStateScope scope) override;
435
};
436
437
} // namespace doris