@@ -383,22 +383,25 @@ func (helper *extractHelper) extractLikePatternCol(
383383 continue
384384 }
385385
386- var canBuildPattern bool
386+ var canBuildPattern , isPrefilter bool
387387 var pattern string
388388 // We use '|' to combine DNF regular expression: .*a.*|.*b.*
389389 // e.g:
390390 // SELECT * FROM t WHERE c LIKE '%a%' OR c LIKE '%b%'
391391 if fn .FuncName .L == ast .LogicOr && ! toLower {
392- canBuildPattern , pattern = helper .extractOrLikePattern (ctx , fn , extractColName , extractCols , needLike2Regexp )
392+ canBuildPattern , pattern , isPrefilter = helper .extractOrLikePattern (ctx , fn , extractColName , extractCols , needLike2Regexp , toLower )
393393 } else {
394- canBuildPattern , pattern = helper .extractLikePattern (ctx , fn , extractColName , extractCols , needLike2Regexp )
394+ canBuildPattern , pattern , isPrefilter = helper .extractLikePattern (ctx , fn , extractColName , extractCols , needLike2Regexp , toLower )
395395 }
396396 if canBuildPattern && toLower {
397397 pattern = strings .ToLower (pattern )
398398 }
399399 if canBuildPattern {
400400 patterns = append (patterns , pattern )
401- } else {
401+ }
402+ // A prefilter narrows the scan but is not exactly equivalent to the
403+ // predicate, so it is pushed down *and* kept for a scalar recheck.
404+ if ! canBuildPattern || isPrefilter {
402405 remained = append (remained , expr )
403406 }
404407 }
@@ -411,40 +414,51 @@ func (helper extractHelper) extractOrLikePattern(
411414 extractColName string ,
412415 extractCols map [int64 ]* types.FieldName ,
413416 needLike2Regexp bool ,
417+ toLower bool ,
414418) (
415419 ok bool ,
416420 pattern string ,
421+ isPrefilter bool ,
417422) {
418423 predicates := expression .SplitDNFItems (orFunc )
419424 if len (predicates ) == 0 {
420- return false , ""
425+ return false , "" , false
421426 }
422427
423428 patternBuilder := make ([]string , 0 , len (predicates ))
424429 for _ , predicate := range predicates {
425430 fn , ok := predicate .(* expression.ScalarFunction )
426431 if ! ok {
427- return false , ""
432+ return false , "" , false
428433 }
429434
430- ok , partPattern := helper .extractLikePattern (ctx , fn , extractColName , extractCols , needLike2Regexp )
435+ ok , partPattern , partIsPrefilter := helper .extractLikePattern (ctx , fn , extractColName , extractCols , needLike2Regexp , toLower )
431436 if ! ok {
432- return false , ""
437+ return false , "" , false
433438 }
439+ // One inexact branch makes the whole disjunction inexact.
440+ isPrefilter = isPrefilter || partIsPrefilter
434441 patternBuilder = append (patternBuilder , partPattern )
435442 }
436- return true , strings .Join (patternBuilder , "|" )
443+ return true , strings .Join (patternBuilder , "|" ), isPrefilter
437444}
438445
446+ // extractLikePattern builds the pushed-down pattern for a single predicate.
447+ // toLower reports whether the caller folds case on both the pattern and the
448+ // scanned value, which decides how a case-insensitive ILIKE is represented.
449+ // isPrefilter reports that the pattern only narrows the scan and is not exactly
450+ // equivalent to the predicate, so the caller must keep a scalar recheck.
439451func (helper extractHelper ) extractLikePattern (
440452 ctx base.PlanContext ,
441453 fn * expression.ScalarFunction ,
442454 extractColName string ,
443455 extractCols map [int64 ]* types.FieldName ,
444456 needLike2Regexp bool ,
457+ toLower bool ,
445458) (
446459 ok bool ,
447460 pattern string ,
461+ isPrefilter bool ,
448462) {
449463 var colName string
450464 var datums []types.Datum
@@ -453,21 +467,62 @@ func (helper extractHelper) extractLikePattern(
453467 colName , datums , _ = helper .extractColBinaryOpConsExpr (ctx , extractCols , fn )
454468 }
455469 if colName != extractColName {
456- return false , ""
470+ return false , "" , false
457471 }
458472 switch fn .FuncName .L {
459473 case ast .EQ :
460- return true , "^" + regexp .QuoteMeta (datums [0 ].GetString ()) + "$"
474+ return true , "^" + regexp .QuoteMeta (datums [0 ].GetString ()) + "$" , false
461475 case ast .Like , ast .Ilike :
462- if needLike2Regexp {
463- return true , stringutil .CompileLike2Regexp (datums [0 ].GetString ())
476+ // The pushed-down pattern must honour the LIKE ESCAPE so it does not
477+ // diverge from the original predicate (issue #69653). The escape has to
478+ // be resolvable at plan time: for a constant escape (including the
479+ // default '\' and the empty escape of NO_BACKSLASH_ESCAPES) we compile
480+ // the regexp with it; a non-constant/deferred escape can't be resolved
481+ // here, so we skip extraction and let the scalar predicate be rechecked.
482+ escape , ok := likeEscapeConst (fn )
483+ if ! ok {
484+ return false , "" , false
485+ }
486+ if ! needLike2Regexp {
487+ return true , datums [0 ].GetString (), false
464488 }
465- return true , datums [0 ].GetString ()
489+ pattern = stringutil .CompileLike2Regexp (datums [0 ].GetString (), escape )
490+ // ILIKE matches case-insensitively while the pattern above is
491+ // case-sensitive. Callers that fold case (toLower) lower both the pattern
492+ // and the scanned value, so it stays equivalent there. Callers that do
493+ // not -- the cluster log path sends the pattern verbatim in
494+ // SearchLogRequest.Patterns -- get a case-insensitive group instead, so
495+ // an "ERROR" line still matches `message ILIKE '%error%'`. The scoped
496+ // (?i:...) form keeps the flag from leaking across a '|' when several
497+ // patterns are combined into one disjunction. Case folding there is the
498+ // regexp engine's rather than ILIKE's, so it is only a prefilter and the
499+ // predicate is rechecked.
500+ if fn .FuncName .L == ast .Ilike && ! toLower {
501+ return true , "(?i:" + pattern + ")" , true
502+ }
503+ return true , pattern , false
466504 case ast .Regexp , ast .RegexpLike :
467- return true , datums [0 ].GetString ()
505+ return true , datums [0 ].GetString (), false
468506 default :
469- return false , ""
507+ return false , "" , false
508+ }
509+ }
510+
511+ // likeEscapeConst returns the ESCAPE byte of a LIKE/ILIKE ScalarFunction when
512+ // it is a plan-time constant (ok=true). The escape must be constant so the
513+ // pushed-down pattern can be compiled to match the predicate exactly (issue
514+ // #69653); a non-constant, deferred, or parameterized escape returns ok=false,
515+ // telling the caller to skip pushdown and keep a scalar recheck.
516+ func likeEscapeConst (fn * expression.ScalarFunction ) (byte , bool ) {
517+ args := fn .GetArgs ()
518+ if len (args ) < 3 {
519+ return 0 , false
520+ }
521+ escape , ok := args [2 ].(* expression.Constant )
522+ if ! ok || escape .DeferredExpr != nil || escape .ParamMarker != nil {
523+ return 0 , false
470524 }
525+ return byte (escape .Value .GetInt64 ()), true
471526}
472527
473528func (extractHelper ) findColumn (schema * expression.Schema , names []* types.FieldName , colName string ) map [int64 ]* types.FieldName {
0 commit comments