github-actions[bot] commented on code in PR #66788:
URL: https://github.com/apache/doris/pull/66788#discussion_r3785419760


##########
be/src/exprs/function/like.cpp:
##########
@@ -36,6 +37,109 @@
 #include "exprs/function/simple_function_factory.h"
 
 namespace doris {
+namespace {
+
+bool is_larger_than_fifty(std::string_view str) {
+    int number = 0;
+    auto [_, error] = std::from_chars(str.data(), str.data() + str.size(), 
number);
+    return error == std::errc() && number > 50;
+}
+
+std::string mask_escaped_characters_and_character_classes(std::string_view 
regexp) {
+    std::string masked_regexp(regexp);
+    bool escaped = false;
+    bool in_character_class = false;
+    bool character_class_can_close = false;
+    for (char& masked_character : masked_regexp) {
+        const char current = masked_character;
+        if (escaped) {
+            masked_character = ' ';
+            escaped = false;
+            if (in_character_class) {
+                character_class_can_close = true;
+            }
+            continue;
+        }
+        if (current == '\\') {
+            masked_character = ' ';
+            escaped = true;
+            continue;
+        }
+        if (in_character_class) {
+            masked_character = ' ';
+            if (current == ']' && character_class_can_close) {
+                in_character_class = false;
+            } else if (current != '^' || character_class_can_close) {
+                character_class_can_close = true;
+            }
+            continue;
+        }
+        if (current == '[') {
+            masked_character = ' ';
+            in_character_class = true;
+            character_class_can_close = false;
+        }
+    }
+    return masked_regexp;
+}
+
+/// Bounded repetitions can expand Hyperscan's compiler graph and make 
compilation extremely
+/// expensive. This checker is adapted from ClickHouse's 
`SlowWithHyperscanChecker`.
+class SlowWithHyperscanChecker {
+public:
+    SlowWithHyperscanChecker()
+            : _searcher_one_repeat(R"(\{\s*([\d]+)\s*,?\s*})"),
+              _searcher_two_repeats(R"(\{\s*([\d]+)\s*,\s*([\d]+)\s*\})") {}
+
+    bool is_slow(std::string_view regexp) const {
+        const std::string masked_regexp = 
mask_escaped_characters_and_character_classes(regexp);

Review Comment:
   [P1] Restrict this check to actual Hyperscan repeats
   
   The lexical mask still leaves braces from valid non-repeat syntax visible. 
For example, [Hyperscan supports `(?# 
comment)`](https://intel.github.io/hyperscan/dev-reference/compilation.html#pattern-support),
 so `(?# note{51})a` previously compiled, but this code sees `{51}`, skips 
Hyperscan, and the default RE2 fallback fails because [RE2 does not support 
that comment form](https://github.com/google/re2/blob/main/doc/syntax.txt). 
Braced escapes such as `\x{51}`, quoted literals, POSIX classes, and 
whitespace-bearing literal braces are misclassified similarly. Please follow 
Hyperscan's lexical contexts and add default/strict tests for supported 
non-repeat syntax.



##########
be/src/exprs/function/like.cpp:
##########
@@ -487,8 +596,21 @@ Status FunctionLikeBase::regexp_fn(const LikeSearchState* 
state, const ColumnStr
 }
 
 // hyperscan compile expression to database and allocate scratch space
+bool FunctionLikeBase::should_fallback_to_re2(std::string_view regexp) {
+    static const SlowWithHyperscanChecker slow_with_hyperscan_checker;
+    return slow_with_hyperscan_checker.is_slow(regexp);
+}
+
 Status FunctionLikeBase::hs_prepare(FunctionContext* context, const char* 
expression,
                                     hs_database_t** database, hs_scratch_t** 
scratch) {
+    if (should_fallback_to_re2(expression)) {

Review Comment:
   [P1] Do not route Hyperscan patterns to an incapable fallback
   
   A real pattern such as `a{1001}` is supported by Hyperscan, but RE2 rejects 
counting forms above 1000; even `\h{51}` stays under that limit yet uses a 
Hyperscan-supported atom RE2 does not understand. This early return therefore 
sends valid patterns to an engine that cannot represent them. Constant-open 
errors with the default `enable_extended_regex=false` (Boost may recover only 
when separately enabled), and the execute-time `ColumnConst` path has no Boost 
fallback. Preserve a safe engine/error contract for every intercepted Hyperscan 
pattern and test both the count ceiling and Hyperscan-only atoms.



##########
fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java:
##########
@@ -5594,6 +5600,7 @@ public TQueryOptions toThrift() {
         
tResult.setAnnIndexCandidateRowsPercentThreshold(annIndexCandidateRowsPercentThreshold);
         tResult.setMergeReadSliceSize(mergeReadSliceSizeBytes);
         tResult.setEnableExtendedRegex(enableExtendedRegex);
+        tResult.setEnableHyperscanFallback(enableHyperscanFallback);

Review Comment:
   [P2] Forward this policy to BE constant folding
   
   Normal execution gets the value here, but `FoldConstantRuleOnBE.evalOnBE()` 
constructs a fresh `TQueryOptions` and never copies `enableHyperscanFallback`. 
With `enable_fold_constant_by_be=true` and this setting false, a constant 
expression such as `'a' REGEXP 'a{51}'` is evaluated with the thrift default 
true and folded to a boolean instead of returning the requested strict error. 
Populate the fold RPC from the current session (or explicitly set this field) 
and cover this alternate sender in a test.



##########
be/src/exprs/function/like.cpp:
##########
@@ -36,6 +37,109 @@
 #include "exprs/function/simple_function_factory.h"
 
 namespace doris {
+namespace {
+
+bool is_larger_than_fifty(std::string_view str) {
+    int number = 0;
+    auto [_, error] = std::from_chars(str.data(), str.data() + str.size(), 
number);
+    return error == std::errc() && number > 50;
+}
+
+std::string mask_escaped_characters_and_character_classes(std::string_view 
regexp) {
+    std::string masked_regexp(regexp);
+    bool escaped = false;
+    bool in_character_class = false;
+    bool character_class_can_close = false;
+    for (char& masked_character : masked_regexp) {
+        const char current = masked_character;
+        if (escaped) {
+            masked_character = ' ';
+            escaped = false;
+            if (in_character_class) {
+                character_class_can_close = true;
+            }
+            continue;
+        }
+        if (current == '\\') {
+            masked_character = ' ';
+            escaped = true;
+            continue;
+        }
+        if (in_character_class) {
+            masked_character = ' ';
+            if (current == ']' && character_class_can_close) {
+                in_character_class = false;
+            } else if (current != '^' || character_class_can_close) {

Review Comment:
   [P1] Treat the second leading caret as class content
   
   In `[^^](ab?c?d){1000,5000}`, the first `^` negates the class but the second 
is ordinary class content. This branch leaves `character_class_can_close` false 
for both, so the real `]` does not close the class and the masker blanks the 
subsequent genuine large repeat. `should_fallback_to_re2()` then returns false 
and the expensive pattern still reaches `hs_compile()`. Track only the first 
caret as special and add a regression for a large repeat following `[^^]`.



##########
gensrc/thrift/PaloInternalService.thrift:
##########
@@ -516,6 +516,8 @@ struct TQueryOptions {
   229: optional i32 coordinator_thrift_max_message_size;
   // FE can explicitly and idempotently acknowledge external-file commit 
reports.
   230: optional bool supports_external_file_report_ack = false;
+  // Fall back to RE2 when Hyperscan cannot compile a regular expression.
+  231: optional bool enable_hyperscan_fallback = true;

Review Comment:
   [P2] Preserve this option when ALTER jobs are replayed
   
   Synchronous-MV jobs capture `SessionVariable.toThrift()`, but `AlterJobV2` 
journals the nested `TQueryOptions` through metadata Gson. Its annotation-only 
strategy drops generated Thrift fields, so an FE restart after the durable 
PENDING record and before task dispatch reconstructs this option with the IDL 
default `true`. A job created with fallback disabled and a WHERE predicate such 
as `value_col REGEXP 'a{51}'` can therefore resume using RE2 instead of 
returning the captured strict error. Persist the required execution options 
through a Thrift-aware representation and cover replay before task dispatch.



##########
be/src/exprs/function/like.cpp:
##########
@@ -36,6 +37,109 @@
 #include "exprs/function/simple_function_factory.h"
 
 namespace doris {
+namespace {
+
+bool is_larger_than_fifty(std::string_view str) {
+    int number = 0;
+    auto [_, error] = std::from_chars(str.data(), str.data() + str.size(), 
number);
+    return error == std::errc() && number > 50;
+}
+
+std::string mask_escaped_characters_and_character_classes(std::string_view 
regexp) {
+    std::string masked_regexp(regexp);
+    bool escaped = false;
+    bool in_character_class = false;
+    bool character_class_can_close = false;
+    for (char& masked_character : masked_regexp) {
+        const char current = masked_character;
+        if (escaped) {
+            masked_character = ' ';
+            escaped = false;
+            if (in_character_class) {
+                character_class_can_close = true;
+            }
+            continue;
+        }
+        if (current == '\\') {
+            masked_character = ' ';
+            escaped = true;
+            continue;
+        }
+        if (in_character_class) {
+            masked_character = ' ';
+            if (current == ']' && character_class_can_close) {
+                in_character_class = false;
+            } else if (current != '^' || character_class_can_close) {
+                character_class_can_close = true;
+            }
+            continue;
+        }
+        if (current == '[') {
+            masked_character = ' ';
+            in_character_class = true;
+            character_class_can_close = false;
+        }
+    }
+    return masked_regexp;
+}
+
+/// Bounded repetitions can expand Hyperscan's compiler graph and make 
compilation extremely
+/// expensive. This checker is adapted from ClickHouse's 
`SlowWithHyperscanChecker`.
+class SlowWithHyperscanChecker {
+public:
+    SlowWithHyperscanChecker()
+            : _searcher_one_repeat(R"(\{\s*([\d]+)\s*,?\s*})"),
+              _searcher_two_repeats(R"(\{\s*([\d]+)\s*,\s*([\d]+)\s*\})") {}
+
+    bool is_slow(std::string_view regexp) const {
+        const std::string masked_regexp = 
mask_escaped_characters_and_character_classes(regexp);
+        return is_slow_one_repeat(masked_regexp) || 
is_slow_two_repeats(masked_regexp);

Review Comment:
   [P1] Account for multiplicative nested repeats
   
   This checks each bound independently, so `(((ab?c?d?){50}){50}){50}` returns 
false even though the nested repeats multiply four positions by `50^3` and 
reach Hyperscan's 500,000-position expansion ceiling. [Hyperscan recursively 
builds the repeated child before checking `vcount * 
copies`](https://github.com/intel/hyperscan/blob/v5.4.2/src/parser/ComponentRepeat.cpp#L113-L152),
 so adding another `{50}` layer still constructs that 500,000-position child 
before the outer rejection. Track effective nested expansion (or conservatively 
intercept nested bounded products) and add direct/open/execute regressions.



##########
fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java:
##########
@@ -3513,6 +3515,10 @@ public void 
checkAnnIndexCandidateRowsPercentThreshold(String value) {
             description = "Enable extended regular expressions, support 
look-around zero-width assertions")
     public boolean enableExtendedRegex = false;
 
+    @VarAttrDef.VarAttr(name = ENABLE_HYPERSCAN_FALLBACK, needForward = true, 
affectQueryResultInExecution = true,

Review Comment:
   [P2] Invalidate cached point-query state when this value changes
   
   Marking the variable execution-affecting does not refresh prepared 
short-circuit contexts. `ShortCircuitQueryContext` serializes `TQueryOptions` 
once, while `isReusable()` checks table metadata and only 
`fileCacheQueryLimitBytes`; BE then keeps that RuntimeState and its opened 
output expressions. A prepared point query with `value_col REGEXP 'a{51}'` can 
therefore keep the RE2-backed state after the session changes this setting from 
true to false. Include the execution-variable digest (or this field) in the 
reuse fence and test a setting change between executions.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to