Coverage Report

Created: 2026-08-16 11:30

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exprs/function/regexps.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
// This file is copied from
18
// https://github.com/ClickHouse/ClickHouse/blob/master/src/Functions/Regexps.h
19
// and modified by Doris
20
21
#pragma once
22
23
#include <hs/hs.h>
24
#include <hs/hs_common.h>
25
26
#include <boost/container_hash/hash.hpp>
27
#include <memory>
28
#include <mutex>
29
#include <optional>
30
#include <string>
31
#include <utility>
32
#include <vector>
33
34
#include "common/exception.h"
35
#include "core/string_ref.h"
36
#include "util/hyperscan_util.h"
37
38
namespace doris::multiregexps {
39
40
template <typename Deleter, Deleter deleter>
41
struct HyperscanDeleter {
42
    template <typename T>
43
34
    void operator()(T* ptr) const {
44
34
        deleter(ptr);
45
34
    }
_ZNK5doris12multiregexps16HyperscanDeleterIPFiP16hs_compile_errorEXadL_Z21hs_free_compile_errorEEEclIS2_EEvPT_
Line
Count
Source
43
1
    void operator()(T* ptr) const {
44
1
        deleter(ptr);
45
1
    }
_ZNK5doris12multiregexps16HyperscanDeleterIPFiP11hs_databaseEXadL_Z16hs_free_databaseEEEclIS2_EEvPT_
Line
Count
Source
43
1
    void operator()(T* ptr) const {
44
1
        deleter(ptr);
45
1
    }
_ZNK5doris12multiregexps16HyperscanDeleterIPFiP10hs_scratchEXadL_Z15hs_free_scratchEEEclIS2_EEvPT_
Line
Count
Source
43
32
    void operator()(T* ptr) const {
44
32
        deleter(ptr);
45
32
    }
46
};
47
48
/// Helper unique pointers to correctly delete the allocated space when hyperscan cannot compile something and we throw an exception.
49
using CompilerError =
50
        std::unique_ptr<hs_compile_error_t,
51
                        HyperscanDeleter<decltype(&hs_free_compile_error), &hs_free_compile_error>>;
52
using ScratchPtr = std::unique_ptr<hs_scratch_t,
53
                                   HyperscanDeleter<decltype(&hs_free_scratch), &hs_free_scratch>>;
54
using DataBasePtr =
55
        std::unique_ptr<hs_database_t,
56
                        HyperscanDeleter<decltype(&hs_free_database), &hs_free_database>>;
57
58
/// Database is thread safe across multiple threads and Scratch is not but we can copy it whenever we use it in the searcher.
59
class Regexps {
60
public:
61
28
    Regexps(hs_database_t* db_, hs_scratch_t* scratch_) : db {db_}, scratch {scratch_} {}
62
63
36
    hs_database_t* getDB() const { return db.get(); }
64
31
    hs_scratch_t* getScratch() const { return scratch.get(); }
65
66
private:
67
    DataBasePtr db;
68
    ScratchPtr scratch;
69
};
70
71
class DeferredConstructedRegexps {
72
public:
73
    explicit DeferredConstructedRegexps(std::function<Regexps()> constructor_)
74
29
            : constructor(std::move(constructor_)) {}
75
76
32
    Regexps* get() {
77
32
        std::lock_guard lock(mutex);
78
32
        if (regexps) {
79
3
            return &*regexps;
80
3
        }
81
29
        regexps = constructor();
82
29
        return &*regexps;
83
32
    }
84
85
private:
86
    std::mutex mutex;
87
    std::function<Regexps()> constructor;
88
    std::optional<Regexps> regexps;
89
};
90
91
using DeferredConstructedRegexpsPtr = std::shared_ptr<DeferredConstructedRegexps>;
92
93
template <bool save_indices, bool WithEditDistance>
94
Regexps constructRegexps(const std::vector<String>& str_patterns,
95
30
                         [[maybe_unused]] std::optional<UInt32> edit_distance) {
96
    /// Common pointers
97
30
    std::vector<const char*> patterns;
98
30
    std::vector<unsigned int> flags;
99
100
    /// Pointer for external edit distance compilation
101
30
    std::vector<hs_expr_ext> ext_exprs;
102
30
    std::vector<const hs_expr_ext*> ext_exprs_ptrs;
103
104
30
    patterns.reserve(str_patterns.size());
105
30
    flags.reserve(str_patterns.size());
106
107
    if constexpr (WithEditDistance) {
108
        ext_exprs.reserve(str_patterns.size());
109
        ext_exprs_ptrs.reserve(str_patterns.size());
110
    }
111
112
335
    for (const auto& ref : str_patterns) {
113
335
        patterns.push_back(ref.data());
114
        /* Flags below are the pattern matching flags.
115
         * HS_FLAG_DOTALL is a compile flag where matching a . will not exclude newlines. This is a good
116
         * performance practice according to Hyperscan API. https://intel.github.io/hyperscan/dev-reference/performance.html#dot-all-mode
117
         * HS_FLAG_ALLOWEMPTY is a compile flag where empty strings are allowed to match.
118
         * HS_FLAG_UTF8 is a flag where UTF8 literals are matched.
119
         * HS_FLAG_SINGLEMATCH is a compile flag where each pattern match will be returned only once. it is a good performance practice
120
         * as it is said in the Hyperscan documentation. https://intel.github.io/hyperscan/dev-reference/performance.html#single-match-flag
121
         */
122
335
        flags.push_back(HS_FLAG_DOTALL | HS_FLAG_SINGLEMATCH | HS_FLAG_ALLOWEMPTY | HS_FLAG_UTF8);
123
        if constexpr (WithEditDistance) {
124
            /// Hyperscan currently does not support UTF8 matching with edit distance.
125
            flags.back() &= ~HS_FLAG_UTF8;
126
            ext_exprs.emplace_back();
127
            /// HS_EXT_FLAG_EDIT_DISTANCE is a compile flag responsible for Levenstein distance.
128
            ext_exprs.back().flags = HS_EXT_FLAG_EDIT_DISTANCE;
129
            ext_exprs.back().edit_distance = edit_distance.value();
130
            ext_exprs_ptrs.push_back(&ext_exprs.back());
131
        }
132
335
    }
133
30
    hs_database_t* db = nullptr;
134
30
    hs_compile_error_t* compile_error = nullptr;
135
136
30
    std::unique_ptr<unsigned int[]> ids;
137
138
    /// We mark the patterns to provide the callback results.
139
    if constexpr (save_indices) {
140
        ids.reset(new unsigned int[patterns.size()]);
141
        for (size_t i = 0; i < patterns.size(); ++i) {
142
            ids[i] = static_cast<unsigned>(i + 1);
143
        }
144
    }
145
146
335
    for (auto& pattern : patterns) {
147
335
        LOG(INFO) << "pattern: " << pattern << "\n";
148
335
        if (is_hyperscan_regexp_expensive(pattern)) {
149
1
            throw doris::Exception(Status::InvalidArgument(HYPERSCAN_BOUNDED_REPEAT_ERROR));
150
1
        }
151
335
    }
152
153
29
    hs_error_t err;
154
29
    if constexpr (!WithEditDistance) {
155
29
        err = hs_compile_multi(patterns.data(), flags.data(), ids.get(),
156
29
                               static_cast<unsigned>(patterns.size()), HS_MODE_BLOCK, nullptr, &db,
157
29
                               &compile_error);
158
    } else {
159
        err = hs_compile_ext_multi(patterns.data(), flags.data(), ids.get(), ext_exprs_ptrs.data(),
160
                                   static_cast<unsigned>(patterns.size()), HS_MODE_BLOCK, nullptr,
161
                                   &db, &compile_error);
162
    }
163
164
29
    if (err != HS_SUCCESS) [[unlikely]] {
165
        /// CompilerError is a unique_ptr, so correct memory free after the exception is thrown.
166
1
        CompilerError error(compile_error);
167
168
1
        if (error->expression < 0) { // error has nothing to do with the patterns themselves
169
0
            throw doris::Exception(Status::InternalError("Compile regexp expression failed. got {}",
170
0
                                                         error->message));
171
1
        } else {
172
1
            throw doris::Exception(Status::InvalidArgument(
173
1
                    "Compile regexp expression failed. got {}. some expressions may be illegal",
174
1
                    error->message));
175
1
        }
176
1
    }
177
178
    /// We allocate the scratch space only once, then copy it across multiple threads with hs_clone_scratch
179
    /// function which is faster than allocating scratch space each time in each thread.
180
28
    hs_scratch_t* scratch = nullptr;
181
28
    err = hs_alloc_scratch(db, &scratch);
182
183
28
    if (err != HS_SUCCESS) [[unlikely]] {
184
0
        if (err == HS_NOMEM) [[unlikely]] {
185
0
            throw doris::Exception(Status::MemoryAllocFailed(
186
0
                    "Allocating memory failed on compiling regexp expressions."));
187
0
        } else {
188
0
            throw doris::Exception(Status::InvalidArgument(
189
0
                    "Compile regexp expression failed with unexpected arguments perhaps"));
190
0
        }
191
0
    }
192
193
28
    return {db, scratch};
194
28
}
195
196
/// Maps string pattern vectors + edit distance to compiled vectorscan regexps. Uses the same eviction mechanism as the LocalCacheTable for
197
/// re2 patterns. Because vectorscan regexes are overall more heavy-weight (more expensive compilation, regexes can grow up to multiple
198
/// MBs, usage of scratch space), 1. GlobalCacheTable is a global singleton and, as a result, needs locking 2. the pattern compilation is
199
/// done outside GlobalCacheTable's lock, at the cost of another level of locking.
200
struct GlobalCacheTable {
201
    constexpr static size_t CACHE_SIZE = 500; /// collision probability
202
203
    struct Bucket {
204
        std::vector<String> patterns;        /// key
205
        std::optional<UInt32> edit_distance; /// key
206
        /// The compiled patterns and their state (vectorscan 'database' + scratch space) are wrapped in a shared_ptr. Refcounting guarantees
207
        /// that eviction of a pattern does not affect parallel threads still using the pattern.
208
        DeferredConstructedRegexpsPtr regexps; /// value
209
    };
210
211
    std::mutex mutex;
212
    std::array<Bucket, CACHE_SIZE> known_regexps;
213
214
    static size_t getBucketIndexFor(const std::vector<String> patterns,
215
32
                                    std::optional<UInt32> edit_distance) {
216
32
        size_t hash = 0;
217
348
        for (const auto& pattern : patterns) {
218
348
            boost::hash_combine(hash, pattern);
219
348
        }
220
32
        boost::hash_combine(hash, edit_distance);
221
32
        return hash % CACHE_SIZE;
222
32
    }
223
};
224
225
/// If WithEditDistance is False, edit_distance must be nullopt. Also, we use templates here because each instantiation of function template
226
/// has its own copy of local static variables which must not be the same for different hyperscan compilations.
227
template <bool save_indices, bool WithEditDistance>
228
DeferredConstructedRegexpsPtr getOrSet(const std::vector<StringRef>& patterns,
229
32
                                       std::optional<UInt32> edit_distance) {
230
32
    static GlobalCacheTable
231
32
            pool; /// Different variables for different pattern parameters, thread-safe in C++11
232
233
32
    std::vector<String> str_patterns;
234
32
    str_patterns.reserve(patterns.size());
235
348
    for (const auto& pattern : patterns) {
236
348
        str_patterns.emplace_back(pattern.to_string());
237
348
    }
238
239
32
    size_t bucket_idx = GlobalCacheTable::getBucketIndexFor(str_patterns, edit_distance);
240
241
    /// Lock cache to find compiled regexp for given pattern vector + edit distance.
242
32
    std::lock_guard lock(pool.mutex);
243
244
32
    GlobalCacheTable::Bucket& bucket = pool.known_regexps[bucket_idx];
245
246
    /// Pattern compilation is expensive and we don't want to block other threads reading from / inserting into the cache while we hold the
247
    /// cache lock during pattern compilation. Therefore, when a cache entry is created or replaced, only set the regexp constructor method
248
    /// and compile outside the cache lock.
249
    /// Note that the string patterns and the edit distance is passed into the constructor lambda by value, i.e. copied - it is not an
250
    /// option to reference the corresponding string patterns / edit distance key in the cache table bucket because the cache entry may
251
    /// already be evicted at the time the compilation starts.
252
253
32
    if (bucket.regexps == nullptr) {
254
        /// insert new entry
255
28
        auto deferred_constructed_regexps =
256
28
                std::make_shared<DeferredConstructedRegexps>([str_patterns, edit_distance]() {
257
28
                    return constructRegexps<save_indices, WithEditDistance>(str_patterns,
258
28
                                                                            edit_distance);
259
28
                });
260
28
        bucket = {std::move(str_patterns), edit_distance, deferred_constructed_regexps};
261
28
    } else if (bucket.patterns != str_patterns || bucket.edit_distance != edit_distance) {
262
        /// replace existing entry
263
1
        auto deferred_constructed_regexps =
264
1
                std::make_shared<DeferredConstructedRegexps>([str_patterns, edit_distance]() {
265
1
                    return constructRegexps<save_indices, WithEditDistance>(str_patterns,
266
1
                                                                            edit_distance);
267
1
                });
268
1
        bucket = {std::move(str_patterns), edit_distance, deferred_constructed_regexps};
269
1
    }
270
271
32
    return bucket.regexps;
272
32
}
273
274
} // namespace doris::multiregexps