Skip to content

planner: rewrite FTS predicates to LIKE for evaluation of non-TiCI query plan (#65626)#68743

Open
ti-chi-bot wants to merge 1 commit into
pingcap:release-8.5from
ti-chi-bot:cherry-pick-65626-to-release-8.5
Open

planner: rewrite FTS predicates to LIKE for evaluation of non-TiCI query plan (#65626)#68743
ti-chi-bot wants to merge 1 commit into
pingcap:release-8.5from
ti-chi-bot:cherry-pick-65626-to-release-8.5

Conversation

@ti-chi-bot
Copy link
Copy Markdown
Member

@ti-chi-bot ti-chi-bot commented May 29, 2026

This is an automated cherry-pick of #65626

What problem does this PR solve?

Issue Number: close #68153

Problem Summary:

What changed and how does it work?

Summary

When tidb_opt_enable_alternative_logical_plans=ON, adds a fallback that rewrites MATCH ... AGAINST to case-insensitive ILIKE predicates so full-text-search queries can execute without an FTS index. The rewrite is intentionally conservative: it only fires for a strict subset of search strings in direct-boolean predicate positions, and reaches the plan only when the round-1 native path is not viable. Anything outside that envelope either keeps the native builtin (errors at execution without an FTS index) or is rejected at plan time, never silently producing wrong rows.

Architecture

Round 1 (default, matches Alt-disabled behavior) — emits the native FTSMysqlMatchAgainst builtin. The expression rewriter records nonViableFTSMatch on the PlanBuilder when a direct-boolean-context MATCH cannot be served natively (no FTS index on a TiFlash replica, or a non-pushdown-safe modifier).

Round 2: fts-like-fallback — fires only when round 1 reported a non-viable MATCH. The driver discards round 1's plan and re-runs the build with AlternativeLogicalPlanFTSLikeFallback=true, which switches the rewriter to ILIKE in direct-boolean positions. If this round also errors (e.g. unsupported search string with no FTS-index rescue), lastAltRoundErr propagates the message instead of the generic "failed to build logical plan" sentinel.

A single flag (AlternativeLogicalPlanFTSLikeFallback) drives the dispatch; viability state stays local to the build on PlanBuilder.nonViableFTSMatch.

Where the rewrite applies

inDirectMatchBooleanContext (modeled on the existing canTreatInSubqueryAsExistsForFilter) walks the AST ancestor stack and accepts only:

  • Root of WHERE / HAVING / JOIN ON, with ancestors limited to AND, OR, NOT, parentheses.

Everything else — MATCH ... > 0.5, MATCH ... = 0, MATCH ... IS NULL, MATCH inside CASE WHEN, arithmetic, scoring (SELECT field list, ORDER BY), etc. — keeps the native builtin, which preserves the float relevance score and errors at execution if no FTS index exists. Substituting a 0/1 integer in those positions would silently corrupt the comparison or sort.

Strict search-string subset

ValidateFTSSearchStringForLikeFallback rejects anything that would tokenize differently in MySQL FTS than a substring ILIKE match. Accepted by mode:

  • Natural-language mode: whitespace-separated alphanumeric word tokens only.
  • Boolean mode: each token is word, +word, or -word, where word is alphanumeric (ASCII or non-ASCII UTF-8).

Rejected at plan time with error 1235 (ErrNotSupportedYet): phrases "...", prefix wildcard term*, relevance modifiers > < ~, grouping (...), mid-word punctuation like xx-yy, and any token containing %, _, , ,, ., :, etc. WITH QUERY EXPANSION is likewise rejected (no ILIKE approximation exists).

Modifier handling

The tipb pushdown protocol does not serialize the FTS modifier (see distsql_builtin.go), so a Boolean-mode or WITH QUERY EXPANSION MATCH pushed down to TiFlash would silently execute as natural-language mode. To prevent this, matchAgainstToBuiltin rejects non-default modifiers at plan time unless matchHasLikeFallbackRescue is true (alt enabled + direct-boolean context, where the alt-rounds driver will discard the native plan and rebuild via fts-like-fallback). In practice:

  │     Modifier     │     Direct-boolean predicate     │ Scalar/scoring position │
  ├──────────────────┼──────────────────────────────────┼─────────────────────────┤                                                                                               
  │ Natural language │ native (round 1) or ILIKE rescue │ native                  │                                                                                             
  ├──────────────────┼──────────────────────────────────┼─────────────────────────┤
  │ Boolean          │ ILIKE rescue                     │ error 1235              │                                                                                               
  ├──────────────────┼──────────────────────────────────┼─────────────────────────┤                                                                                               
  │ With QE          │ error 1235 from ILIKE round      │ error 1235              │                                                                                               
  └──────────────────┴──────────────────────────────────┴─────────────────────────┘                                                                                               

NULL search handling

MATCH(c) AGAINST(NULL) matches nothing in MySQL FTS semantics, but three-valued logic matters under NOT: native evalReal returns NULL, so NOT NULL = NULL filters the row. The rewrite emits Constant(NULL) rather than Constant(0) so the same semantics hold under NOT, IS NULL, etc. The plan-cache skip is set before the NULL fast-path, so a prepared statement bound to NULL first followed by a non-NULL bind re-plans and returns correct rows instead of reusing a cached constant-false plan.

Boolean-mode operator support (post-strict-subset)

  │   Operator    │                              Behavior                               │                                                                                       
  ├───────────────┼─────────────────────────────────────────────────────────────────────┤                                                                                         
  │ +term         │ All required terms must match (AND across the per-term column DNFs) │
  ├───────────────┼─────────────────────────────────────────────────────────────────────┤                                                                                         
  │ -term         │ NOT(any column matches term)                                        │                                                                                         
  ├───────────────┼─────────────────────────────────────────────────────────────────────┤
  │ term          │ Optional — anchors the result only when no +term exists             │                                                                                         
  ├───────────────┼─────────────────────────────────────────────────────────────────────┤                                                                                         
  │ --only        │ Returns empty (matches MySQL)                                       │
  ├───────────────┼─────────────────────────────────────────────────────────────────────┤                                                                                         
  │ Anything else │ Rejected by ValidateFTSSearchStringForLikeFallback (1235)           │                                                                                       
  └───────────────┴─────────────────────────────────────────────────────────────────────┘                                                                                         

Plan cache

Marks the plan non-cacheable when the AGAINST argument is mutable across executions (? parameter marker, user variable, deferred expression). Literal AGAINST values keep the plan cacheable. The skip runs before the NULL fast-path so a NULL first bind can't bake a constant plan that gets reused later.

Selectivity

BuildFTSToILikeExpressionFromBuiltin substitutes the equivalent ILIKE form for the opaque FTSMysqlMatchAgainst builtin so the round 1 native plan's cost reflects column histogram/TopN rather than the flat SelectivityFactor (0.8). Restricted to single-column MATCH because GetSelectivityByFilter declines multi-column expressions — a multi-column substitute would fall through to the same str-match default anyway.

Known semantic differences

These apply to ILIKE round queries only; the native path preserves full MySQL semantics:

  • No relevance scoring (0/1 boolean output).
  • No word boundaries (%cat% matches concatenate).
  • No stop-word filtering or minimum word length.
  • Optional-term relevance ranking is approximated as a filter when no required terms exist.

Files changed

  │                           File                            │                                                    Purpose                                                     │
  ├───────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤  
  │ pkg/expression/fts_to_like.go                             │ Strict-subset validator, ILIKE expression builder, mode-dispatch helpers, single-column substitution wrapper   │
  ├───────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤  
  │ pkg/expression/fts_to_like_test.go                        │ Validator coverage (33 cases), BuildFTSToILikeExpressionFromBuiltin coverage                                   │  
  ├───────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤  
  │ pkg/expression/builtin_fts.go                             │ Native builtinFtsMysqlMatchAgainstSig with modifier, defensive evalReal returning NULL for NULL search         │  
  ├───────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤  
  │ pkg/planner/core/expression_rewriter.go                   │ matchAgainstToExpression dispatcher, inDirectMatchBooleanContext, ftsNativeViable, modifier guard,             │
  │                                                           │ matchHasLikeFallbackRescue                                                                                     │  
  ├───────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pkg/planner/core/fulltext_to_like.go                      │ Thin wrapper delegating to pkg/expression                                                                      │  
  ├───────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pkg/planner/core/fulltext_to_like_test.go                 │ Unit tests for ftsModifierAllowsNativePushdown, tableHasPublicFTSIndexOnColumn                                 │  
  ├───────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pkg/planner/core/planbuilder.go                           │ PlanBuilder.nonViableFTSMatch + HasNonViableFTSMatch/MarkNonViableFTSMatch accessors                           │  
  ├───────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pkg/planner/optimize.go                                   │ fts-like-fallback alternative round, round-1 plan invalidation, lastAltRoundErr                                │  
  ├───────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pkg/planner/cardinality/selectivity.go                    │ Selectivity substitution for FTSMysqlMatchAgainst (single-column only)                                         │  
  ├───────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pkg/sessionctx/stmtctx/stmtctx.go                         │ Single AlternativeLogicalPlanFTSLikeFallback flag                                                              │  
  ├───────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ pkg/parser/ast/functions.go                               │ FTSMysqlMatchAgainst = "match_against" constant                                                                │  
  ├───────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ tests/integrationtest/t/planner/core/fulltext_search.test │ 94 integration test cases                                                                                      │  
  └───────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Test plan

  • 94 integration cases covering: natural-language mode, boolean mode (+/-/optional), empty/whitespace/NULL search, non-string columns, HAVING/JOIN ON predicates, NOT MATCH,
    parenthesized MATCH, scalar positions (IS NULL, > 0.5, = 0, CASE), non-default modifiers in scoring/scalar/alt-disabled contexts, prepared statements (literal cacheable,
    parameter-marker non-cacheable, NULL-first-bind re-plans).
  • Unit: validator (33 cases), BuildFTSToILikeExpressionFromBuiltin (nil/wrong-fn/single-column/multi-column/NULL/non-subset), parser tests aligned with the strict subset.
  • Strict-subset rejections previously-passing edge cases now expect error 1235.
  • make lint passes.
  • gofmt clean.

🤖 Generated with https://claude.com/claude-code

Check List

Tests

  • Unit test
  • Integration test
  • Manual test (add detailed scripts or steps below)
  • No need to test
    • I checked and no code files have been changed.

Side effects

  • Performance regression: Consumes more CPU
  • Performance regression: Consumes more Memory
  • Breaking backward compatibility

Documentation

  • Affects user behaviors
  • Contains syntax changes
  • Contains variable changes
  • Contains experimental features
  • Changes MySQL compatibility

Release note

Please refer to Release Notes Language Style Guide to write a quality release note.

None

Summary by CodeRabbit

Release Notes

  • New Features

    • Added full-text search (FTS) functionality with support for MATCH...AGAINST queries and FTS_MATCH_WORD operations.
    • Implemented FTS to LIKE expression fallback for compatibility when native full-text indexes are unavailable.
  • Improvements

    • Extended TiFlash expression pushdown to recognize full-text search operations with appropriate modifier validation.
    • Enhanced query optimizer with multi-round alternative logical plan evaluation to improve FTS query execution strategy selection.
  • Tests

    • Added comprehensive test coverage for full-text search query handling, edge cases, and NULL semantics.

Review Change Stack

Signed-off-by: ti-chi-bot <ti-community-prow-bot@tidb.io>
@ti-chi-bot ti-chi-bot added do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. ok-to-test Indicates a PR is ready to be tested. release-note-none Denotes a PR that doesn't merit a release note. sig/planner SIG: Planner size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. type/cherry-pick-for-release-8.5 This PR is cherry-picked to release-8.5 from a source PR. labels May 29, 2026
@ti-chi-bot
Copy link
Copy Markdown
Member Author

@terry1purcell This PR has conflicts, I have hold it.
Please resolve them or ask others to resolve them, then comment /unhold to remove the hold label.

@ti-chi-bot
Copy link
Copy Markdown

ti-chi-bot Bot commented May 29, 2026

@ti-chi-bot: ## If you want to know how to resolve it, please read the guide in TiDB Dev Guide.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the ti-community-infra/tichi repository.

@ti-chi-bot
Copy link
Copy Markdown

ti-chi-bot Bot commented May 29, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign 0xpoe, d3hunter, windtalker for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This PR implements full-text search (FTS) query support in TiDB with automatic fallback to ILIKE when native FTS infrastructure is unavailable. It adds MATCH...AGAINST builtin functions, expression rewriting logic to convert FTS to ILIKE predicates under a strict subset, and a multi-round optimizer that can rebuild logical plans to try FTS fallback when native execution isn't viable.

Changes

FTS Query Rewriting and Alternative Logical Planning

Layer / File(s) Summary
FTS builtin functions and expression registry
pkg/parser/ast/functions.go, pkg/expression/builtin_fts.go, pkg/expression/builtin.go, pkg/expression/distsql_builtin.go, pkg/expression/BUILD.bazel, pkg/expression/builtin_threadunsafe_generated.go
Added FTSMatchWord and FTSMysqlMatchAgainst builtin function implementations with signature dispatch, modifier handling, and thread-unsafe method generation for ~60 builtin signature types.
FTS→ILIKE fallback expression conversion
pkg/expression/fts_to_like.go, pkg/expression/fts_to_like_test.go
Implements conversion from MATCH...AGAINST to ILIKE predicates: parses boolean/natural mode search strings, validates against unsupported constructs, escapes LIKE metacharacters, builds CNF/DNF predicate combinations per mode, and handles NULL semantics preservation.
Planner FTS detection and routing
pkg/planner/core/expression_rewriter.go, pkg/planner/core/fulltext_to_like.go, pkg/planner/core/fulltext_to_like_test.go, pkg/planner/core/planbuilder.go
Expression rewriter detects MATCH...AGAINST in direct boolean contexts, checks native FTS viability (TiFlash availability, public FULLTEXT indexes, supported modifiers), and routes to either native builtin emission or ILIKE fallback with predicate/state tracking.
Alternative logical plan optimization framework
pkg/planner/optimize.go, pkg/sessionctx/stmtctx/stmtctx.go
Refactors optimizer to support multi-round evaluation: snapshots logical-build state, runs default round, detects FTS fallback signals, restores initial context for per-round rebuilds, applies session setup/cleanup per round, and selects lowest-cost plan.
Selectivity estimation and TiFlash pushdown for FTS
pkg/planner/cardinality/selectivity.go, pkg/expression/infer_pushdown.go
Updates selectivity computation to substitute FTS→ILIKE for single-column cases to improve cardinality estimation; guards TiFlash pushdown to reject FTS modifiers that would be silently dropped (Boolean mode, WITH QUERY EXPANSION).
FTS integration testing and validation
tests/integrationtest/t/planner/core/fulltext_search.test, tests/integrationtest/r/planner/core/fulltext_search.result
Comprehensive test suite covering natural-language and boolean mode queries, NULL semantics, modifier rejection, edge cases in different SQL contexts (SELECT, ORDER BY, HAVING, JOIN), and prepared-statement caching behavior.
Testing infrastructure and verification
pkg/expression/function_traits_test.go, pkg/planner/util/null_misc_test.go
Regression test snapshots for builtin function registry and null-rejection proof modes; validators ensure correctness of function trait filtering and expression evaluation semantics.
Configuration and file cleanup
.gitignore, tests/integrationtest/r/executor/show.result, pkg/planner/core/BUILD.bazel, pkg/expression/BUILD.bazel, pkg/expression/integration_test/integration_test.go
Updates build manifests to include new FTS files, refreshes test output snapshots to reflect SHOW BUILTINS changes, and marks conflicting test region as commented-out pending resolution.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pingcap/tidb#65626: Introduces the same core FTS→LIKE machinery across expression and planner layers.
  • pingcap/tidb#68726: Modifies the same fts_to_like.go implementation for search-string parsing and validation changes.
  • pingcap/tidb#68708: Related FTS→ILIKE conversion logic for handling quoted/special term syntax in searches.

Suggested reviewers

  • qw4990
  • winoros
  • AilinKid
  • terry1purcell

Poem

🐰 With FTS queries now converted to LIKE,
We fallback gracefully when TiFlash isn't quite right,
Boolean terms and natural modes in a dance,
Multi-round plans that give every search a chance!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: rewriting FTS predicates to LIKE for query plan evaluation without native FTS support.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering the problem, architecture, implementation details, test coverage, and file changes.
Linked Issues check ✅ Passed The PR addresses issue #68153 by implementing an FTS-to-LIKE fallback mechanism that allows MATCH...AGAINST queries to execute without a fulltext index.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the FTS-to-LIKE fallback feature described in the objectives and issue #68153; no unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ti-chi-bot
Copy link
Copy Markdown

ti-chi-bot Bot commented May 29, 2026

@ti-chi-bot: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
idc-jenkins-ci-tidb/unit-test 598a386 link true /test unit-test
idc-jenkins-ci-tidb/check_dev 598a386 link true /test check-dev
idc-jenkins-ci-tidb/check_dev_2 598a386 link true /test check-dev2
idc-jenkins-ci-tidb/mysql-test 598a386 link true /test mysql-test
idc-jenkins-ci-tidb/build 598a386 link true /test build

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@tiprow
Copy link
Copy Markdown

tiprow Bot commented May 29, 2026

@ti-chi-bot: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
fast_test_tiprow_for_release 598a386 link true /test fast_test_tiprow_for_release

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.gitignore:
- Around line 40-67: The .gitignore contains leftover merge conflict markers
(<<<<<<< HEAD, =======, >>>>>>>) and both sets of entries; remove the conflict
markers and merge the two sides into the intended final ignore list by choosing
or combining the desired lines (e.g., keep the consolidated ignores such as
*.log.json, genkeyword, test_coverage, coverage.dat plus the integration tests,
local dev artifacts, personal config files, and .claude entries) so the file is
clean and contains only valid ignore patterns with no conflict markers.

In `@pkg/expression/BUILD.bazel`:
- Around line 63-67: The BUILD.bazel srcs block contains unresolved cherry-pick
conflict markers (<<<<<<< HEAD / ======= / >>>>>>>) and is missing
builtin_fts.go which will break compilation; remove the conflict markers from
the go_library.srcs list, ensure the entries include "fts_helper.go",
"fts_to_like.go", and add "builtin_fts.go" to the srcs for the pkg/expression
go_library so Bazel can parse the BUILD file and compile the package.

In `@pkg/expression/builtin.go`:
- Around line 972-978: There are leftover git conflict markers in the funcs
registry; remove the markers (<<<<<<< HEAD and >>>>>>> ...) and ensure the two
entries for ast.FTSMatchWord -> ftsMatchWordFunctionClass and
ast.FTSMysqlMatchAgainst -> ftsMysqlMatchAgainstFunctionClass remain correctly
placed in the funcs map with proper commas and no conflict text so
pkg/expression builds cleanly.

In `@pkg/expression/distsql_builtin.go`:
- Around line 1152-1163: Remove the leftover git conflict markers (<<<<<<<,
=======, >>>>>>>) in the DistSQL scalar function switch and restore the two FTS
cases so the switch compiles: include the case tipb.ScalarFuncSig_FTSMatchWord
with f = &builtinFtsMatchWordSig{base} and the case
tipb.ScalarFuncSig_FTSMatchExpression with f =
&builtinFtsMysqlMatchAgainstSig{baseBuiltinFunc: base} (or match the surrounding
cases' struct field style), ensuring there are no conflict markers left and the
switch entries follow the existing switch syntax.

In `@pkg/expression/fts_to_like.go`:
- Around line 67-73: isFTSWordByte wrongly treats any byte >127 as a word
character; change the logic to perform rune-level validation using unicode
properties (e.g., unicode.IsLetter/unicode.IsDigit) or the repo's tokenizer
utility instead of byte checks. Replace isFTSWordByte with a rune-based checker
(or add isFTSRune) and update callers such as
ValidateFTSSearchStringForLikeFallback to iterate over runes (not bytes) and
call the new rune validator so non-ASCII punctuation/symbols are not treated as
word characters.

In `@pkg/expression/function_traits_test.go`:
- Around line 28-312: Remove the leftover git conflict markers (<<<<<<<,
=======, >>>>>>>) that wrap the TestIllegalFunctions4GeneratedColumns block so
the file is valid Go; ensure only one coherent copy of the
TestIllegalFunctions4GeneratedColumns function remains (keeping the intended
knownGood list and the loop that computes legal using GetBuiltinList() and
IllegalFunctions4GeneratedColumns), delete the conflict separators and
duplicated code, and run go test to confirm the package compiles.

In `@pkg/expression/infer_pushdown.go`:
- Around line 416-432: Remove the leftover merge conflict markers (<<<<<<<,
=======, >>>>>>>) and merge in the new FTS cases so the switch returns the
intended values: add the case ast.FTSMatchWord to return true and the case
ast.FTSMysqlMatchAgainst to extract
function.Function.(*builtinFtsMysqlMatchAgainstSig) and return ok &&
!sig.modifier.IsBooleanMode() && !sig.modifier.WithQueryExpansion(); ensure the
code compiles and keep the comment about TiFlash/modifier behavior and the
reference to matchAgainstToBuiltin intact.

In `@pkg/expression/integration_test/integration_test.go`:
- Around line 65-267: There is an unresolved Git conflict block (<<<<<<< HEAD /
======= / >>>>>>>) in the integration_test.go file starting near the
TestFTSParser/TestFTSSyntax/TestFTSIndexSyntax tests; remove the conflict
markers and merge the two sides into a single valid Go source so the package
builds (ensure the TestFTSParser, TestFTSSyntax and TestFTSIndexSyntax functions
remain intact and any duplicated or commented-out sections are resolved into the
intended final test code).

In `@pkg/parser/ast/functions.go`:
- Around line 369-375: The snippet contains unresolved Git conflict markers
(<<<<<<<, =======, >>>>>>>) around the FTS constants which prevents compilation;
remove the conflict markers and merge the branches so the FTS constants are
defined cleanly (e.g., ensure FTSMatchWord and FTSMysqlMatchAgainst are present
in the same const block within the functions.go file and no conflict markers
remain); verify the const block compiles and run a build to confirm the parser
package compiles.

In `@pkg/planner/core/BUILD.bazel`:
- Around line 18-23: Resolve the Git conflict markers in BUILD.bazel by removing
the <<<<<<<, =======, and >>>>>>> lines and producing a correct srcs list that
contains all intended source files (ensure entries like "foreign_key.go",
"fragment.go", "fragment_test.go" and "fulltext_to_like.go" are present if they
belong to this target); update the same resolution for the duplicate conflict at
the other hunk (around the referenced 231-235 area). Ensure the final target's
srcs is a valid comma-separated list of string literals with no conflict markers
so Bazel can parse the file.

In `@pkg/planner/core/expression_rewriter.go`:
- Around line 680-769: Remove the leftover conflict markers and restore the
three functions (canTreatInSubqueryAsExistsForFilter,
inDirectMatchBooleanContext, matchHasLikeFallbackRescue) exactly as intended,
then wire up AST ancestry plumbing so er.astNodeStack exists and is maintained:
add a stack field to expressionRewriter (e.g. astNodeStack []ast.Node) or reuse
an existing ancestor stack on planCtx, and update expressionRewriter.Enter and
Leave to push the visited ast.Node onto astNodeStack on Enter and pop on Leave.
Ensure the functions reference the correct clause via planCtx.builder.curClause
(or planCtx.curClause if that is the intended field) and guard nil
planCtx/builder as in the diff so the file compiles.

In `@pkg/planner/core/planbuilder.go`:
- Around line 322-371: The file contains unresolved Git conflict markers in the
PlanBuilder type area; remove the conflict markers (<<<<<<<, =======, >>>>>>>)
and reconcile the two competing blocks by merging the SavedViews field with the
new FTS-related fields (nonViableFTSMatch, predicateMatchSeen) into a single
PlanBuilder struct, and keep the accompanying accessor/mutator methods
(HasNonViableFTSMatch, MarkNonViableFTSMatch, HasPredicateMatch,
MarkPredicateMatch) only once; ensure PlanBuilder now declares SavedViews
[]*ast.TableName plus the two boolean fields and that the added methods
reference that single struct definition so the file compiles.

In `@pkg/planner/optimize.go`:
- Around line 469-703: The file contains unresolved git conflict markers
(<<<<<<<, =======, >>>>>>>) leaving a half-merged optimizer refactor; remove the
conflict markers and produce a single coherent version by keeping the new
logicalPlanBuildCtx, saveLogicalPlanBuildCtx, restoreLogicalPlanBuildCtx and
buildAndOptimizeLogicalPlanRound implementations (and only one declaration of
optimizeCnt), deleting the duplicate old fragment, and ensure references to
stmtctx, rule and any new symbols are correctly imported/used; verify
alternativeRounds and related helpers (shouldTryNonDecorrelationRound,
shouldTryOrderAwareReorderRound, shouldTryCorrelateRound,
savedEnableCorrelateSubquery, savedFTSLikeFallback) are present and consistent
with the rest of the file so the file compiles.
- Around line 639-699: The two package-level flags savedEnableCorrelateSubquery
and savedFTSLikeFallback are unsafe because they are shared across sessions;
make the saved state local to each optimize invocation by removing those globals
and storing the saved values per-round instead (either add fields like
savedEnableCorrelateSubquery/savedFTSLikeFallback to the alternativeRound struct
or rebuild alternativeRounds inside optimize() so each round’s setup/cleanup
closures capture local variables). Update the correlate round’s setup/cleanup
and the fts-like-fallback round’s setup/cleanup to read/write the saved value
from the per-round storage (or captured local) and ensure optimize() uses the
per-invocation alternativeRounds so concurrent Optimize calls don’t overwrite
each other.

In `@pkg/sessionctx/stmtctx/stmtctx.go`:
- Around line 449-486: The file contains unresolved git conflict markers
(<<<<<<<, =======, >>>>>>>) around the StatementContext additions, leaving the
struct half-merged; remove the conflict markers and ensure the full set of new
fields (AlternativeLogicalPlanDecorrelatedApply,
AlternativeLogicalPlanSameOrderIndexJoin,
AlternativeLogicalPlanOrderAwareJoinReorder,
AlternativeLogicalPlanPreferCorrelate, AlternativeLogicalPlanFTSLikeFallback,
AlternativeLogicalPlanHasPredicateContextMatch) are present exactly once in the
StatementContext definition (and any related setup/cleanup code blocks), delete
any duplicate or partial blocks from the other branch, and run go build/go vet
to verify the file parses cleanly.

In `@tests/integrationtest/r/executor/show.result`:
- Around line 759-763: The snapshot contains unresolved merge markers between
the builtin names "master_pos_wait" and "match_against"; open the expected
result file (tests/integrationtest/r/executor/show.result), remove the conflict
markers and choose the correct builtin entry (either keep "master_pos_wait" or
"match_against" as appropriate for the current codebase), then regenerate and
re-run the integration test to verify the SHOW BUILTINS golden output is updated
and committed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 899a881a-980a-4b99-8fdd-a1e3422bd6f1

📥 Commits

Reviewing files that changed from the base of the PR and between 651272b and 598a386.

📒 Files selected for processing (24)
  • .gitignore
  • pkg/expression/BUILD.bazel
  • pkg/expression/builtin.go
  • pkg/expression/builtin_fts.go
  • pkg/expression/builtin_threadunsafe_generated.go
  • pkg/expression/distsql_builtin.go
  • pkg/expression/fts_to_like.go
  • pkg/expression/fts_to_like_test.go
  • pkg/expression/function_traits_test.go
  • pkg/expression/infer_pushdown.go
  • pkg/expression/integration_test/integration_test.go
  • pkg/parser/ast/functions.go
  • pkg/planner/cardinality/selectivity.go
  • pkg/planner/core/BUILD.bazel
  • pkg/planner/core/expression_rewriter.go
  • pkg/planner/core/fulltext_to_like.go
  • pkg/planner/core/fulltext_to_like_test.go
  • pkg/planner/core/planbuilder.go
  • pkg/planner/optimize.go
  • pkg/planner/util/null_misc_test.go
  • pkg/sessionctx/stmtctx/stmtctx.go
  • tests/integrationtest/r/executor/show.result
  • tests/integrationtest/r/planner/core/fulltext_search.result
  • tests/integrationtest/t/planner/core/fulltext_search.test

Comment thread .gitignore
Comment on lines +40 to +67
<<<<<<< HEAD
*.log.json
genkeyword
test_coverage
coverage.dat
=======

# Integration tests
tests/integrationtest/integration-test.out
tests/integrationtest/integrationtest_tidb-server
tests/integrationtest/s/
tests/integrationtest/replayer/

# Local dev artifacts
bench_daily.json
compose-dev.yaml
fix.sql
export-20*/
var

# Personal config files
/*config.toml
.cache

# Claude Code runtime state (per-user, not part of repo)
.claude/scheduled_tasks.lock
.claude/settings.local.json
>>>>>>> f96cd1c2fd5 (planner: rewrite FTS predicates to LIKE for evaluation of non-TiCI query plan (#65626))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resolve the leftover cherry-pick conflict in .gitignore.

This hunk still contains Git conflict markers and both sides of the merge. Please collapse it to the intended final ignore list before merging; otherwise the file stays corrupted and the added ignore rules are not reviewable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore around lines 40 - 67, The .gitignore contains leftover merge
conflict markers (<<<<<<< HEAD, =======, >>>>>>>) and both sets of entries;
remove the conflict markers and merge the two sides into the intended final
ignore list by choosing or combining the desired lines (e.g., keep the
consolidated ignores such as *.log.json, genkeyword, test_coverage, coverage.dat
plus the integration tests, local dev artifacts, personal config files, and
.claude entries) so the file is clean and contains only valid ignore patterns
with no conflict markers.

Comment on lines +63 to +67
<<<<<<< HEAD
=======
"fts_helper.go",
"fts_to_like.go",
>>>>>>> f96cd1c2fd5 (planner: rewrite FTS predicates to LIKE for evaluation of non-TiCI query plan (#65626))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix this Bazel srcs block before merge.

This hunk still has cherry-pick markers, and builtin_fts.go is also missing from go_library.srcs. As written, Bazel will either fail to parse the BUILD file or fail to compile pkg/expression once the FTS registrations reference the new types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/expression/BUILD.bazel` around lines 63 - 67, The BUILD.bazel srcs block
contains unresolved cherry-pick conflict markers (<<<<<<< HEAD / ======= /
>>>>>>>) and is missing builtin_fts.go which will break compilation; remove the
conflict markers from the go_library.srcs list, ensure the entries include
"fts_helper.go", "fts_to_like.go", and add "builtin_fts.go" to the srcs for the
pkg/expression go_library so Bazel can parse the BUILD file and compile the
package.

Comment thread pkg/expression/builtin.go
Comment on lines +972 to +978
<<<<<<< HEAD
=======
// fts functions
ast.FTSMatchWord: &ftsMatchWordFunctionClass{baseFunctionClass{ast.FTSMatchWord, 2, 2}},
ast.FTSMysqlMatchAgainst: &ftsMysqlMatchAgainstFunctionClass{baseFunctionClass{ast.FTSMysqlMatchAgainst, 2, -1}},

>>>>>>> f96cd1c2fd5 (planner: rewrite FTS predicates to LIKE for evaluation of non-TiCI query plan (#65626))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove the unresolved conflict markers in the builtin registry.

The funcs map still includes cherry-pick markers here, which makes pkg/expression fail to build.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/expression/builtin.go` around lines 972 - 978, There are leftover git
conflict markers in the funcs registry; remove the markers (<<<<<<< HEAD and
>>>>>>> ...) and ensure the two entries for ast.FTSMatchWord ->
ftsMatchWordFunctionClass and ast.FTSMysqlMatchAgainst ->
ftsMysqlMatchAgainstFunctionClass remain correctly placed in the funcs map with
proper commas and no conflict text so pkg/expression builds cleanly.

Comment on lines +1152 to +1163
<<<<<<< HEAD
=======
case tipb.ScalarFuncSig_FTSMatchWord:
f = &builtinFtsMatchWordSig{base}
case tipb.ScalarFuncSig_FTSMatchExpression:
// NOTE: builtinFtsMysqlMatchAgainstSig.modifier is not serialized in the
// protobuf encoding because the tipb schema has no FTS metadata message.
// The reconstructed sig therefore uses the zero modifier value
// (FulltextSearchModifierNaturalLanguageMode). TiFlash must derive the
// search mode from other context when executing this expression.
f = &builtinFtsMysqlMatchAgainstSig{baseBuiltinFunc: base}
>>>>>>> f96cd1c2fd5 (planner: rewrite FTS predicates to LIKE for evaluation of non-TiCI query plan (#65626))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Clean up the cherry-pick markers in the DistSQL signature switch.

The unresolved <<<<<<< / ======= / >>>>>>> text leaves this switch syntactically invalid and blocks compilation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/expression/distsql_builtin.go` around lines 1152 - 1163, Remove the
leftover git conflict markers (<<<<<<<, =======, >>>>>>>) in the DistSQL scalar
function switch and restore the two FTS cases so the switch compiles: include
the case tipb.ScalarFuncSig_FTSMatchWord with f = &builtinFtsMatchWordSig{base}
and the case tipb.ScalarFuncSig_FTSMatchExpression with f =
&builtinFtsMysqlMatchAgainstSig{baseBuiltinFunc: base} (or match the surrounding
cases' struct field style), ensuring there are no conflict markers left and the
switch entries follow the existing switch syntax.

Comment on lines +67 to +73
// isFTSWordByte returns true for alphanumeric ASCII and non-ASCII bytes.
// Punctuation including underscore is NOT a word character, consistent with
// MySQL's built-in FTS tokenizer which treats _ as a word separator. Used by
// ValidateFTSSearchStringForLikeFallback to gate the LIKE rewrite.
func isFTSWordByte(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c > 127
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tighten the non-ASCII token validation.

isFTSWordByte currently accepts any byte > 127, so terms containing non-ASCII punctuation/symbols will pass the “strict subset” gate and get rewritten to ILIKE. For example, a token with full-width punctuation is not alphanumeric, but this validator will still accept it byte-by-byte. That widens the fallback beyond the PR contract and can produce incorrect rewrites instead of leaving the query on the native/error path.

Use rune-level validation for letters/digits (or the repo’s tokenizer equivalent) instead of treating every non-ASCII byte as a word character.

Also applies to: 119-141

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/expression/fts_to_like.go` around lines 67 - 73, isFTSWordByte wrongly
treats any byte >127 as a word character; change the logic to perform rune-level
validation using unicode properties (e.g., unicode.IsLetter/unicode.IsDigit) or
the repo's tokenizer utility instead of byte checks. Replace isFTSWordByte with
a rune-based checker (or add isFTSRune) and update callers such as
ValidateFTSSearchStringForLikeFallback to iterate over runes (not bytes) and
call the new rune validator so non-ASCII punctuation/symbols are not treated as
word characters.

Comment on lines +322 to +371
<<<<<<< HEAD
// SavedViews is a stack that saves all views when traversing the AST. We depend on it to:
// 1. know whether the AST node is under a view
// 2. report precise error in appendColNamesToVisitInfo.
SavedViews []*ast.TableName
=======
// nonViableFTSMatch is set during build when the expression rewriter
// encounters a predicate-context MATCH...AGAINST whose native form
// (FTSMysqlMatchAgainst) cannot be executed — the matched columns lack a
// public FULLTEXT index on a TiFlash-backed table, or the modifier is not
// supported by pushdown. The flag is read by the alternative-rounds driver
// after the round to invalidate the round's plan and trigger the
// fts-like-fallback round (see optimize.go).
nonViableFTSMatch bool

// predicateMatchSeen is set during build when the expression rewriter
// encounters a direct-boolean-context MATCH...AGAINST (one whose 0/1 boolean
// result is consumed directly as a predicate). The alternative-rounds driver
// uses this to enable the fts-like-fallback round even when round 1's
// native plan is executable, so the LIKE-based plan can compete on cost.
predicateMatchSeen bool
}

// HasNonViableFTSMatch reports whether the most recent build round saw a
// predicate-context MATCH...AGAINST that could not be served by the native
// FTSMysqlMatchAgainst builtin. The caller (optimize.go) uses this to
// invalidate the round's plan and trigger the fts-like-fallback round.
func (b *PlanBuilder) HasNonViableFTSMatch() bool {
return b.nonViableFTSMatch
}

// MarkNonViableFTSMatch records that a predicate-context MATCH...AGAINST in
// the current build cannot be served natively. See HasNonViableFTSMatch.
func (b *PlanBuilder) MarkNonViableFTSMatch() {
b.nonViableFTSMatch = true
}

// HasPredicateMatch reports whether the most recent build round saw a
// direct-boolean-context MATCH...AGAINST. The caller (optimize.go) uses this
// to decide whether to run the fts-like-fallback round for cost competition,
// independent of whether round 1's native plan is executable.
func (b *PlanBuilder) HasPredicateMatch() bool {
return b.predicateMatchSeen
}

// MarkPredicateMatch records that the current build encountered a
// direct-boolean-context MATCH...AGAINST. See HasPredicateMatch.
func (b *PlanBuilder) MarkPredicateMatch() {
b.predicateMatchSeen = true
>>>>>>> f96cd1c2fd5 (planner: rewrite FTS predicates to LIKE for evaluation of non-TiCI query plan (#65626))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Resolve the cherry-pick conflict before merge.

This hunk still contains Git conflict markers and two competing PlanBuilder definitions, so planbuilder.go is syntactically invalid and won't compile until the conflict is resolved.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/planner/core/planbuilder.go` around lines 322 - 371, The file contains
unresolved Git conflict markers in the PlanBuilder type area; remove the
conflict markers (<<<<<<<, =======, >>>>>>>) and reconcile the two competing
blocks by merging the SavedViews field with the new FTS-related fields
(nonViableFTSMatch, predicateMatchSeen) into a single PlanBuilder struct, and
keep the accompanying accessor/mutator methods (HasNonViableFTSMatch,
MarkNonViableFTSMatch, HasPredicateMatch, MarkPredicateMatch) only once; ensure
PlanBuilder now declares SavedViews []*ast.TableName plus the two boolean fields
and that the added methods reference that single struct definition so the file
compiles.

Comment thread pkg/planner/optimize.go
Comment on lines +469 to +703
<<<<<<< HEAD
// optimizeCnt is a global variable only used for test.
var optimizeCnt int

=======
type logicalPlanBuildCtx struct {
stmtCtxState stmtctx.LogicalPlanBuildState
plannerSelectBlockAsName *[]ast.HintTable
mapScalarSubQ []any
mapHashCode2UniqueID map[string]int
rewritePhaseInfo variable.RewritePhaseInfo
}

func saveLogicalPlanBuildCtx(sessVars *variable.SessionVars) logicalPlanBuildCtx {
return logicalPlanBuildCtx{
stmtCtxState: sessVars.StmtCtx.SaveLogicalPlanBuildState(),
plannerSelectBlockAsName: sessVars.PlannerSelectBlockAsName.Load(),
mapScalarSubQ: sessVars.MapScalarSubQ,
mapHashCode2UniqueID: sessVars.MapHashCode2UniqueID4ExtendedCol,
rewritePhaseInfo: sessVars.RewritePhaseInfo,
}
}

func restoreLogicalPlanBuildCtx(sessVars *variable.SessionVars, logicalPlanCtx logicalPlanBuildCtx) {
sessVars.StmtCtx.RestoreLogicalPlanBuildState(logicalPlanCtx.stmtCtxState)
sessVars.PlannerSelectBlockAsName.Store(logicalPlanCtx.plannerSelectBlockAsName)
sessVars.MapScalarSubQ = logicalPlanCtx.mapScalarSubQ
sessVars.MapHashCode2UniqueID4ExtendedCol = logicalPlanCtx.mapHashCode2UniqueID
sessVars.RewritePhaseInfo = logicalPlanCtx.rewritePhaseInfo
}

func buildAndOptimizeLogicalPlanRound(
ctx context.Context,
sctx planctx.PlanContext,
node *resolve.NodeW,
is infoschema.InfoSchema,
hintProcessor *hint.QBHintHandler,
checked *bool,
optimizeStarted *bool,
beginOpt *time.Time,
needRestoreLogicalPlanCtx bool,
bestPlan *base.PhysicalPlan,
bestNames *types.NameSlice,
bestCost *float64,
bestLogicalPlanCtx *logicalPlanBuildCtx,
optFlagAdjust func(uint64) uint64,
) (base.Plan, types.NameSlice, bool, error) {
builder := planBuilderPool.Get().(*core.PlanBuilder)
defer planBuilderPool.Put(builder.ResetForReuse())
// TODO: when buildRound > 1, only emit unused view-hint warnings for the winner build.
defer builder.HandleUnusedViewHints()

builder.Init(sctx, is, hintProcessor)

// todo: you can customize each round's special builder (like semi join rewrite or not by signal)
p, err := buildLogicalPlan(ctx, sctx, node, builder)
if err != nil {
return nil, nil, false, err
}
names := p.OutputNames()

if !*checked {
// Keep privilege and lock checks fail-fast. These depend on visitInfo
// produced by the logical build, but not on the later cost winner.
if pm := privilege.GetPrivilegeManager(sctx); pm != nil {
visitInfo := core.VisitInfo4PrivCheck(ctx, is, node.Node, builder.GetVisitInfo())
if err := core.CheckPrivilege(sctx.GetSessionVars().ActiveRoles, pm, visitInfo); err != nil {
return nil, nil, false, err
}
}

if err := core.CheckTableLock(sctx, is, builder.GetVisitInfo()); err != nil {
return nil, nil, false, err
}

if err := core.CheckTableMode(node); err != nil {
return nil, nil, false, err
}
*checked = true
}

// Handle the non-logical plan statement.
logic, isLogicalPlan := p.(base.LogicalPlan)
if !isLogicalPlan {
return p, names, true, nil
}

core.RecheckCTE(logic)

// todo: also you can customize each round's special logical opt flag here (like decorrelate rule or not)
if !*optimizeStarted {
*optimizeStarted = true
*beginOpt = time.Now()
}
optFlag := builder.GetOptFlag()
if sctx.GetSessionVars().EnableAlternativeLogicalPlans &&
optFlag&rule.FlagPushDownTopN > 0 &&
optFlag&rule.FlagJoinReOrder > 0 {
sctx.GetSessionVars().StmtCtx.MarkAlternativeLogicalPlanOrderAwareJoinReorder()
}
if optFlagAdjust != nil {
optFlag = optFlagAdjust(optFlag)
}
finalPlan, cost, err := core.DoOptimize(ctx, sctx, optFlag, logic)
if err != nil {
return nil, nil, false, err
}

// Record predicate-context MATCH for cost competition. The fts-like-fallback
// alternative round reads this signal to decide whether to build a competing
// ILIKE-based plan alongside round 1's native plan, so the cheaper of the
// two wins via the normal alt-rounds cost comparison.
if builder.HasPredicateMatch() {
sctx.GetSessionVars().StmtCtx.AlternativeLogicalPlanHasPredicateContextMatch = true
}

// If this round saw a predicate-context MATCH that cannot be served by the
// native FTSMysqlMatchAgainst builtin, the produced plan would fail at
// execution. Discard it and arm AlternativeLogicalPlanFTSLikeFallback so
// any intervening rounds (correlate, etc.) re-rewrite with ILIKE too. The
// fts-like-fallback round below also forces this flag during setup; this
// outer assignment covers the non-viable case where the flag must stay
// true across all subsequent rounds, not just inside the LIKE round.
if builder.HasNonViableFTSMatch() {
sctx.GetSessionVars().StmtCtx.AlternativeLogicalPlanFTSLikeFallback = true
return p, names, false, nil
}

if *bestPlan == nil || cost < *bestCost {
*bestCost = cost
*bestPlan = finalPlan
*bestNames = names
if needRestoreLogicalPlanCtx {
*bestLogicalPlanCtx = saveLogicalPlanBuildCtx(sctx.GetSessionVars())
}
}
return p, names, false, nil
}

// optimizeCnt is a global variable only used for test.
var optimizeCnt int

func shouldTryNonDecorrelationRound(sessVars *variable.SessionVars) bool {
return sessVars.EnableAlternativeLogicalPlans &&
sessVars.StmtCtx.AlternativeLogicalPlanDecorrelatedApply &&
!sessVars.StmtCtx.AlternativeLogicalPlanSameOrderIndexJoin
}

func shouldTryOrderAwareReorderRound(sessVars *variable.SessionVars) bool {
return sessVars.EnableAlternativeLogicalPlans &&
sessVars.StmtCtx.AlternativeLogicalPlanOrderAwareJoinReorder
}

func shouldTryCorrelateRound(sessVars *variable.SessionVars) bool {
return sessVars.EnableAlternativeLogicalPlans &&
sessVars.StmtCtx.AlternativeLogicalPlanPreferCorrelate
}

// alternativeRound describes one alternative logical-plan round.
// adjustFlag adjusts the optimization flags for the round.
// enabled returns true when the round should be attempted.
// setup/cleanup optionally modify session state before/after plan building.
type alternativeRound struct {
name string
adjustFlag func(uint64) uint64
enabled func(*variable.SessionVars) bool
setup func(*variable.SessionVars)
cleanup func(*variable.SessionVars)
}

// savedEnableCorrelateSubquery holds the pre-round value of
// EnableCorrelateSubquery so setup/cleanup can share it without a closure
// wrapper. Safe because optimize is single-threaded per session.
var savedEnableCorrelateSubquery bool

// savedFTSLikeFallback holds the pre-round value of
// AlternativeLogicalPlanFTSLikeFallback so the fts-like-fallback round's
// setup/cleanup can restore it after running with the flag forced on. Safe
// because optimize is single-threaded per session.
var savedFTSLikeFallback bool

var alternativeRounds = [...]alternativeRound{
{
name: "non-decorrelate",
adjustFlag: func(flag uint64) uint64 { return flag &^ rule.FlagDecorrelate },
enabled: shouldTryNonDecorrelationRound,
},
{
name: "order-aware-reorder",
adjustFlag: func(flag uint64) uint64 { return flag | rule.FlagOrderAwareJoinReorder },
enabled: shouldTryOrderAwareReorderRound,
},
{
name: "correlate",
adjustFlag: func(flag uint64) uint64 { return flag | rule.FlagCorrelate },
enabled: shouldTryCorrelateRound,
setup: func(sv *variable.SessionVars) {
savedEnableCorrelateSubquery = sv.EnableCorrelateSubquery
sv.EnableCorrelateSubquery = true
},
cleanup: func(sv *variable.SessionVars) {
sv.EnableCorrelateSubquery = savedEnableCorrelateSubquery
},
},
{
// fts-like-fallback: rebuild the plan rewriting predicate-context
// MATCH...AGAINST to ILIKE so it can compete with round 1's native plan
// on cost (and serve as the only valid plan when native is non-viable).
// Round 1 always uses the native builtin (same as Alt-disabled). This
// round fires whenever round 1 saw a direct-boolean-context MATCH
// (HasPredicateContextMatch) — both plans then compete via the strict-`<`
// cost comparison in buildAndOptimizeLogicalPlanRound — or whenever
// round 1 saw a MATCH whose native form cannot execute
// (FTSLikeFallback, set by the round driver after discarding round 1).
// In the discard case, round 1's plan is unavailable and this round's
// plan wins by default.
name: "fts-like-fallback",
enabled: func(sv *variable.SessionVars) bool {
if !sv.EnableAlternativeLogicalPlans {
return false
}
return sv.StmtCtx.AlternativeLogicalPlanFTSLikeFallback ||
sv.StmtCtx.AlternativeLogicalPlanHasPredicateContextMatch
},
setup: func(sv *variable.SessionVars) {
savedFTSLikeFallback = sv.StmtCtx.AlternativeLogicalPlanFTSLikeFallback
sv.StmtCtx.AlternativeLogicalPlanFTSLikeFallback = true
},
cleanup: func(sv *variable.SessionVars) {
sv.StmtCtx.AlternativeLogicalPlanFTSLikeFallback = savedFTSLikeFallback
},
},
}

>>>>>>> f96cd1c2fd5 (planner: rewrite FTS predicates to LIKE for evaluation of non-TiCI query plan (#65626))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Finish resolving the cherry-pick in this file.

This file still contains merge markers, so it will not compile as-is. It also leaves the new optimizer path half-merged, including references like stmtctx / rule that are not integrated cleanly yet.

Also applies to: 719-899

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/planner/optimize.go` around lines 469 - 703, The file contains unresolved
git conflict markers (<<<<<<<, =======, >>>>>>>) leaving a half-merged optimizer
refactor; remove the conflict markers and produce a single coherent version by
keeping the new logicalPlanBuildCtx, saveLogicalPlanBuildCtx,
restoreLogicalPlanBuildCtx and buildAndOptimizeLogicalPlanRound implementations
(and only one declaration of optimizeCnt), deleting the duplicate old fragment,
and ensure references to stmtctx, rule and any new symbols are correctly
imported/used; verify alternativeRounds and related helpers
(shouldTryNonDecorrelationRound, shouldTryOrderAwareReorderRound,
shouldTryCorrelateRound, savedEnableCorrelateSubquery, savedFTSLikeFallback) are
present and consistent with the rest of the file so the file compiles.

Comment thread pkg/planner/optimize.go
Comment on lines +639 to +699
// savedEnableCorrelateSubquery holds the pre-round value of
// EnableCorrelateSubquery so setup/cleanup can share it without a closure
// wrapper. Safe because optimize is single-threaded per session.
var savedEnableCorrelateSubquery bool

// savedFTSLikeFallback holds the pre-round value of
// AlternativeLogicalPlanFTSLikeFallback so the fts-like-fallback round's
// setup/cleanup can restore it after running with the flag forced on. Safe
// because optimize is single-threaded per session.
var savedFTSLikeFallback bool

var alternativeRounds = [...]alternativeRound{
{
name: "non-decorrelate",
adjustFlag: func(flag uint64) uint64 { return flag &^ rule.FlagDecorrelate },
enabled: shouldTryNonDecorrelationRound,
},
{
name: "order-aware-reorder",
adjustFlag: func(flag uint64) uint64 { return flag | rule.FlagOrderAwareJoinReorder },
enabled: shouldTryOrderAwareReorderRound,
},
{
name: "correlate",
adjustFlag: func(flag uint64) uint64 { return flag | rule.FlagCorrelate },
enabled: shouldTryCorrelateRound,
setup: func(sv *variable.SessionVars) {
savedEnableCorrelateSubquery = sv.EnableCorrelateSubquery
sv.EnableCorrelateSubquery = true
},
cleanup: func(sv *variable.SessionVars) {
sv.EnableCorrelateSubquery = savedEnableCorrelateSubquery
},
},
{
// fts-like-fallback: rebuild the plan rewriting predicate-context
// MATCH...AGAINST to ILIKE so it can compete with round 1's native plan
// on cost (and serve as the only valid plan when native is non-viable).
// Round 1 always uses the native builtin (same as Alt-disabled). This
// round fires whenever round 1 saw a direct-boolean-context MATCH
// (HasPredicateContextMatch) — both plans then compete via the strict-`<`
// cost comparison in buildAndOptimizeLogicalPlanRound — or whenever
// round 1 saw a MATCH whose native form cannot execute
// (FTSLikeFallback, set by the round driver after discarding round 1).
// In the discard case, round 1's plan is unavailable and this round's
// plan wins by default.
name: "fts-like-fallback",
enabled: func(sv *variable.SessionVars) bool {
if !sv.EnableAlternativeLogicalPlans {
return false
}
return sv.StmtCtx.AlternativeLogicalPlanFTSLikeFallback ||
sv.StmtCtx.AlternativeLogicalPlanHasPredicateContextMatch
},
setup: func(sv *variable.SessionVars) {
savedFTSLikeFallback = sv.StmtCtx.AlternativeLogicalPlanFTSLikeFallback
sv.StmtCtx.AlternativeLogicalPlanFTSLikeFallback = true
},
cleanup: func(sv *variable.SessionVars) {
sv.StmtCtx.AlternativeLogicalPlanFTSLikeFallback = savedFTSLikeFallback
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Keep per-round saved state out of package globals.

savedEnableCorrelateSubquery and savedFTSLikeFallback are shared across all sessions. Two concurrent Optimize calls can overwrite each other's saved values and restore the wrong session state during cleanup, which is both a race and a correctness bug. Make this saved state per-optimize invocation instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/planner/optimize.go` around lines 639 - 699, The two package-level flags
savedEnableCorrelateSubquery and savedFTSLikeFallback are unsafe because they
are shared across sessions; make the saved state local to each optimize
invocation by removing those globals and storing the saved values per-round
instead (either add fields like
savedEnableCorrelateSubquery/savedFTSLikeFallback to the alternativeRound struct
or rebuild alternativeRounds inside optimize() so each round’s setup/cleanup
closures capture local variables). Update the correlate round’s setup/cleanup
and the fts-like-fallback round’s setup/cleanup to read/write the saved value
from the per-round storage (or captured local) and ensure optimize() uses the
per-invocation alternativeRounds so concurrent Optimize calls don’t overwrite
each other.

Comment on lines +449 to +486
<<<<<<< HEAD
=======
// AlternativeLogicalPlanDecorrelatedApply indicates whether the current logical
// optimization round decorrelated at least one Apply into Join.
AlternativeLogicalPlanDecorrelatedApply bool
// AlternativeLogicalPlanSameOrderIndexJoin indicates whether the current first
// round already produced a same-order index join candidate for a decorrelated Apply.
AlternativeLogicalPlanSameOrderIndexJoin bool
// AlternativeLogicalPlanOrderAwareJoinReorder indicates whether at least one
// logical build round produced an order-aware join reorder candidate that is
// worth exploring in a dedicated alternative round.
AlternativeLogicalPlanOrderAwareJoinReorder bool
// AlternativeLogicalPlanPreferCorrelate indicates whether the current logical
// build round encountered a non-correlated IN subquery eligible for the
// correlate-to-Apply alternative.
AlternativeLogicalPlanPreferCorrelate bool
// AlternativeLogicalPlanFTSLikeFallback is a mode flag controlling how the
// expression rewriter handles MATCH...AGAINST in predicate contexts. When
// false (the default, matching Alt-disabled behavior) the rewriter emits
// the native FTSMysqlMatchAgainst builtin. When true, the rewriter emits
// ILIKE-based predicates instead.
//
// Round 1 always runs with this flag false. The "fts-like-fallback"
// alternative round flips it to true (via its setup/cleanup) while it
// builds a competing ILIKE-based plan; the cost-cheapest plan wins via the
// normal alt-rounds cost comparison. If round 1's build records a
// predicate-context MATCH that cannot be served natively (no FTS index on a
// matched column / no TiFlash replica / modifier not pushdown-supported),
// optimize.go additionally invalidates round 1's plan and forces this flag
// true outside the round so any intervening rounds (correlate, etc.) also
// produce executable LIKE-based plans.
AlternativeLogicalPlanFTSLikeFallback bool
// AlternativeLogicalPlanHasPredicateContextMatch indicates that round 1
// encountered a direct-boolean-context MATCH...AGAINST. The round driver
// uses this to enable the fts-like-fallback round for cost competition even
// when round 1's native plan is executable.
AlternativeLogicalPlanHasPredicateContextMatch bool
>>>>>>> f96cd1c2fd5 (planner: rewrite FTS predicates to LIKE for evaluation of non-TiCI query plan (#65626))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Resolve the leftover cherry-pick conflict markers.

Lines 449 and 613 still contain <<<<<<< / ======= / >>>>>>>, so this file will not parse and the new StatementContext fields/methods are only half-merged.

Also applies to: 613-689

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/sessionctx/stmtctx/stmtctx.go` around lines 449 - 486, The file contains
unresolved git conflict markers (<<<<<<<, =======, >>>>>>>) around the
StatementContext additions, leaving the struct half-merged; remove the conflict
markers and ensure the full set of new fields
(AlternativeLogicalPlanDecorrelatedApply,
AlternativeLogicalPlanSameOrderIndexJoin,
AlternativeLogicalPlanOrderAwareJoinReorder,
AlternativeLogicalPlanPreferCorrelate, AlternativeLogicalPlanFTSLikeFallback,
AlternativeLogicalPlanHasPredicateContextMatch) are present exactly once in the
StatementContext definition (and any related setup/cleanup code blocks), delete
any duplicate or partial blocks from the other branch, and run go build/go vet
to verify the file parses cleanly.

Comment on lines +759 to +763
<<<<<<< HEAD
master_pos_wait
=======
match_against
>>>>>>> f96cd1c2fd5 (planner: rewrite FTS predicates to LIKE for evaluation of non-TiCI query plan (#65626))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix the unresolved merge conflict in the expected result snapshot.

These conflict markers make the SHOW BUILTINS golden output invalid, so the integration test can no longer verify a real result set. Resolve the hunk to the correct builtin list and regenerate/verify the snapshot.

As per coding guidelines, "Integration test files (tests/integrationtest/t/**) changed: record and verify regenerated result correctness".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integrationtest/r/executor/show.result` around lines 759 - 763, The
snapshot contains unresolved merge markers between the builtin names
"master_pos_wait" and "match_against"; open the expected result file
(tests/integrationtest/r/executor/show.result), remove the conflict markers and
choose the correct builtin entry (either keep "master_pos_wait" or
"match_against" as appropriate for the current codebase), then regenerate and
re-run the integration test to verify the SHOW BUILTINS golden output is updated
and committed.

@ti-chi-bot ti-chi-bot Bot added cherry-pick-approved Cherry pick PR approved by release team. and removed do-not-merge/cherry-pick-not-approved labels May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cherry-pick-approved Cherry pick PR approved by release team. do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. ok-to-test Indicates a PR is ready to be tested. release-note-none Denotes a PR that doesn't merit a release note. sig/planner SIG: Planner size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. type/cherry-pick-for-release-8.5 This PR is cherry-picked to release-8.5 from a source PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants