Coverage Report

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