Skip to content

planner: enable cd-c algorithm by default for join reorder | tidb-test=pr/2759#68880

Open
guo-shaoge wants to merge 33 commits into
pingcap:release-8.5from
guo-shaoge:cp_post_cdc_impl
Open

planner: enable cd-c algorithm by default for join reorder | tidb-test=pr/2759#68880
guo-shaoge wants to merge 33 commits into
pingcap:release-8.5from
guo-shaoge:cp_post_cdc_impl

Conversation

@guo-shaoge
Copy link
Copy Markdown
Collaborator

@guo-shaoge guo-shaoge commented Jun 2, 2026

What problem does this PR solve?

Issue Number: close #63887

Problem Summary: manually cherry pick #66349

What changed and how does it work?

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

  • New Features

    • Advanced join-reorder optimizer added (DP + improved greedy) with a session switch to enable it.
  • Tests

    • New join-reorder test suites and a join-order benchmark added; many expected-plan fixtures updated to match new planner behavior.
  • Bug Fixes / Behavior

    • Leading-hint warning behavior refined (DP reports inapplicable leading hints); EXPLAIN/plan outputs show updated operator identifiers and formatting.

Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
@ti-chi-bot ti-chi-bot Bot added do-not-merge/cherry-pick-not-approved release-note-none Denotes a PR that doesn't merit a release note. labels Jun 2, 2026
@ti-chi-bot
Copy link
Copy Markdown

ti-chi-bot Bot commented Jun 2, 2026

This cherry pick PR is for a release branch and has not yet been approved by triage owners.
Adding the do-not-merge/cherry-pick-not-approved label.

To merge this cherry pick:

  1. It must be LGTMed and approved by the reviewers firstly.
  2. For pull requests to TiDB-x branches, it must have no failed tests.
  3. AFTER it has lgtm and approved labels, please wait for the cherry-pick merging approval from triage owners.
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.

@ti-chi-bot ti-chi-bot Bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. sig/planner SIG: Planner labels Jun 2, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Too much diff to scan? Review this PR in Change Stack to start with the highest-impact changes.

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements a new join-order planner (ConflictDetector + DP/greedy algorithms), gates it behind a session var, adds benchmarks, registers CDC join-reorder casetests (inputs + expected outputs), updates test build rules, and regenerates many planner/executor expected fixtures.

Changes

CDC Join Reordering Core Implementation

Layer / File(s) Summary
Conflict Detector Algorithm and Join Graph Construction
pkg/planner/core/joinorder/conflict_detector.go
Implements CD-C: builds join graphs, creates inner/non-inner edges with TES, generates conflict rules, validates connections, aligns conditions, and provides remaining-edge checks.
Greedy and DP Join Order Optimization
pkg/planner/core/joinorder/join_order.go
Adds exported Optimize entrypoint, extractJoinGroup, DP enumeration, greedy connector, cartesian handling, bushy-tree reconstruction, and edge-usage reporting.
Utility Methods and Set Helpers
pkg/util/intset/fast_int_set.go, pkg/planner/core/operator/logicalop/logical_join.go
Adds FastIntSet.GetSmallUInt64() and runtime JoinType constant assertions to ensure integer mapping stability.
Planner Integration
pkg/planner/core/rule_join_reorder.go
When TiDBOptEnableAdvancedJoinReorder is enabled, JoinReOrderSolver delegates to joinorder.Optimize(p).
Build Configuration
pkg/planner/core/joinorder/BUILD.bazel
Adds joinorder go_library and joinorder_test go_test with benchmark embedding and deps.
Conflict Detector Performance Benchmarking
pkg/planner/core/joinorder/bitset_bench_test.go
Benchmark comparing FastIntSet vs bitset.BitSet operations for conflict detection.
Test Infrastructure and Suite Registration
pkg/planner/core/casetest/rule/main_test.go
Registers cdc_join_reorder_suite and exposes GetCDCJoinReorderSuiteData().
CDC Join Reorder Test Implementation
pkg/planner/core/casetest/rule/rule_cdc_join_reorder_test.go
Adds tests: TestCDCJoinReorder, TestJoinReorderPushSelection, TestDPJoinReorder, TestDPJoinReorderLeadingHint.
Test Input Cases
pkg/planner/core/casetest/rule/testdata/cdc_join_reorder_suite_in.json
Adds three test suites: TestCDCJoinReorder, TestJoinReorderPushSelection, TestDPJoinReorder with many SQL cases.
Test Expected Output
pkg/planner/core/casetest/rule/testdata/cdc_join_reorder_suite_out.json
Adds expected plan-tree fragments and result rows for the three suites.
Rule Test Build and Suite Updates
pkg/planner/core/casetest/rule/BUILD.bazel
Includes new test file in rule_test, increases shard_count from 12 to 13, and adds //pkg/testkit/testfailpoint dep.
Updated Regression Test Expectations
many pkg/planner/core/casetest/*/testdata/*_out.json, tests/integrationtest/r/*, pkg/session/bootstraptest/BUILD.bazel
Regenerated fixtures and adjusted bootstrap shard_count; numerous expected plan strings renumbered or reformatted to match new planner output.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant JoinReOrderSolver
  participant joinorder.Optimize
  participant ConflictDetector
  Client->>JoinReOrderSolver: request Optimize(p)
  JoinReOrderSolver->>joinorder.Optimize: delegate when TiDBOptEnableAdvancedJoinReorder=1
  joinorder.Optimize->>ConflictDetector: Build(joinGroup)
  joinorder.Optimize->>joinorder.Optimize: run DP or Greedy optimizer
  joinorder.Optimize->>ConflictDetector: CheckConnection / MakeJoin during enumeration
  joinorder.Optimize->>JoinReOrderSolver: return optimized plan
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pingcap/tidb#68870: Overlaps on joinorder machinery and modifications to rule_join_reorder delegation.
  • pingcap/tidb#68881: Introduces/registers the TiDBOptEnableAdvancedJoinReorder session var used here.
  • pingcap/tidb#66349: Related earlier work touching conflict-detector and join-reorder wiring.

Suggested labels

ok-to-test, needs-cherry-pick-release-8.5

Suggested reviewers

  • qw4990
  • AilinKid
  • winoros

"I hopped through plans with careful art,
Built edges, rules, and a greedy heart.
DP stitched forests, tidy and neat,
Tests now run and benches beat —
Join trees prance, the rabbit's fleet."

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

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.

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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pkg/util/hint/hint.go (1)

955-983: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear LeadingList when the LEADING hint is invalidated.

This block only resets leadingJoinOrder, but the new optimizer path consumes PlanHints.LeadingList directly. With multiple LEADING hints—or LEADING plus STRAIGHT_JOIN—the code still returns the first parsed LeadingList, so join reorder can keep applying a hint after warning that all LEADING hints are invalid.

Suggested fix
 	if leadingHintCnt > 1 || (leadingHintCnt > 0 && straightJoinOrder) {
 		// If there are more leading hints or the straight_join hint existes, all leading hints will be invalid.
 		leadingJoinOrder = leadingJoinOrder[:0]
+		leadingList = nil
 		if leadingHintCnt > 1 {
 			warnHandler.SetHintWarning("We can only use one leading hint at most, when multiple leading hints are used, all leading hints will be invalid")
 		} else if straightJoinOrder {
 			warnHandler.SetHintWarning("We can only use the straight_join hint, when we use the leading hint and straight_join hint at the same time, all leading hints will be invalid")
 		}
 	}
🤖 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/util/hint/hint.go` around lines 955 - 983, The LEADING invalidation
branch currently clears leadingJoinOrder but leaves the parsed leadingList
intact, so PlanHints.LeadingList is still returned; update the branch in the
block that checks (leadingHintCnt > 1 || (leadingHintCnt > 0 &&
straightJoinOrder)) to also clear leadingList (e.g., set leadingList to nil or
leadingList = leadingList[:0]) when you clear leadingJoinOrder, so
PlanHints.LeadingList is empty when LEADING hints are invalidated; keep the
existing warnHandler.SetHintWarning calls as-is.
pkg/planner/core/casetest/rule/BUILD.bazel (1)

20-38: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the missing Bazel dep for testfailpoint.

rule_cdc_join_reorder_test.go imports github.com/pingcap/tidb/pkg/testkit/testfailpoint on Line 22, but rule_test does not list //pkg/testkit/testfailpoint in deps. With rules_go strict deps, this target will fail to compile once the new test file is built.

Suggested fix
     deps = [
         "//pkg/config",
         "//pkg/domain",
         "//pkg/expression",
         "//pkg/expression/aggregation",
         "//pkg/expression/exprstatic",
         "//pkg/parser/ast",
         "//pkg/parser/mysql",
         "//pkg/planner/util/coreusage",
         "//pkg/testkit",
         "//pkg/testkit/testdata",
+        "//pkg/testkit/testfailpoint",
         "//pkg/testkit/testmain",
         "//pkg/testkit/testsetup",
         "//pkg/types",
         "//pkg/util/mock",
         "`@com_github_pingcap_failpoint//`:failpoint",
🤖 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/casetest/rule/BUILD.bazel` around lines 20 - 38, The
BUILD.bazel deps for the rule_test target are missing the testfailpoint package
used by rule_cdc_join_reorder_test.go; update the deps array in
pkg/planner/core/casetest/rule/BUILD.bazel to include the
//pkg/testkit/testfailpoint bazel label so the import in
rule_cdc_join_reorder_test.go resolves under rules_go strict deps.
🧹 Nitpick comments (1)
pkg/planner/core/joinorder/bitset_bench_test.go (1)

78-85: ⚡ Quick win

Benchmark setup only measures the failing TES ⊆ S path.

buildFastEdges/buildBitEdges always put extra into tes, but the benchmark candidate set s is only left ∪ right. That makes tes.SubsetOf(s) / s.IsSuperSet(tes) false for every edge, so the first condition short-circuits and the Intersects / IntersectionCardinality checks from the real checkInnerEdgeApplicable hot path never run. This will skew the comparison toward the failure case instead of the production path in pkg/planner/core/joinorder/conflict_detector.go.

Possible adjustment
-                    s := e.left.Union(e.right)
+                    s := e.left.Union(e.right).Union(e.tes) // or split pass/fail cases into separate sub-benchmarks

Also applies to: 103-110, 140-170

🤖 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/joinorder/bitset_bench_test.go` around lines 78 - 85, The
benchmark currently always puts the `extra` node into `tes` (in
`buildFastEdges`/`buildBitEdges`) while the candidate set `s` used in the
benchmark is only `left ∪ right`, forcing `tes ⊆ s` to fail and only measuring
the failing path; update the benchmark so `s` includes `extra` (or alternatively
remove `extra` from how `tes` is constructed) so that
`tes.SubsetOf(s)`/`s.IsSuperSet(tes)` can succeed and the hot path in
`checkInnerEdgeApplicable` (the `Intersects`/`IntersectionCardinality` checks)
is exercised. Use the unique symbols `tes`, `extra`, `left`, `right`,
`buildFastEdges`/`buildBitEdges`, and `checkInnerEdgeApplicable` to locate and
adjust the setup.
🤖 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 `@pkg/parser/ast/misc.go`:
- Around line 3967-4005: RestoreWithQB currently forwards the query-block name
into nested LeadingList items, which moves the `@qb` inside tuples; change the
logic in LeadingList.RestoreWithQB so that if i==0 and currentQBName.L != "" and
not qbOnTable and the item is a *LeadingList, you emit the "@<qb>" before
restoring the nested group (use ctx.WriteKeyWord("@") and
ctx.WriteName(currentQBName.String()) plus a space), then call t.RestoreWithQB
with an empty model.CIStr (clearing the qb for the recursive call) so the marker
stays at the outer list; adjust currentQBName handling accordingly (do not pass
the qb into the recursive call and ensure currentQBName is cleared after
emitting).

In `@pkg/parser/hintparser_test.go`:
- Around line 369-474: The test cases in pkg/parser/hintparser_test.go use
ast.NewCIStr which doesn't exist; replace all uses of ast.NewCIStr with
model.NewCIStr in the table hint expectations (e.g. the LEADING test blocks that
construct ast.TableOptimizerHint, ast.LeadingList, and ast.HintTable) and update
imports to include the model package (removing or adjusting the ast import if it
becomes unused) so the file compiles.

In `@pkg/planner/core/casetest/rule/testdata/cdc_join_reorder_suite_in.json`:
- Around line 5-245: The fixture contains JavaScript-style inline comments
(lines starting with "//", e.g. the group headings like "Group 1: Inner join
reorder (baseline)" and inline notes) which make the file invalid JSON; remove
all "// ..." comments throughout the file so only valid JSON remains (keep the
array of SQL string literals such as "SELECT * FROM t1 JOIN t2 ON t1.a = t2.a
..." and the surrounding JSON punctuation), ensuring no stray comment characters
remain and the file parses as strict JSON.

In `@pkg/planner/core/joinorder/conflict_detector.go`:
- Around line 827-846: The appended post-join Selection built from
checkResult.appliedInnerEdges must have its NOT_NULL metadata realigned before
being attached to existingJoin; for each condition produced (from
expression.ScalarFuncs2Exprs(e.eqConds) and e.nonEQConds) call
alignNotNullWithSchema using the null-producing child's schema (the child schema
used when the outer join was reordered) so NOT_NULL flags reflect the current
child schemas, then Init the logicalop.LogicalSelection (as now) and
SetChildren(existingJoin) and return it.

In `@pkg/planner/core/joinorder/join_order.go`:
- Line 100: The code currently checks join.JoinType against
base.LeftOuterJoin/base.RightOuterJoin; update the condition to compare
join.JoinType to logicalop.LeftOuterJoin and logicalop.RightOuterJoin instead.
Specifically in the gate that reads
p.SCtx().GetSessionVars().EnableOuterJoinReorder && (join.JoinType == ...),
replace references to base.LeftOuterJoin and base.RightOuterJoin with
logicalop.LeftOuterJoin and logicalop.RightOuterJoin so the join-type
comparisons use the correct constants.

---

Outside diff comments:
In `@pkg/planner/core/casetest/rule/BUILD.bazel`:
- Around line 20-38: The BUILD.bazel deps for the rule_test target are missing
the testfailpoint package used by rule_cdc_join_reorder_test.go; update the deps
array in pkg/planner/core/casetest/rule/BUILD.bazel to include the
//pkg/testkit/testfailpoint bazel label so the import in
rule_cdc_join_reorder_test.go resolves under rules_go strict deps.

In `@pkg/util/hint/hint.go`:
- Around line 955-983: The LEADING invalidation branch currently clears
leadingJoinOrder but leaves the parsed leadingList intact, so
PlanHints.LeadingList is still returned; update the branch in the block that
checks (leadingHintCnt > 1 || (leadingHintCnt > 0 && straightJoinOrder)) to also
clear leadingList (e.g., set leadingList to nil or leadingList =
leadingList[:0]) when you clear leadingJoinOrder, so PlanHints.LeadingList is
empty when LEADING hints are invalidated; keep the existing
warnHandler.SetHintWarning calls as-is.

---

Nitpick comments:
In `@pkg/planner/core/joinorder/bitset_bench_test.go`:
- Around line 78-85: The benchmark currently always puts the `extra` node into
`tes` (in `buildFastEdges`/`buildBitEdges`) while the candidate set `s` used in
the benchmark is only `left ∪ right`, forcing `tes ⊆ s` to fail and only
measuring the failing path; update the benchmark so `s` includes `extra` (or
alternatively remove `extra` from how `tes` is constructed) so that
`tes.SubsetOf(s)`/`s.IsSuperSet(tes)` can succeed and the hot path in
`checkInnerEdgeApplicable` (the `Intersects`/`IntersectionCardinality` checks)
is exercised. Use the unique symbols `tes`, `extra`, `left`, `right`,
`buildFastEdges`/`buildBitEdges`, and `checkInnerEdgeApplicable` to locate and
adjust the setup.
🪄 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: f5c32c72-29ea-456f-8f77-5335e2446935

📥 Commits

Reviewing files that changed from the base of the PR and between beff77b and 96dcc8a.

📒 Files selected for processing (24)
  • pkg/expression/schema.go
  • pkg/parser/ast/misc.go
  • pkg/parser/ast/misc_test.go
  • pkg/parser/hintparser.go
  • pkg/parser/hintparser.y
  • pkg/parser/hintparser_test.go
  • pkg/parser/parser_test.go
  • pkg/planner/core/BUILD.bazel
  • pkg/planner/core/casetest/rule/BUILD.bazel
  • pkg/planner/core/casetest/rule/main_test.go
  • pkg/planner/core/casetest/rule/rule_cdc_join_reorder_test.go
  • pkg/planner/core/casetest/rule/testdata/cdc_join_reorder_suite_in.json
  • pkg/planner/core/casetest/rule/testdata/cdc_join_reorder_suite_out.json
  • pkg/planner/core/casetest/rule/testdata/cdc_join_reorder_suite_xut.json
  • pkg/planner/core/joinorder/BUILD.bazel
  • pkg/planner/core/joinorder/bitset_bench_test.go
  • pkg/planner/core/joinorder/conflict_detector.go
  • pkg/planner/core/joinorder/join_order.go
  • pkg/planner/core/joinorder/util.go
  • pkg/planner/core/operator/logicalop/logical_join.go
  • pkg/planner/core/operator/logicalop/logical_projection.go
  • pkg/planner/core/plan_cost_ver2.go
  • pkg/planner/core/rule_join_reorder.go
  • pkg/util/hint/hint.go

Comment thread pkg/parser/ast/misc.go
Comment on lines +3967 to +4005
func (lt *LeadingList) RestoreWithQB(ctx *format.RestoreCtx, qbName model.CIStr, needParen bool, isTop bool, qbOnTable bool) error {
if lt == nil || len(lt.Items) == 0 {
return nil
}
if needParen {
ctx.WritePlain("(")
}

currentQBName := qbName // hint level QBName

for i, item := range lt.Items {
if i > 0 {
ctx.WritePlain(", ")
}

switch t := item.(type) {
case *HintTable:
if i == 0 && currentQBName.L != "" && !qbOnTable {
ctx.WriteKeyWord("@")
ctx.WriteName(currentQBName.String())
ctx.WritePlain(" ")
t.Restore(ctx)
currentQBName = model.CIStr{}
} else {
t.Restore(ctx)
}
case *LeadingList:
if err := t.RestoreWithQB(ctx, currentQBName, true, false, qbOnTable); err != nil {
return err
}
currentQBName = model.CIStr{}
default:
return fmt.Errorf("unexpected type in LeadingList: %T", t)
}
}
if needParen {
ctx.WritePlain(")")
}
return nil
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

Preserve hint-level QB placement when the first LEADING item is a nested group.

RestoreWithQB currently pushes @qb down to the first HintTable it sees. For LEADING(@sel1 (t1, t2), t3), that restores as LEADING((@sel1 t1, t2), t3), but pkg/parser/hintparser.y only parses the query-block marker before the whole leading list, not inside a nested tuple. That breaks restore/parse round-tripping for a valid hint.

Proposed fix
 func (lt *LeadingList) RestoreWithQB(ctx *format.RestoreCtx, qbName model.CIStr, needParen bool, isTop bool, qbOnTable bool) error {
 	if lt == nil || len(lt.Items) == 0 {
 		return nil
 	}
 	if needParen {
 		ctx.WritePlain("(")
 	}

 	currentQBName := qbName // hint level QBName

 	for i, item := range lt.Items {
 		if i > 0 {
 			ctx.WritePlain(", ")
 		}
+		if i == 0 && currentQBName.L != "" && !qbOnTable {
+			ctx.WriteKeyWord("@")
+			ctx.WriteName(currentQBName.String())
+			ctx.WritePlain(" ")
+			currentQBName = model.CIStr{}
+		}

 		switch t := item.(type) {
 		case *HintTable:
-			if i == 0 && currentQBName.L != "" && !qbOnTable {
-				ctx.WriteKeyWord("@")
-				ctx.WriteName(currentQBName.String())
-				ctx.WritePlain(" ")
-				t.Restore(ctx)
-				currentQBName = model.CIStr{}
-			} else {
-				t.Restore(ctx)
-			}
+			t.Restore(ctx)
 		case *LeadingList:
-			if err := t.RestoreWithQB(ctx, currentQBName, true, false, qbOnTable); err != nil {
+			if err := t.RestoreWithQB(ctx, model.CIStr{}, true, false, qbOnTable); err != nil {
 				return err
 			}
-			currentQBName = model.CIStr{}
 		default:
 			return fmt.Errorf("unexpected type in LeadingList: %T", t)
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (lt *LeadingList) RestoreWithQB(ctx *format.RestoreCtx, qbName model.CIStr, needParen bool, isTop bool, qbOnTable bool) error {
if lt == nil || len(lt.Items) == 0 {
return nil
}
if needParen {
ctx.WritePlain("(")
}
currentQBName := qbName // hint level QBName
for i, item := range lt.Items {
if i > 0 {
ctx.WritePlain(", ")
}
switch t := item.(type) {
case *HintTable:
if i == 0 && currentQBName.L != "" && !qbOnTable {
ctx.WriteKeyWord("@")
ctx.WriteName(currentQBName.String())
ctx.WritePlain(" ")
t.Restore(ctx)
currentQBName = model.CIStr{}
} else {
t.Restore(ctx)
}
case *LeadingList:
if err := t.RestoreWithQB(ctx, currentQBName, true, false, qbOnTable); err != nil {
return err
}
currentQBName = model.CIStr{}
default:
return fmt.Errorf("unexpected type in LeadingList: %T", t)
}
}
if needParen {
ctx.WritePlain(")")
}
return nil
func (lt *LeadingList) RestoreWithQB(ctx *format.RestoreCtx, qbName model.CIStr, needParen bool, isTop bool, qbOnTable bool) error {
if lt == nil || len(lt.Items) == 0 {
return nil
}
if needParen {
ctx.WritePlain("(")
}
currentQBName := qbName // hint level QBName
for i, item := range lt.Items {
if i > 0 {
ctx.WritePlain(", ")
}
if i == 0 && currentQBName.L != "" && !qbOnTable {
ctx.WriteKeyWord("@")
ctx.WriteName(currentQBName.String())
ctx.WritePlain(" ")
currentQBName = model.CIStr{}
}
switch t := item.(type) {
case *HintTable:
t.Restore(ctx)
case *LeadingList:
if err := t.RestoreWithQB(ctx, model.CIStr{}, true, false, qbOnTable); err != nil {
return err
}
default:
return fmt.Errorf("unexpected type in LeadingList: %T", t)
}
}
if needParen {
ctx.WritePlain(")")
}
return nil
}
🤖 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/parser/ast/misc.go` around lines 3967 - 4005, RestoreWithQB currently
forwards the query-block name into nested LeadingList items, which moves the `@qb`
inside tuples; change the logic in LeadingList.RestoreWithQB so that if i==0 and
currentQBName.L != "" and not qbOnTable and the item is a *LeadingList, you emit
the "@<qb>" before restoring the nested group (use ctx.WriteKeyWord("@") and
ctx.WriteName(currentQBName.String()) plus a space), then call t.RestoreWithQB
with an empty model.CIStr (clearing the qb for the recursive call) so the marker
stays at the outer list; adjust currentQBName handling accordingly (do not pass
the qb into the recursive call and ensure currentQBName is cleared after
emitting).

Comment thread pkg/parser/hintparser_test.go
Comment on lines +5 to +245
// =============================================
// Group 1: Inner join reorder (baseline)
// =============================================

// 3-table inner join
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a JOIN t3 ON t2.a = t3.a ORDER BY t1.a",
// 4-table inner join
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a JOIN t3 ON t2.a = t3.a JOIN t4 ON t3.a = t4.a ORDER BY t1.a",

// =============================================
// Group 2: LEFT JOIN + INNER JOIN
// =============================================

// assoc(inner, left) = 1: (t1 INNER t2) LEFT t3
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t2.a = t3.a ORDER BY t1.a, t2.a",
// assoc(left, inner) = 0: (t1 LEFT t2) INNER t3 on t1.a = t3.a
// A conflict rule should be added to prevent incorrect reorder.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a JOIN t3 ON t1.a = t3.a ORDER BY t1.a, t3.a",
// leftAsscom(left, inner) = 1: (t1 LEFT t2) INNER t3 on t2.a = t3.a
// leftAsscom allows reorder when the inner join condition references the right side of the left join.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a JOIN t3 ON t2.a = t3.a ORDER BY t1.a, t2.a, t3.a",

// =============================================
// Group 3: RIGHT JOIN + INNER JOIN
// =============================================

// assoc(right, inner) = 1: (t1 RIGHT t2) INNER t3
"SELECT * FROM t1 RIGHT JOIN t2 ON t1.a = t2.a JOIN t3 ON t2.a = t3.a ORDER BY t2.a, t3.a",
// assoc(inner, right) = 0: (t1 INNER t2) RIGHT t3
// A conflict rule prevents incorrect reorder.
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a RIGHT JOIN t3 ON t2.a = t3.a ORDER BY t3.a",

// =============================================
// Group 4: Nested LEFT JOINs
// =============================================

// assoc(left, left) = 1: (t1 LEFT t2) LEFT t3 on t2.a = t3.a
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t2.a = t3.a ORDER BY t1.a",
// leftAsscom(left, left) = 1: (t1 LEFT t2) LEFT t3 on t1.a = t3.a
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t1.a = t3.a ORDER BY t1.a",

// =============================================
// Group 5: Nested RIGHT JOINs
// =============================================

// assoc(right, right) = 1: (t1 RIGHT t2) RIGHT t3 on t2.a = t3.a
"SELECT * FROM t1 RIGHT JOIN t2 ON t1.a = t2.a RIGHT JOIN t3 ON t2.a = t3.a ORDER BY t3.a",

// =============================================
// Group 6: LEFT + RIGHT mixed
// =============================================

// assoc(right, left) = 1: (t1 RIGHT t2) LEFT t3 on t2.a = t3.a
"SELECT * FROM t1 RIGHT JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t2.a = t3.a ORDER BY t2.a",
// assoc(left, right) = 0: (t1 LEFT t2) RIGHT t3 on t1.a = t3.a
// Conflict rule should prevent incorrect reorder.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a RIGHT JOIN t3 ON t1.a = t3.a ORDER BY t3.a",

// =============================================
// Group 7: rightAsscom(right, left) = 1
// =============================================

// rightAsscom(right, left) = 1: t1 RIGHT (t2 LEFT t3)
// In SQL: t2 LEFT t3 is a subgroup; t1 RIGHT JOIN that.
// We write it as: t1 RIGHT JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t1.a = t3.a
// Parse tree: (t1 RIGHT t2) LEFT t3
// Use a different structure to test rightAsscom:
// t2 LEFT t3 ON t2.a = t3.a, then t1 RIGHT t2 ON t1.b = t2.b
// This is achieved via: t1 RIGHT JOIN (t2 LEFT JOIN t3 ON t2.a = t3.a) ON t1.a = t2.a
// But MySQL syntax doesn't allow this directly. Use:
"SELECT * FROM t2 LEFT JOIN t3 ON t2.a = t3.a RIGHT JOIN t1 ON t2.a = t1.a ORDER BY t1.a",

// =============================================
// Group 8: Complex 4-table mixed outer joins
// =============================================

// LEFT + INNER + LEFT: (t1 LEFT t2) INNER t3 LEFT t4
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a JOIN t3 ON t1.a = t3.a LEFT JOIN t4 ON t3.a = t4.a ORDER BY t1.a",
// RIGHT + INNER + RIGHT: (t1 RIGHT t2) INNER t3 RIGHT t4
"SELECT * FROM t1 RIGHT JOIN t2 ON t1.a = t2.a JOIN t3 ON t2.a = t3.a RIGHT JOIN t4 ON t3.a = t4.a ORDER BY t4.a",
// INNER + LEFT + RIGHT: t1 INNER t2 LEFT t3 RIGHT t4
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t2.a = t3.a RIGHT JOIN t4 ON t3.a = t4.a ORDER BY t4.a",

// =============================================
// Group 9: All inner join with more complex conditions
// =============================================

// Inner join with multiple conditions
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a AND t1.b < t2.b JOIN t3 ON t2.a = t3.a ORDER BY t1.a",
// Inner join chain with different join columns
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a JOIN t3 ON t2.b = t3.b ORDER BY t1.a, t2.a, t3.a",

// =============================================
// Group 10: Join-condition orientation and filters
// =============================================

// Reversed equality orientation to exercise eq condition alignment.
"SELECT * FROM t1 JOIN t2 ON t2.a = t1.a JOIN t3 ON t3.a = t2.a ORDER BY t1.a",
// Inner join with one-side filter in ON condition.
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a AND t2.b > 150 JOIN t3 ON t2.a = t3.a ORDER BY t1.a, t2.a",
// Degenerate predicate (single-side reference) + normal edge.
"SELECT * FROM t1 JOIN t2 ON t1.b > 15 JOIN t3 ON t2.a = t3.a WHERE t1.a = 3 ORDER BY t2.a, t3.a",
// Cross-style inner join with join predicate in WHERE.
"SELECT * FROM t1 JOIN t2 JOIN t3 ON t2.a = t3.a WHERE t1.a = t2.a ORDER BY t1.a",

// =============================================
// Group 11: Reorder barriers and fallback paths
// =============================================

// Null-safe equality should stop join-group expansion.
"SELECT * FROM t1 JOIN t2 ON t1.a <=> t2.a JOIN t3 ON t2.a = t3.a ORDER BY t1.a",
// STRAIGHT_JOIN should preserve explicit order.
"SELECT * FROM t1 STRAIGHT_JOIN t2 ON t1.a = t2.a JOIN t3 ON t2.a = t3.a ORDER BY t1.a",
// Non-equi LEFT JOIN should not be reordered as non-inner edge.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a < t2.a JOIN t3 ON t1.a = t3.a ORDER BY t1.a, t2.a, t3.a",
// Non-equi RIGHT JOIN should not be reordered as non-inner edge.
"SELECT * FROM t1 RIGHT JOIN t2 ON t1.a < t2.a JOIN t3 ON t2.a = t3.a ORDER BY t2.a, t3.a",

// =============================================
// Group 12: Parenthesized/derived join groups
// =============================================

// Explicit parenthesized mixed joins.
"SELECT * FROM (t1 LEFT JOIN t2 ON t1.a = t2.a) JOIN (t3 JOIN t4 ON t3.a = t4.a) ON t1.a = t3.a ORDER BY t1.a, t3.a",
// Right join over an explicit sub-join.
"SELECT * FROM t1 RIGHT JOIN (t2 LEFT JOIN t3 ON t2.a = t3.a) ON t1.a = t2.a ORDER BY t2.a, t1.a",
// Derived-table leafs inside join reorder.
"SELECT * FROM (SELECT a, b FROM t1 WHERE a <= 3) x JOIN (SELECT a, b FROM t2 WHERE a <= 2) y ON x.a = y.a JOIN t3 ON y.a = t3.a ORDER BY x.a",
// Mixed outer chain with one-side outer condition.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a AND t2.b > 150 LEFT JOIN t3 ON t1.a = t3.a JOIN t4 ON t3.a = t4.a ORDER BY t1.a, t4.a",

// =============================================
// Group 13: 5-table joins
// =============================================

// 5-table inner join chain.
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a JOIN t3 ON t2.a = t3.a JOIN t4 ON t3.a = t4.a JOIN t5 ON t4.a = t5.a ORDER BY t1.a",
// 5-table mixed: INNER + LEFT + INNER + RIGHT.
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t2.a = t3.a JOIN t4 ON t1.a = t4.a RIGHT JOIN t5 ON t4.a = t5.a ORDER BY t5.a",
// 5-table all LEFT JOINs.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t2.a = t3.a LEFT JOIN t4 ON t3.a = t4.a LEFT JOIN t5 ON t4.a = t5.a ORDER BY t1.a",

// =============================================
// Group 14: Deep outer join chains (4-table)
// =============================================

// 4 consecutive LEFT JOINs: assoc(left,left) exercised at each level.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t2.a = t3.a LEFT JOIN t4 ON t3.a = t4.a ORDER BY t1.a",
// 4 consecutive RIGHT JOINs: assoc(right,right) exercised at each level.
"SELECT * FROM t1 RIGHT JOIN t2 ON t1.a = t2.a RIGHT JOIN t3 ON t2.a = t3.a RIGHT JOIN t4 ON t3.a = t4.a ORDER BY t4.a",

// =============================================
// Group 15: Alternating join type patterns
// =============================================

// LEFT-RIGHT-LEFT alternating: conflict rules from mixed joins interact.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a RIGHT JOIN t3 ON t2.a = t3.a LEFT JOIN t4 ON t3.a = t4.a ORDER BY t3.a",
// RIGHT-LEFT-RIGHT alternating: another direction of mixed chain.
"SELECT * FROM t1 RIGHT JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t2.a = t3.a RIGHT JOIN t4 ON t3.a = t4.a ORDER BY t4.a",

// =============================================
// Group 16: WHERE clause interaction with outer joins
// =============================================

// WHERE filter on preserved side of 3-table LEFT JOIN chain.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t2.a = t3.a WHERE t1.b > 10 ORDER BY t1.a",
// WHERE IS NULL filter: preserves outer join semantics (anti-join pattern).
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t1.a = t3.a WHERE t2.a IS NULL ORDER BY t1.a",
// WHERE on null-extended side may convert LEFT to INNER before reorder.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a JOIN t3 ON t1.a = t3.a WHERE t2.b > 100 ORDER BY t1.a",

// =============================================
// Group 17: Self-joins
// =============================================

// Self inner join: same table joined with itself.
"SELECT * FROM t1 x JOIN t1 y ON x.a = y.b ORDER BY x.a, y.a",
// Self left join + another table: CD-C must track distinct vertex IDs.
"SELECT * FROM t1 x LEFT JOIN t1 y ON x.a = y.b JOIN t2 ON x.a = t2.a ORDER BY x.a, t2.a",

// =============================================
// Group 18: Compound ON conditions for non-inner edges
// =============================================

// LEFT JOIN with multiple ON conditions + INNER.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a AND t1.b < t2.b JOIN t3 ON t1.a = t3.a ORDER BY t1.a, t3.a",
// RIGHT JOIN with multiple ON conditions + INNER.
"SELECT * FROM t1 RIGHT JOIN t2 ON t1.a = t2.a AND t1.b > t2.b JOIN t3 ON t2.a = t3.a ORDER BY t2.a, t3.a",

// =============================================
// Group 19: leftAsscom/rightAsscom = 0 specific cases
// =============================================

// leftAsscom(right, left) = 0: (t1 RIGHT t2) LEFT t3 ON t1.a = t3.a.
// The LEFT join references the inner side of the RIGHT join.
"SELECT * FROM t1 RIGHT JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t1.a = t3.a ORDER BY t2.a",
// leftAsscom(right, inner) = 0: (t1 RIGHT t2) INNER t3 ON t1.a = t3.a.
// The INNER join references the inner side of the RIGHT join.
"SELECT * FROM t1 RIGHT JOIN t2 ON t1.a = t2.a JOIN t3 ON t1.a = t3.a ORDER BY t2.a, t3.a",
// rightAsscom(left, inner) = 0: t1 LEFT (t2 INNER t3).
// Achieved via t2 JOIN t3 ON t2.a = t3.a LEFT JOIN t1 ON t1.a = t2.a, then reversed.
"SELECT * FROM t2 JOIN t3 ON t2.a = t3.a LEFT JOIN t1 ON t1.a = t3.a ORDER BY t2.a",

// =============================================
// Group 20: Aggregation and LIMIT on top of join reorder
// =============================================

// GROUP BY after 3-table mixed join.
"SELECT t1.a, COUNT(*) FROM t1 JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t2.a = t3.a GROUP BY t1.a ORDER BY t1.a",
// LIMIT after 3-table inner join.
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a JOIN t3 ON t2.a = t3.a ORDER BY t1.a LIMIT 2",

// =============================================
// Group 21: Semi/Anti join as join-group barriers
// =============================================

// EXISTS subquery becomes semi join, forms a barrier for join group expansion.
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a WHERE EXISTS (SELECT 1 FROM t3 WHERE t3.a = t1.a) ORDER BY t1.a",
// NOT EXISTS subquery becomes anti join.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a WHERE NOT EXISTS (SELECT 1 FROM t3 WHERE t3.a = t1.a) ORDER BY t1.a",
// IN subquery becomes semi join.
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a WHERE t1.a IN (SELECT a FROM t3) ORDER BY t1.a",

// =============================================
// Group 22: Join with OR and complex predicates
// =============================================

// Inner join with OR in WHERE.
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a JOIN t3 ON t2.a = t3.a WHERE t1.b > 10 OR t2.b > 150 ORDER BY t1.a",
// Left join with compound predicate in ON.
"SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a AND (t2.b > 100 OR t2.b < 50) JOIN t3 ON t1.a = t3.a ORDER BY t1.a",

// =============================================
// Group 23: Cross join columns (join on column b)
// =============================================

// Join on column b instead of a.
"SELECT * FROM t1 JOIN t2 ON t1.b = t2.b LEFT JOIN t3 ON t2.b = t3.b ORDER BY t1.a",
// Mixed columns: a for one edge, b for another.
"SELECT * FROM t1 JOIN t2 ON t1.a = t2.a LEFT JOIN t3 ON t2.b = t3.b ORDER BY t1.a"
]
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

Remove the inline comments from this JSON fixture.

This file is committed as .json, but the // ... section comments make it invalid JSON. Static analysis is already failing to parse it, so the fixture is no longer consumable by standard JSON tooling or CI parsers.

🧰 Tools
🪛 Biome (2.4.16)

[error] 5-5: Expected an array, an object, or a literal but instead found '// ============================================='.

(parse)


[error] 10-10: End of file expected

(parse)


[error] 10-11: End of file expected

(parse)


[error] 12-12: End of file expected

(parse)


[error] 12-18: End of file expected

(parse)


[error] 19-19: End of file expected

(parse)


[error] 19-21: End of file expected

(parse)


[error] 22-22: End of file expected

(parse)


[error] 22-24: End of file expected

(parse)


[error] 25-25: End of file expected

(parse)


[error] 25-31: End of file expected

(parse)


[error] 32-32: End of file expected

(parse)


[error] 32-34: End of file expected

(parse)


[error] 35-35: End of file expected

(parse)


[error] 35-41: End of file expected

(parse)


[error] 42-42: End of file expected

(parse)


[error] 42-43: End of file expected

(parse)


[error] 44-44: End of file expected

(parse)


[error] 44-50: End of file expected

(parse)


[error] 51-51: End of file expected

(parse)


[error] 51-57: End of file expected

(parse)


[error] 58-58: End of file expected

(parse)


[error] 58-60: End of file expected

(parse)


[error] 61-61: End of file expected

(parse)


[error] 61-74: End of file expected

(parse)


[error] 75-75: End of file expected

(parse)


[error] 75-81: End of file expected

(parse)


[error] 82-82: End of file expected

(parse)


[error] 82-83: End of file expected

(parse)


[error] 84-84: End of file expected

(parse)


[error] 84-85: End of file expected

(parse)


[error] 86-86: End of file expected

(parse)


[error] 86-92: End of file expected

(parse)


[error] 93-93: End of file expected

(parse)


[error] 93-94: End of file expected

(parse)


[error] 95-95: End of file expected

(parse)


[error] 95-101: End of file expected

(parse)


[error] 102-102: End of file expected

(parse)


[error] 102-103: End of file expected

(parse)


[error] 104-104: End of file expected

(parse)


[error] 104-105: End of file expected

(parse)


[error] 106-106: End of file expected

(parse)


[error] 106-107: End of file expected

(parse)


[error] 108-108: End of file expected

(parse)


[error] 108-114: End of file expected

(parse)


[error] 115-115: End of file expected

(parse)


[error] 115-116: End of file expected

(parse)


[error] 117-117: End of file expected

(parse)


[error] 117-118: End of file expected

(parse)


[error] 119-119: End of file expected

(parse)


[error] 119-120: End of file expected

(parse)


[error] 121-121: End of file expected

(parse)


[error] 121-127: End of file expected

(parse)


[error] 128-128: End of file expected

(parse)


[error] 128-129: End of file expected

(parse)


[error] 130-130: End of file expected

(parse)


[error] 130-131: End of file expected

(parse)


[error] 132-132: End of file expected

(parse)


[error] 132-133: End of file expected

(parse)


[error] 134-134: End of file expected

(parse)


[error] 134-140: End of file expected

(parse)


[error] 141-141: End of file expected

(parse)


[error] 141-142: End of file expected

(parse)


[error] 143-143: End of file expected

(parse)


[error] 143-144: End of file expected

(parse)


[error] 145-145: End of file expected

(parse)


[error] 145-151: End of file expected

(parse)


[error] 152-152: End of file expected

(parse)


[error] 152-153: End of file expected

(parse)


[error] 154-154: End of file expected

(parse)


[error] 154-160: End of file expected

(parse)


[error] 161-161: End of file expected

(parse)


[error] 161-162: End of file expected

(parse)


[error] 163-163: End of file expected

(parse)


[error] 163-169: End of file expected

(parse)


[error] 170-170: End of file expected

(parse)


[error] 170-171: End of file expected

(parse)


[error] 172-172: End of file expected

(parse)


[error] 172-173: End of file expected

(parse)


[error] 174-174: End of file expected

(parse)


[error] 174-180: End of file expected

(parse)


[error] 181-181: End of file expected

(parse)


[error] 181-182: End of file expected

(parse)


[error] 183-183: End of file expected

(parse)


[error] 183-189: End of file expected

(parse)


[error] 190-190: End of file expected

(parse)


[error] 190-191: End of file expected

(parse)


[error] 192-192: End of file expected

(parse)


[error] 192-199: End of file expected

(parse)


[error] 200-200: End of file expected

(parse)


[error] 200-202: End of file expected

(parse)


[error] 203-203: End of file expected

(parse)


[error] 203-205: End of file expected

(parse)


[error] 206-206: End of file expected

(parse)


[error] 206-212: End of file expected

(parse)


[error] 213-213: End of file expected

(parse)


[error] 213-214: End of file expected

(parse)


[error] 215-215: End of file expected

(parse)


[error] 215-221: End of file expected

(parse)


[error] 222-222: End of file expected

(parse)


[error] 222-223: End of file expected

(parse)


[error] 224-224: End of file expected

(parse)


[error] 224-225: End of file expected

(parse)


[error] 226-226: End of file expected

(parse)


[error] 226-232: End of file expected

(parse)


[error] 233-233: End of file expected

(parse)


[error] 233-234: End of file expected

(parse)


[error] 235-235: End of file expected

(parse)


[error] 235-241: End of file expected

(parse)


[error] 242-242: End of file expected

(parse)


[error] 242-243: End of file expected

(parse)


[error] 244-244: End of file expected

(parse)

🤖 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/casetest/rule/testdata/cdc_join_reorder_suite_in.json`
around lines 5 - 245, The fixture contains JavaScript-style inline comments
(lines starting with "//", e.g. the group headings like "Group 1: Inner join
reorder (baseline)" and inline notes) which make the file invalid JSON; remove
all "// ..." comments throughout the file so only valid JSON remains (keep the
array of SQL string literals such as "SELECT * FROM t1 JOIN t2 ON t1.a = t2.a
..." and the surrounding JSON punctuation), ensuring no stray comment characters
remain and the file parses as strict JSON.

Comment thread pkg/planner/core/joinorder/conflict_detector.go
Comment thread pkg/planner/core/joinorder/join_order.go Outdated
guo-shaoge added 15 commits June 5, 2026 10:11
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
@ti-chi-bot
Copy link
Copy Markdown

ti-chi-bot Bot commented Jun 6, 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 fixdb 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

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: 3

🤖 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 `@pkg/planner/core/casetest/rule/rule_cdc_join_reorder_test.go`:
- Around line 118-123: The test preallocates output to len(input) in
LoadTestCasesByName but the loop in rule_cdc_join_reorder_test.go skips "set "
statements and still uses planCaseIdx, causing mismatched counts and trailing
empty records; fix by changing the preallocation logic to use the number of
non-"set " SQLs (or simply build output with append) so planCaseIdx increments
and output length stay in sync: update LoadTestCasesByName (and the loop that
uses planCaseIdx) to either compute validSQLCount (skip "set " entries) before
preallocating or remove fixed preallocation and append to output when processing
each non-"set " SQL, keeping references to planCaseIdx, input, output and the
for-loop that checks strings.HasPrefix(normalized, "set ") consistent.
- Around line 59-69: Phase-1 may inadvertently run with the advanced join
reorder enabled, so explicitly pin the legacy reorder before collecting the
baseline: before building expectedResults (i.e., prior to iterating over input
and calling tk.MustQuery), call the session setter to disable advanced reorder
(use tk.MustExec to set @@tidb_opt_enable_advanced_join_reorder = 0), then
proceed to compute expectedResults via tk.MustQuery/ConvertRowsToStrings; keep
the later tk.MustExec("set @@tidb_opt_enable_advanced_join_reorder = 1")
unchanged for Phase-2 validation.

In `@pkg/planner/core/joinorder/join_order.go`:
- Around line 90-93: The selection look-through currently only blocks
mutable-effects expressions; update the guard in the LogicalSelection path
(where p is type-asserted to logicalop.LogicalSelection and sel.Conditions is
inspected) to also reject non-deterministic predicates by checking
expression.CheckNonDeterministic for any condition, alongside the existing
expression.IsMutableEffectsExpr check and session flag
TiDBOptJoinReorderThroughSel, before calling extractJoinGroup on
sel.Children()[0]; ensure both checks are applied (e.g., use slices.ContainsFunc
against sel.Conditions with both predicate checks) so non-deterministic
expressions like RAND() prevent join reorder through the selection.
🪄 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: 78ea7dda-f6c2-45bb-a8fc-8ee32c4f1b9e

📥 Commits

Reviewing files that changed from the base of the PR and between 96dcc8a and 4c94c2c.

📒 Files selected for processing (18)
  • pkg/planner/core/casetest/binaryplan/testdata/binary_plan_suite_out.json
  • pkg/planner/core/casetest/cbotest/testdata/analyze_suite_out.json
  • pkg/planner/core/casetest/enforcempp/testdata/enforce_mpp_suite_out.json
  • pkg/planner/core/casetest/hint/testdata/integration_suite_out.json
  • pkg/planner/core/casetest/rule/BUILD.bazel
  • pkg/planner/core/casetest/rule/rule_cdc_join_reorder_test.go
  • pkg/planner/core/casetest/rule/testdata/cdc_join_reorder_suite_in.json
  • pkg/planner/core/casetest/rule/testdata/cdc_join_reorder_suite_out.json
  • pkg/planner/core/casetest/rule/testdata/join_reorder_suite_out.json
  • pkg/planner/core/casetest/rule/testdata/outer2inner_out.json
  • pkg/planner/core/casetest/tpch/testdata/tpch_suite_out.json
  • pkg/planner/core/joinorder/BUILD.bazel
  • pkg/planner/core/joinorder/conflict_detector.go
  • pkg/planner/core/joinorder/join_order.go
  • pkg/planner/core/operator/logicalop/logical_join.go
  • pkg/planner/core/rule_join_reorder.go
  • pkg/session/bootstraptest/BUILD.bazel
  • pkg/util/intset/fast_int_set.go
💤 Files with no reviewable changes (1)
  • pkg/planner/core/casetest/rule/testdata/cdc_join_reorder_suite_out.json
✅ Files skipped from review due to trivial changes (4)
  • pkg/planner/core/casetest/cbotest/testdata/analyze_suite_out.json
  • pkg/planner/core/casetest/hint/testdata/integration_suite_out.json
  • pkg/planner/core/casetest/enforcempp/testdata/enforce_mpp_suite_out.json
  • pkg/planner/core/casetest/rule/testdata/cdc_join_reorder_suite_in.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/planner/core/joinorder/BUILD.bazel
  • pkg/planner/core/operator/logicalop/logical_join.go
  • pkg/planner/core/joinorder/conflict_detector.go

Comment on lines +59 to +69
// Phase 1: Collect expected results using the old join reorder algorithm
// (CD-C is NOT enabled yet). These serve as the ground-truth baseline.
expectedResults := make([][]string, len(input))
for i, sql := range input {
expectedResults[i] = testdata.ConvertRowsToStrings(tk.MustQuery(sql).Rows())
}

// Phase 2: Enable CD-C algorithm, then verify both the plan and the
// result correctness for every case.
tk.MustExec("set @@tidb_opt_enable_advanced_join_reorder = 1")

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

Pin Phase-1 to legacy reorder explicitly.

Phase-1 is documented as the “old join reorder algorithm,” but this PR’s objective enables advanced reorder by default. Without explicitly setting @@tidb_opt_enable_advanced_join_reorder = 0, the baseline can accidentally use CD-C too, making the cross-check non-effective.

Suggested patch
 	// Phase 1: Collect expected results using the old join reorder algorithm
 	// (CD-C is NOT enabled yet). These serve as the ground-truth baseline.
+	tk.MustExec("set @@tidb_opt_enable_advanced_join_reorder = 0")
 	expectedResults := make([][]string, len(input))
 	for i, sql := range input {
 		expectedResults[i] = testdata.ConvertRowsToStrings(tk.MustQuery(sql).Rows())
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Phase 1: Collect expected results using the old join reorder algorithm
// (CD-C is NOT enabled yet). These serve as the ground-truth baseline.
expectedResults := make([][]string, len(input))
for i, sql := range input {
expectedResults[i] = testdata.ConvertRowsToStrings(tk.MustQuery(sql).Rows())
}
// Phase 2: Enable CD-C algorithm, then verify both the plan and the
// result correctness for every case.
tk.MustExec("set @@tidb_opt_enable_advanced_join_reorder = 1")
// Phase 1: Collect expected results using the old join reorder algorithm
// (CD-C is NOT enabled yet). These serve as the ground-truth baseline.
tk.MustExec("set @@tidb_opt_enable_advanced_join_reorder = 0")
expectedResults := make([][]string, len(input))
for i, sql := range input {
expectedResults[i] = testdata.ConvertRowsToStrings(tk.MustQuery(sql).Rows())
}
// Phase 2: Enable CD-C algorithm, then verify both the plan and the
// result correctness for every case.
tk.MustExec("set @@tidb_opt_enable_advanced_join_reorder = 1")
🤖 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/casetest/rule/rule_cdc_join_reorder_test.go` around lines 59
- 69, Phase-1 may inadvertently run with the advanced join reorder enabled, so
explicitly pin the legacy reorder before collecting the baseline: before
building expectedResults (i.e., prior to iterating over input and calling
tk.MustQuery), call the session setter to disable advanced reorder (use
tk.MustExec to set @@tidb_opt_enable_advanced_join_reorder = 0), then proceed to
compute expectedResults via tk.MustQuery/ConvertRowsToStrings; keep the later
tk.MustExec("set @@tidb_opt_enable_advanced_join_reorder = 1") unchanged for
Phase-2 validation.

Comment on lines +118 to +123
planCaseIdx := 0
for _, sql := range input {
normalized := strings.ToLower(strings.TrimSpace(sql))
if strings.HasPrefix(normalized, "set ") {
tk.MustExec(sql)
continue
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 | 🟡 Minor | ⚡ Quick win

Record-mode case counting can fail when set statements are present.

LoadTestCasesByName preallocates output to len(input) in record mode, but this loop skips set statements. That makes planCaseIdx smaller than len(output), causing a false failure and leaving trailing empty recorded cases.

Suggested patch
-	require.Equalf(t, len(output), planCaseIdx,
-		"unexpected output case count, output=%d, actual explain cases=%d", len(output), planCaseIdx)
+	if testdata.Record() {
+		output = output[:planCaseIdx]
+	} else {
+		require.Equalf(t, len(output), planCaseIdx,
+			"unexpected output case count, output=%d, actual explain cases=%d", len(output), planCaseIdx)
+	}

Also applies to: 145-146

🤖 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/casetest/rule/rule_cdc_join_reorder_test.go` around lines
118 - 123, The test preallocates output to len(input) in LoadTestCasesByName but
the loop in rule_cdc_join_reorder_test.go skips "set " statements and still uses
planCaseIdx, causing mismatched counts and trailing empty records; fix by
changing the preallocation logic to use the number of non-"set " SQLs (or simply
build output with append) so planCaseIdx increments and output length stay in
sync: update LoadTestCasesByName (and the loop that uses planCaseIdx) to either
compute validSQLCount (skip "set " entries) before preallocating or remove fixed
preallocation and append to output when processing each non-"set " SQL, keeping
references to planCaseIdx, input, output and the for-loop that checks
strings.HasPrefix(normalized, "set ") consistent.

Comment on lines +90 to +93
if sel, isSel := p.(*logicalop.LogicalSelection); isSel {
if p.SCtx().GetSessionVars().TiDBOptJoinReorderThroughSel &&
!slices.ContainsFunc(sel.Conditions, expression.IsMutableEffectsExpr) {
childGroup := extractJoinGroup(sel.Children()[0])
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

Guard Selection look-through against non-deterministic predicates

Line 92 only checks mutable-effects expressions. This allows join reorder through non-deterministic predicates (e.g. RAND()), which can change evaluation behavior/results. Please align this with the legacy path by also rejecting expression.CheckNonDeterministic(expr).

Suggested fix
-	if sel, isSel := p.(*logicalop.LogicalSelection); isSel {
-		if p.SCtx().GetSessionVars().TiDBOptJoinReorderThroughSel &&
-			!slices.ContainsFunc(sel.Conditions, expression.IsMutableEffectsExpr) {
+	if sel, isSel := p.(*logicalop.LogicalSelection); isSel {
+		if p.SCtx().GetSessionVars().TiDBOptJoinReorderThroughSel &&
+			!slices.ContainsFunc(sel.Conditions, func(expr expression.Expression) bool {
+				return expression.IsMutableEffectsExpr(expr) || expression.CheckNonDeterministic(expr)
+			}) {
🤖 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/joinorder/join_order.go` around lines 90 - 93, The selection
look-through currently only blocks mutable-effects expressions; update the guard
in the LogicalSelection path (where p is type-asserted to
logicalop.LogicalSelection and sel.Conditions is inspected) to also reject
non-deterministic predicates by checking expression.CheckNonDeterministic for
any condition, alongside the existing expression.IsMutableEffectsExpr check and
session flag TiDBOptJoinReorderThroughSel, before calling extractJoinGroup on
sel.Children()[0]; ensure both checks are applied (e.g., use slices.ContainsFunc
against sel.Conditions with both predicate checks) so non-deterministic
expressions like RAND() prevent join reorder through the selection.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 6, 2026

Codecov Report

❌ Patch coverage is 72.92540% with 323 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (release-8.5@2763499). Learn more about missing BASE report.

Additional details and impacted files
@@               Coverage Diff                @@
##             release-8.5     #68880   +/-   ##
================================================
  Coverage               ?   55.1127%           
================================================
  Files                  ?       1830           
  Lines                  ?     660706           
  Branches               ?          0           
================================================
  Hits                   ?     364133           
  Misses                 ?     269365           
  Partials               ?      27208           
Flag Coverage Δ
integration 38.2194% <72.0033%> (?)
unit 65.0507% <72.6739%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
dumpling 55.3108% <0.0000%> (?)
parser ∅ <0.0000%> (?)
br 54.6107% <0.0000%> (?)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
Signed-off-by: guo-shaoge <shaoge1994@163.com>
@guo-shaoge guo-shaoge changed the title planner: enable cd-c algorithm by default for join reorder planner: enable cd-c algorithm by default for join reorder | tidb-test=pr/2759 Jun 6, 2026
@guo-shaoge
Copy link
Copy Markdown
Collaborator Author

/retest

@guo-shaoge
Copy link
Copy Markdown
Collaborator Author

/test mysql-test

@tiprow
Copy link
Copy Markdown

tiprow Bot commented Jun 6, 2026

@guo-shaoge: The specified target(s) for /test were not found.
The following commands are available to trigger required jobs:

/test fast_test_tiprow_for_release

Use /test all to run all jobs.

Details

In response to this:

/test mysql-test

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.

@guo-shaoge
Copy link
Copy Markdown
Collaborator Author

/retest

Signed-off-by: guo-shaoge <shaoge1994@163.com>
@guo-shaoge
Copy link
Copy Markdown
Collaborator Author

/retest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/cherry-pick-not-approved 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant