Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a5a6400
planner: rewrite FTS predicates to LIKE if no FTS index
terry1purcell Jan 18, 2026
4ded0d5
build errors
terry1purcell Jan 18, 2026
5051049
build errors2
terry1purcell Jan 18, 2026
a11e436
testcase1
terry1purcell Jan 18, 2026
3379ed5
testcase2
terry1purcell Jan 18, 2026
ba0f3d7
review1
terry1purcell Jan 18, 2026
82416d8
review2
terry1purcell Jan 18, 2026
f7b1fa5
review3
terry1purcell Jan 18, 2026
c63c6bb
review4
terry1purcell Jan 18, 2026
0c74944
review5
terry1purcell Jan 18, 2026
dbdbce1
review6
terry1purcell Jan 18, 2026
dc10c79
review7
terry1purcell Jan 18, 2026
48373d1
review8
terry1purcell Jan 19, 2026
2121e81
review9
terry1purcell Jan 19, 2026
0b4ba84
review10
terry1purcell Jan 19, 2026
69b1497
Merge branch 'pingcap:master' into fts
terry1purcell Mar 1, 2026
04b00aa
Merge branch 'master' into fts
terry1purcell Apr 7, 2026
ab11dda
refactor
terry1purcell Apr 7, 2026
c9e0409
Merge branch 'master' into fts
terry1purcell Apr 25, 2026
8ba9a8a
expression: revert fts_match_word arity/impl changes not needed for L…
terry1purcell Apr 26, 2026
19e9dc3
planner, expression: fix four review findings in MATCH...AGAINST LIKE…
terry1purcell Apr 26, 2026
569f3aa
rebase after months of change
terry1purcell Apr 26, 2026
19aba2c
planner: handle optional+excluded boolean FTS terms in LIKE fallback
terry1purcell Apr 26, 2026
eb1d412
planner: fix four correctness issues in MATCH...AGAINST LIKE fallback
terry1purcell Apr 26, 2026
4200060
planner/util: update null-reject builtin registry snapshot for match_…
terry1purcell Apr 26, 2026
a566282
expression: regenerate builtin thread-safety files for builtinFtsMysq…
terry1purcell Apr 27, 2026
afcf285
tests: add match_against to SHOW BUILTINS expected output
terry1purcell Apr 27, 2026
d5cfdcb
planner, expression: fix review findings in MATCH...AGAINST LIKE fall…
terry1purcell Apr 27, 2026
4080f42
planner, expression: address review findings in MATCH...AGAINST LIKE …
terry1purcell Apr 27, 2026
c22f54c
planner: restrict MATCH...AGAINST LIKE rewrite to predicate contexts
terry1purcell Apr 27, 2026
b7733c7
planner: use ILIKE for case-insensitive MATCH...AGAINST LIKE fallback
terry1purcell Apr 27, 2026
e603043
planner: fix gofmt comment formatting in fulltext_to_like.go
terry1purcell Apr 27, 2026
1ddbcca
planner: add fts-native alternative round for TiFlash FTS cost compet…
terry1purcell Apr 27, 2026
dc2cccb
review updates
terry1purcell May 3, 2026
98e97fd
Merge branch 'pingcap:master' into fts
terry1purcell May 11, 2026
32b7c04
planner, expression: address review feedback on MATCH...AGAINST LIKE …
terry1purcell May 11, 2026
bac401f
bazel update
terry1purcell May 11, 2026
4aa6f93
*: stop tracking Claude Code runtime state
terry1purcell May 11, 2026
49db4da
planner: fix NULL search handling in MATCH...AGAINST LIKE fallback
terry1purcell May 11, 2026
3647abe
expression: gate FTSMysqlMatchAgainst Flash pushdown on default modifier
terry1purcell May 11, 2026
8803614
cardinality: route constant FTS substitutes to the constants bucket
terry1purcell May 11, 2026
b0b04c4
expression: emit Constant(NULL) for AGAINST(NULL) in selectivity subs…
terry1purcell May 12, 2026
aba6e18
planner: move column-type check above NULL fast-path in LIKE fallback
terry1purcell May 12, 2026
57f98c4
cardinality: refresh stale Constant(0) comment after Constant(NULL) c…
terry1purcell May 12, 2026
a18872c
expression: add defensive bounds check before indexing FTS validator …
terry1purcell May 12, 2026
ed3e7a3
planner: restore FTS cost competition between native and ILIKE plans
terry1purcell May 13, 2026
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
7 changes: 6 additions & 1 deletion pkg/expression/integration_test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,12 @@ func TestFTSSyntax(t *testing.T) {
// tk.MustContainErrMsg("select * from t where (fts_match_word('hello', title)) > 0", "Currently 'FTS_MATCH_WORD()' must be used alone")
// tk.MustContainErrMsg("select (fts_match_word('hello', title)) AS score from t where fts_match_word('hello', title)", "Currently 'FTS_MATCH_WORD()' cannot be used in SELECT fields")
tk.MustContainErrMsg("select * from t where match() against ('hello')", `You have an error in your SQL syntax`)
tk.MustContainErrMsg("select * from t where match(title) against ('hello' in boolean mode)", `UnknownType: *ast.MatchAgainst`)
// Test MATCH...AGAINST with default 'like' fallback mode - should succeed
tk.MustQuery("select * from t where match(title) against ('hello' in boolean mode)")
// Test MATCH...AGAINST with 'error' fallback mode - should fail
tk.MustExec("set @@tidb_opt_fulltext_search_fallback='error'")
tk.MustContainErrMsg("select * from t where match(title) against ('hello' in boolean mode)", `This version of TiDB doesn't yet support 'MATCH...AGAINST without fulltext index'`)
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test appears inconsistent with the implementation logic. The table at line 198 has a FULLTEXT INDEX on the title column. According to the logic in expression_rewriter.go lines 2234-2238, when a fulltext index exists, MATCH...AGAINST should always return an error "MATCH...AGAINST with fulltext index (native fulltext search not supported)", regardless of the fallback mode setting. However, line 211 expects the query to succeed with the default 'like' fallback mode. Either the test expectations are incorrect, or the hasFulltextIndex function is not properly detecting the index in this test environment.

Suggested change
// Test MATCH...AGAINST with default 'like' fallback mode - should succeed
tk.MustQuery("select * from t where match(title) against ('hello' in boolean mode)")
// Test MATCH...AGAINST with 'error' fallback mode - should fail
tk.MustExec("set @@tidb_opt_fulltext_search_fallback='error'")
tk.MustContainErrMsg("select * from t where match(title) against ('hello' in boolean mode)", `This version of TiDB doesn't yet support 'MATCH...AGAINST without fulltext index'`)
// Test MATCH...AGAINST when a fulltext index exists - should report native fulltext search not supported
tk.MustContainErrMsg("select * from t where match(title) against ('hello' in boolean mode)", `MATCH...AGAINST with fulltext index (native fulltext search not supported)`)
// Test MATCH...AGAINST with 'error' fallback mode - still reports native fulltext search not supported when fulltext index exists
tk.MustExec("set @@tidb_opt_fulltext_search_fallback='error'")
tk.MustContainErrMsg("select * from t where match(title) against ('hello' in boolean mode)", `MATCH...AGAINST with fulltext index (native fulltext search not supported)`)

Copilot uses AI. Check for mistakes.
tk.MustExec("set @@tidb_opt_fulltext_search_fallback='like'")
tk.MustContainErrMsg("select * from t where fts_match_word(title, body)", `match against a non-constant string`)
tk.MustContainErrMsg("select * from t where fts_match_word(45.67, body)", `match against a non-constant string`)
tk.MustContainErrMsg("select * from t where fts_match_word('hello', title, body)", `Incorrect parameter count in the call to native function`)
Expand Down
2 changes: 2 additions & 0 deletions pkg/planner/core/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ go_library(
"expression_rewriter.go",
"find_best_task.go",
"flat_plan.go",
"fulltext_to_like.go",
"hint_utils.go",
"index_join_path.go",
"indexmerge_path.go",
Expand Down Expand Up @@ -209,6 +210,7 @@ go_test(
"exhaust_physical_plans_test.go",
"expression_test.go",
"find_best_task_test.go",
"fulltext_to_like_test.go",
"hint_test.go",
"indexmerge_intersection_test.go",
"indexmerge_path_test.go",
Expand Down
67 changes: 67 additions & 0 deletions pkg/planner/core/expression_rewriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -1692,6 +1692,8 @@ func (er *expressionRewriter) Leave(originInNode ast.Node) (retNode ast.Node, ok
}
er.ctxStack[len(er.ctxStack)-1].SetCoercibility(expression.CoercibilityExplicit)
er.ctxStack[len(er.ctxStack)-1].SetCharsetAndCollation(arg.GetType(er.sctx.GetEvalCtx()).GetCharset(), arg.GetType(er.sctx.GetEvalCtx()).GetCollate())
case *ast.MatchAgainst:
er.matchAgainstToExpression(v)
default:
er.err = errors.Errorf("UnknownType: %T", v)
return retNode, false
Expand Down Expand Up @@ -2217,6 +2219,71 @@ func (er *expressionRewriter) patternLikeOrIlikeToExpression(v *ast.PatternLikeO
er.ctxStackAppend(function, types.EmptyName)
}

func (er *expressionRewriter) matchAgainstToExpression(v *ast.MatchAgainst) {
// Check the session variable to determine behavior
var fallbackMode string
if er.planCtx != nil && er.planCtx.builder != nil && er.planCtx.builder.ctx != nil {
fallbackMode = er.planCtx.builder.ctx.GetSessionVars().FulltextSearchFallback
} else {
fallbackMode = "like" // default
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nil check chain at lines 2225-2226 could potentially panic if any of the intermediate values are nil. While the fallback to 'like' at line 2228 provides a default, the chained dereferencing (er.planCtx.builder.ctx.GetSessionVars()) could panic if 'builder' or 'ctx' is nil, before reaching the else block. Consider checking each level individually or restructuring the condition to be safer.

Suggested change
var fallbackMode string
if er.planCtx != nil && er.planCtx.builder != nil && er.planCtx.builder.ctx != nil {
fallbackMode = er.planCtx.builder.ctx.GetSessionVars().FulltextSearchFallback
} else {
fallbackMode = "like" // default
fallbackMode := "like" // default
if er.planCtx != nil && er.planCtx.builder != nil && er.planCtx.builder.ctx != nil {
if sv := er.planCtx.builder.ctx.GetSessionVars(); sv != nil {
fallbackMode = sv.FulltextSearchFallback
}

Copilot uses AI. Check for mistakes.
}

if fallbackMode == "error" {
er.err = expression.ErrNotSupportedYet.GenWithStackByArgs("MATCH...AGAINST without fulltext index")
return
}

// Both the column expressions and Against expression have been visited
// and pushed onto the ctxStack. The stack layout is:
// [..., col1, col2, ..., colN, against]
numColumns := len(v.ColumnNames)
l := len(er.ctxStack)
if l < numColumns+1 {
er.err = errors.Errorf("MATCH...AGAINST: expected %d column expressions and Against expression on stack, got %d", numColumns+1, l)
return
}

// The Against expression is the last one on the stack
againstExpr := er.ctxStack[l-1]

// Check if it's a constant string
constExpr, ok := againstExpr.(*expression.Constant)
if !ok {
er.err = expression.ErrNotSupportedYet.GenWithStackByArgs("MATCH...AGAINST with non-constant search string")
return
}

searchText, err := constExpr.Eval(er.sctx.GetEvalCtx(), chunk.Row{})
if err != nil {
er.err = err
return
}

if searchText.Kind() != types.KindString {
er.err = expression.ErrNotSupportedYet.GenWithStackByArgs("MATCH...AGAINST with non-string search expression")
return
}

// Get the column expressions from the stack
// They're at positions [l-numColumns-1 : l-1]
columns := make([]expression.Expression, numColumns)
for i := range numColumns {
columns[i] = er.ctxStack[l-numColumns-1+i]
}

// Pop all column expressions and the Against expression
er.ctxStackPop(numColumns + 1)

// Convert to LIKE predicates
result, err := er.convertMatchAgainstToLike(columns, searchText.GetString(), v.Modifier)
if err != nil {
er.err = err
return
}

er.ctxStackAppend(result, types.EmptyName)
}
Comment thread
terry1purcell marked this conversation as resolved.

func (er *expressionRewriter) regexpToScalarFunc(v *ast.PatternRegexpExpr) {
l := len(er.ctxStack)
er.err = expression.CheckArgsNotMultiColumnRow(er.ctxStack[l-2:]...)
Expand Down
Loading
Loading