From a8b00e039851af3cbd51587f7a58a230560f316b Mon Sep 17 00:00:00 2001 From: vismaytiwari Date: Sun, 5 Jul 2026 00:47:47 +0530 Subject: [PATCH] planner: recheck LIKE/ILIKE with a non-default ESCAPE on memtable scans The information_schema memtable predicate extractor pushes a LIKE pattern down into the scan and drops the original predicate, but always compiles the pattern with the default '\' escape via stringutil.CompileLike2Regexp. When the query uses a custom ESCAPE, the pushed-down pattern diverges from the real predicate, so the scan can return rows that fail the predicate and omit rows that match. The same is true when NO_BACKSLASH_ESCAPES makes the escape empty. Only build a pushed-down pattern when the escape is the default backslash; otherwise leave the predicate in place so it is rechecked as a scalar Selection. This keeps the existing fast path for the common default-escape case while restoring correct results for custom escapes. Issue Number: close #69653 Signed-off-by: vismaytiwari --- .../core/memtable_predicate_extractor.go | 25 +++++++++++++++++ .../memtable_infoschema_extractor_test.go | 28 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/pkg/planner/core/memtable_predicate_extractor.go b/pkg/planner/core/memtable_predicate_extractor.go index f77707fa940fd..db5a5a2a0e993 100644 --- a/pkg/planner/core/memtable_predicate_extractor.go +++ b/pkg/planner/core/memtable_predicate_extractor.go @@ -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()) } @@ -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 { diff --git a/pkg/planner/core/tests/extractor/memtable_infoschema_extractor_test.go b/pkg/planner/core/tests/extractor/memtable_infoschema_extractor_test.go index 73a049369d761..6b7c641ce2e34 100644 --- a/pkg/planner/core/tests/extractor/memtable_infoschema_extractor_test.go +++ b/pkg/planner/core/tests/extractor/memtable_infoschema_extractor_test.go @@ -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) + } +}