Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions pkg/planner/core/memtable_predicate_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,14 @@ func (helper extractHelper) extractLikePattern(
case ast.EQ:
return true, "^" + regexp.QuoteMeta(datums[0].GetString()) + "$"
case ast.Like, ast.Ilike:
// The extracted pattern is compiled with the default escape ('\') by
// stringutil.CompileLike2Regexp, so a non-default ESCAPE would make the
// pushed-down pattern diverge from the original predicate. Only build a
// pattern when the escape is the default; otherwise skip extraction so
// the scalar predicate is kept and rechecked (issue #69653).
if !likeEscapeIsDefault(fn) {
return false, ""
}
if needLike2Regexp {
return true, stringutil.CompileLike2Regexp(datums[0].GetString())
}
Expand All @@ -470,6 +478,23 @@ func (helper extractHelper) extractLikePattern(
}
}

// likeEscapeIsDefault reports whether the ESCAPE argument of a LIKE/ILIKE
// ScalarFunction is the default backslash escape. Only then is it safe to
// compile the pattern with stringutil.CompileLike2Regexp, which hardcodes '\'
// as the escape. A non-default (or non-constant) escape must fall back to a
// scalar recheck instead of being pushed down. See issue #69653.
func likeEscapeIsDefault(fn *expression.ScalarFunction) bool {
args := fn.GetArgs()
if len(args) < 3 {
return false
}
escape, ok := args[2].(*expression.Constant)
if !ok || escape.DeferredExpr != nil || escape.ParamMarker != nil {
return false
}
return escape.Value.GetInt64() == int64('\\')
}

func (extractHelper) findColumn(schema *expression.Schema, names []*types.FieldName, colName string) map[int64]*types.FieldName {
extractCols := make(map[int64]*types.FieldName)
for i, name := range names {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,3 +510,31 @@ func TestMemtableInfoschemaExtractorPart4(t *testing.T) {
}
testMemtableInfoschemaExtractor(t, tcs)
}

func TestInfoSchemaTableNameLikeEscape(t *testing.T) {
store := testkit.CreateMockStore(t)
tk := testkit.NewTestKit(t, store)
tk.MustExec("create database like_escape")
tk.MustExec("use like_escape")
tk.MustExec("create table `abc_def` (a int)")
tk.MustExec("create table `abc#x` (a int)")

// With ESCAPE '#', "#_" is a literal underscore, so only `abc_def` matches.
// The memtable extractor must not push the LIKE pattern down while ignoring
// the custom escape: doing so compiles the pattern with the default '\'
// escape, which instead matches `abc#x` (a row that fails its own
// predicate) and drops `abc_def`. Every returned row must satisfy the same
// LIKE ... ESCAPE predicate it was selected by. See issue #69653.
tk.MustQuery("select table_name, table_name like '%#_%' escape '#' as self_true " +
"from information_schema.tables " +
"where table_schema = 'like_escape' and table_name like '%#_%' escape '#'").
Check(testkit.Rows("abc_def 1"))

// A non-default escape must be kept as a scalar Selection rather than folded
// into the pushed-down table_name pattern.
plan := tk.MustQuery("explain format='brief' select table_name from information_schema.tables " +
"where table_schema = 'like_escape' and table_name like '%#_%' escape '#'").String()
if !strings.Contains(plan, "Selection") {
t.Fatalf("expected the custom-escape LIKE to be kept as a Selection, plan:\n%s", plan)
}
}