Skip to content

planner: Add optimizer support to LATERAL join - #67095

Closed
terry1purcell wants to merge 9 commits into
pingcap:masterfrom
terry1purcell:lateral-planner
Closed

terry1purcell wants to merge 9 commits into
pingcap:masterfrom
terry1purcell:lateral-planner

Conversation

@terry1purcell

@terry1purcell terry1purcell commented Mar 17, 2026 •

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: ref #40328

Problem Summary:

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

    • Added LATERAL derived-table support: LATERAL keyword, column-alias lists, and correct SQL emission/restoration.
  • Error Handling

    • Added a new, specific planner error for invalid LATERAL usage and updated related error messages.
  • Tests

    • Added extensive parser, planner, and integration tests covering parsing, planning, decorrelation, reordering, error paths, and many LATERAL edge cases.

terry1purcell and others added 6 commits March 16, 2026 18:18
Add parser support for the LATERAL keyword in derived tables, following
MySQL 8.0 syntax. This is parser-only; planner support follows in a
separate PR.

Grammar: LATERAL SubSelect TableAsName IdentListWithParenOpt
- Alias is required (TableAsName, not TableAsNameOpt)
- AS keyword is optional
- Optional column list: LATERAL (...) AS dt(c1, c2)

AST changes:
- TableSource.Lateral bool flag
- TableSource.ColumnNames []CIStr for column aliases
- Restore() emits LATERAL keyword and column list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lands

The parser now accepts the LATERAL keyword (from the previous commit),
but the planner does not yet support it. Add a preprocessor check that
returns a clear "not supported yet" error for any TableSource with
Lateral=true, so users get an actionable message instead of a confusing
plan-build failure.

This guard will be removed when planner support for LATERAL is added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a guard in TableSource.Restore() that returns a descriptive error
if ColumnNames is non-empty but AsName is empty, instead of silently
dropping the column list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verify that LATERAL derived tables are rejected during preprocessing
with ErrNotSupportedYet, covering both comma syntax and LEFT JOIN forms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Check Lateral flag and ColumnNames on both the original and re-parsed
(round-tripped) AST so that regressions in Restore() column-list
emission are caught. Also adds negative assertion for non-LATERAL
cases on the round-tripped statement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…yntax

Implement planner support for LATERAL derived tables with INNER JOIN
semantics (explicit JOIN and comma syntax). LATERAL makes left-side
columns available to the right-side subquery via LogicalApply.

Supported forms:
- FROM t1, LATERAL (SELECT ...) AS dt
- FROM t1 JOIN LATERAL (SELECT ...) AS dt ON <cond>
- FROM t1 CROSS JOIN LATERAL (SELECT ...) AS dt

Not yet supported (returns ErrNotSupportedYet):
- LEFT/RIGHT JOIN LATERAL

Key changes:
- buildLateralJoin() in logical_plan_builder.go builds LogicalApply
- IsLateral flag on LogicalApply/LogicalJoin prevents unsafe rewrites
- IsLateral guard in pruneRedundantApply (rule_decorrelate.go)
- LATERAL-aware join reorder prevention (rule_join_reorder.go)
- setIsInApplyForCTE for CTE-in-LATERAL correctness
- Decorrelation enabled for safe equi-join patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ti-chi-bot ti-chi-bot Bot added the release-note-none Denotes a PR that doesn't merit a release note. label Mar 17, 2026
@pantheon-ai

pantheon-ai Bot commented Mar 17, 2026 •

Copy link
Copy Markdown

Review Complete

Findings: 2 issues
Posted: 2
Duplicates/Skipped: 0

ℹ️ Learn more details on Pantheon AI.

@ti-chi-bot ti-chi-bot Bot added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Mar 17, 2026
@terry1purcell

Copy link
Copy Markdown
Contributor Author

/ok-to-test

@ti-chi-bot ti-chi-bot Bot added the ok-to-test Indicates a PR is ready to be tested. label Mar 17, 2026
@ti-chi-bot

ti-chi-bot Bot commented Mar 17, 2026

Copy link
Copy Markdown

[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 benmeadowcroft, d3hunter, 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

@tiprow

tiprow Bot commented Mar 17, 2026

Copy link
Copy Markdown

Hi @terry1purcell. Thanks for your PR.

PRs from untrusted users cannot be marked as trusted with /ok-to-test in this repo meaning untrusted PR authors can never trigger tests themselves. Collaborators can still trigger tests on the PR using /test all.

I understand the commands that are listed here.

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 the sig/planner SIG: Planner label Mar 17, 2026
@coderabbitai

coderabbitai Bot commented Mar 17, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds SQL LATERAL support: parser recognizes LATERAL and records column aliases; planner treats LATERAL derived tables as LogicalApply (IsLateral), preserves outer-scope correlation, prevents reorder/prune, and introduces ErrInvalidLateralJoin. Extensive unit and integration tests added/updated.

Changes

Cohort / File(s) Summary
Error Definitions
errors.toml, pkg/errno/errcode.go, pkg/errno/errname.go, pkg/util/dbterror/plannererrors/planner_terror.go
Add error 3809 (ErrInvalidLateralJoin) and mapping "Invalid use of LATERAL: %s".
Parser: keywords, tokens & grammar
pkg/parser/keywords.go, pkg/parser/keywords_test.go, pkg/parser/misc.go, pkg/parser/parser.y
Introduce LATERAL keyword/token and grammar production for LATERAL derived-table syntax.
Parser AST & printing
pkg/parser/ast/dml.go
Add Lateral bool and ColumnNames []CIStr to TableSource; validate and emit LATERAL and column alias lists.
Parser tests & build
pkg/parser/lateral_test.go, pkg/parser/BUILD.bazel
Add LATERAL parsing/restore/round-trip tests and include file in parser_test target.
Planner core: builder state & lateral path
pkg/planner/core/planbuilder.go, pkg/planner/core/logical_plan_builder.go, pkg/planner/core/planbuilder.go
Add builder flags for lateral, helpers (findJoinFullSchema, containsLateralTableSource), implement buildLateralJoin to create LogicalApply for LATERAL and propagate outer-schema with cloning.
Logical operators: Apply & Join
pkg/planner/core/operator/logicalop/logical_apply.go, pkg/planner/core/operator/logicalop/hash64_equals_generated.go, pkg/planner/core/operator/logicalop/logical_join.go
Add IsLateral to LogicalApply (affects hash/equals, pruning, stats); add helper to find child FullSchema and use full-schema when extracting used/correlated columns.
Optimization / decorrelate / reorder
pkg/planner/core/rule_decorrelate.go, pkg/planner/core/rule_join_reorder.go, pkg/planner/core/expression_rewriter.go
Prevent pruning of lateral Apply, prefer full-join schema for CorCols extraction, treat LogicalApply as non-reorderable, and handle LogicalApply in NATURAL/USING resolution.
Planner tests & build
pkg/planner/core/lateral_join_test.go, pkg/planner/core/casetest/rule/rule_outer2inner_test.go, pkg/planner/core/preprocess_test.go, pkg/planner/core/BUILD.bazel
Add extensive unit tests for lateral planning/optimization/reordering/schema/error paths and include tests in build.
Planner casetest JSON updates
pkg/planner/core/casetest/enforcempp/testdata/*, pkg/planner/core/casetest/rule/testdata/outer2inner_*.json
Normalize explain-format strings and add TestOuter2InnerLateralSelection test group data.
Integration tests (cascades & lateral)
tests/integrationtest/t/planner/cascades/integration.test, tests/integrationtest/r/planner/cascades/integration.result, tests/integrationtest/t/planner/core/lateral_join.test, tests/integrationtest/r/planner/core/lateral_join.result
Add Cascades integration suite and new LATERAL integration tests/results covering planner behavior and expected outputs.
Misc test expectation updates
tests/integrationtest/r/cte.result, tests/integrationtest/r/executor/aggregate.result, tests/integrationtest/r/expression/misc.result, tests/integrationtest/r/planner/core/integration.result
Adjust expected error message text/formatting (recursive CTE/LATERAL exceptions, column qualifiers).

Sequence Diagram

sequenceDiagram
    participant Parser as Parser
    participant PlanBuilder as Plan Builder
    participant Apply as LogicalApply
    participant Decorrelate as DecorrelateSolver
    participant Reorder as JoinReorder
    participant Optimizer as Optimizer

    Parser->>Parser: tokenize/parse SQL, recognize `LATERAL`
    Parser->>PlanBuilder: produce TableSource(Lateral=true, ColumnNames?)
    PlanBuilder->>PlanBuilder: detect LATERAL on join right side
    PlanBuilder->>PlanBuilder: findJoinFullSchema() to capture outer scope
    PlanBuilder->>Apply: buildLateralJoin() -> create LogicalApply (IsLateral=true, CorCols)
    Apply->>Decorrelate: DecorrelateSolver.optimize()
    Decorrelate->>Decorrelate: see IsLateral -> skip pruneRedundantApply
    Apply->>Reorder: extractJoinGroup()
    Reorder->>Reorder: detect LogicalApply -> early return (no reorder)
    Optimizer->>Optimizer: continue optimization with preserved Apply
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • qw4990
  • AilinKid
  • winoros

Poem

🐰 I hopped through parser, planner, and test,
Found LATERAL cosy in SQL's nest,
Apply keeps its outer names near,
Reorder pauses — dependencies clear,
A rabbit cheers: now LATERAL is here!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description uses the standard template but lacks the required 'Problem Summary' and 'What changed and how does it work?' sections. Only the issue reference and test checklist are provided; critical implementation details are missing. Complete the 'Problem Summary' section with context from issue #40328 and fill in 'What changed and how does it work?' with a clear explanation of the LATERAL join implementation and optimization strategy.
Docstring Coverage ⚠️ Warning Docstring coverage is 70.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'planner: Add optimizer support to LATERAL join' is clear, concise, and accurately describes the main change in the PR—adding planner/optimizer support for LATERAL joins.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.11.3)

Command failed


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.

Tip

You can customize the high-level summary generated by CodeRabbit.

Configure the reviews.high_level_summary_instructions setting to provide custom instructions for generating the high-level summary.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (2)
pkg/planner/core/rule_decorrelate.go (1)

164-205: ⚠️ Potential issue | 🟠 Major

Nested LATERAL Apply can still be pruned incorrectly.

Line 167 blocks only the current Apply. The loop (Lines 191-205) still removes downstream LogicalApply nodes unconditionally; if any nextApply.IsLateral is true, pruning can change multiplicity and query results.

💡 Proposed guard in the nested-apply traversal
 		for {
 			child := finalResult.Children()[0]
 			nextApply, ok := child.(*logicalop.LogicalApply)
 			if !ok {
 				if len(groupByColumn) == 0 {
 					return child, true
 				}
 				for col := range groupByColumn {
 					if apply.Schema().Contains(col) && !child.Schema().Contains(col) {
 						return nil, false
 					}
 				}
 				return child, true // Return the child of the last LogicalApply
 			}
+			if nextApply.IsLateral {
+				return nil, false
+			}
 			finalResult = nextApply
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/rule_decorrelate.go` around lines 164 - 205, The loop that
walks through nested LogicalApply nodes (finalResult/nextApply) must stop and
avoid pruning if any downstream apply has IsLateral==true; currently only the
top-level apply.IsLateral is checked. Modify the traversal in the for loop that
inspects child := finalResult.Children()[0] / nextApply :=
child.(*logicalop.LogicalApply) to check nextApply.IsLateral and if true return
nil, false (or otherwise abort pruning) before assigning finalResult =
nextApply; this ensures any nested lateral apply prevents elimination and
preserves result multiplicity (also keep existing groupByColumn and schema
containment checks intact).
pkg/planner/core/operator/logicalop/logical_apply.go (1)

146-174: ⚠️ Potential issue | 🟠 Major

Carry right-side NDVs into lateral Apply stats.

IsLateral updates RowCount, but the NDV block below still treats every right-side output column as leftProfile.RowCount. For multi-row lateral joins that collapses right-column distinctness and can badly distort follow-up estimates. When IsLateral is true, seed right-side NDVs from rightProfile.ColNDVs capped by rowCount, similar to LogicalJoin.DeriveStats.

Suggested fix
 	la.SetStats(&property.StatsInfo{
 		RowCount: rowCount,
 		ColNDVs:  make(map[int64]float64, selfSchema.Len()),
 	})
 	// TODO: investigate why this cannot be replaced with maps.Copy()
 	for id, c := range leftProfile.ColNDVs {
 		la.StatsInfo().ColNDVs[id] = c
 	}
 	if la.JoinType == base.LeftOuterSemiJoin || la.JoinType == base.AntiLeftOuterSemiJoin {
 		la.StatsInfo().ColNDVs[selfSchema.Columns[selfSchema.Len()-1].UniqueID] = 2.0
+	} else if la.IsLateral {
+		for id, c := range rightProfile.ColNDVs {
+			la.StatsInfo().ColNDVs[id] = min(c, rowCount)
+		}
 	} else {
 		for i := childSchema[0].Len(); i < selfSchema.Len(); i++ {
 			la.StatsInfo().ColNDVs[selfSchema.Columns[i].UniqueID] = leftProfile.RowCount
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/operator/logicalop/logical_apply.go` around lines 146 - 174,
The NDV population currently always assigns right-side output columns NDV =
leftProfile.RowCount; instead, when la.IsLateral is true incorporate
rightProfile.ColNDVs for right output columns (seed each right column NDV =
min(rightProfile.ColNDVs[colID], rowCount)) and fall back to
leftProfile.RowCount only when a right NDV is missing; update the block that
fills la.StatsInfo().ColNDVs (after la.SetStats) to copy leftProfile.ColNDVs as
before and then for i from childSchema[0].Len() to selfSchema.Len() use
rightProfile.ColNDVs capped by rowCount (mirroring LogicalJoin.DeriveStats
behavior) and ensure the LeftOuterSemi/Anti case still sets the trailing column
to 2.0.
🧹 Nitpick comments (5)
pkg/planner/core/casetest/enforcempp/testdata/enforce_mpp_suite_in.json (1)

25-25: Fix typo in inline SQL comment (huristic → heuristic).

Non-functional, but correcting this helps readability and grep-ability across test artifacts.

✏️ Suggested tweak
-      "explain format='brief' select  /*+ READ_FROM_STORAGE(TIFLASH[s]) */ a from s where a = 10 and b is null; -- index path huristic rule will prune tiflash path",
+      "explain format='brief' select  /*+ READ_FROM_STORAGE(TIFLASH[s]) */ a from s where a = 10 and b is null; -- index path heuristic rule will prune tiflash path",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/casetest/enforcempp/testdata/enforce_mpp_suite_in.json` at
line 25, Update the inline SQL comment in the test string that currently reads
"index path huristic rule will prune tiflash path" to correct the typo to "index
path heuristic rule will prune tiflash path" in the SQL literal ("explain
format='brief' select  /*+ READ_FROM_STORAGE(TIFLASH[s]) */ a from s where a =
10 and b is null; -- index path huristic rule will prune tiflash path") so the
comment is spelled correctly for readability and grep-ability.
tests/integrationtest/t/planner/cascades/integration.test (1)

199-239: Consider adding explicit cascades LATERAL SQL cases in this suite.

This file validates Join/Apply broadly, but it does not directly assert cascades behavior for LATERAL syntax. Adding one positive case (INNER/CROSS/,) and one unsupported-case assertion (LEFT/RIGHT JOIN LATERAL) here would improve regression coverage for the planner path targeted by this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integrationtest/t/planner/cascades/integration.test` around lines 199 -
239, Add explicit LATERAL cases to the existing cascades planner tests: under
the TestApply block add a positive inner/cross LATERAL case (e.g. "select t1.a,
x.b from t1 cross join lateral (select b from t2 where t2.a = t1.a) as x;" with
both explain format="plan_tree" and the select to assert plan + result), and add
unsupported-case assertions for LEFT/RIGHT JOIN LATERAL (e.g. "select ... from
t1 left join lateral (...)" and right join lateral) that check the planner
rejects or produces the expected unsupported-plan/error; place these near the
other apply/join examples so they exercise the same session settings
(tidb_enable_cascades_planner) and use the same test tables t1/t2 to match
existing patterns.
tests/integrationtest/t/planner/core/lateral_join.test (1)

127-133: Stabilize EXPLAIN assertions at Line 127 and Line 133.

These two EXPLAIN checks should use --replace_column like earlier cases to avoid brittle plan-id/estRows diffs.

Suggested patch
 ## EXPLAIN: LATERAL + USING + ON must show Apply, not plain Join
+--replace_column 1 <id> 2 <estRows> 3 <task> 4 <access_object> 5 <operator_info>
 explain format='brief' select * from t1 join t2 using(a) join t3 on t2.b=t3.b, lateral (select count(*) as c from t4 where t4.a=t2.a) as dt;
@@
 ## EXPLAIN: LATERAL + USING + ON with aggregate that blocks decorrelation
+--replace_column 1 <id> 2 <estRows> 3 <task> 4 <access_object> 5 <operator_info>
 explain format='brief' select * from t1 join t2 using(a) join t3 on t2.b=t3.b, lateral (select sum(t4.c) as s from t4 where t4.a=t2.a) as dt;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integrationtest/t/planner/core/lateral_join.test` around lines 127 -
133, Update the two EXPLAIN assertions that are currently brittle by adding the
same --replace_column handling used in earlier tests: modify the EXPLAIN for
"select * from t1 join t2 using(a) join t3 on t2.b=t3.b, lateral (select
count(*) as c from t4 where t4.a=t2.a) as dt" (Line 127) and the EXPLAIN for
"select * from t1 join t2 using(a) join t3 on t2.b=t3.b, lateral (select
sum(t4.c) as s from t4 where t4.a=t2.a) as dt" (Line 133) to invoke
--replace_column for plan-id/estRows columns so the assertions ignore transient
plan identifiers; follow the same --replace_column syntax and column names used
in the nearby tests to ensure consistency.
pkg/planner/core/lateral_join_test.go (2)

35-41: Dead code: expectedErrorCode field is never used.

The expectedErrorCode field is defined but never set to a non-zero value in any test case. The conditional check on lines 94-97 will never execute. Either populate this field with actual error codes (e.g., 3809 for ErrInvalidLateralJoin) or remove it to keep tests minimal.

🔧 Option 1: Remove unused field and dead code
 	testCases := []struct {
 		name              string
 		sql               string
 		expectApply       bool
 		expectError       bool
-		expectedErrorCode int
 	}{

And remove lines 94-97:

 			if tc.expectError {
 				require.Error(t, err, "Expected error for: %s", tc.sql)
-				if tc.expectedErrorCode != 0 {
-					require.True(t, plannererrors.ErrInvalidLateralJoin.Equal(err),
-						"Expected ErrInvalidLateralJoin error, got: %v", err)
-				}
+				require.True(t, plannererrors.ErrInvalidLateralJoin.Equal(err),
+					"Expected ErrInvalidLateralJoin error, got: %v", err)
 				return
 			}
🔧 Option 2: Use the field by setting error codes
 		{
 			name:        "LATERAL with LEFT JOIN not yet supported",
 			sql:         "SELECT * FROM t LEFT JOIN LATERAL (SELECT t.b) AS dt ON true",
 			expectError: true,
+			expectedErrorCode: 3809,
 		},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/lateral_join_test.go` around lines 35 - 41, The tests define
an unused struct field expectedErrorCode in the testCases slice and the
follow-up conditional that checks it (referencing expectedErrorCode) is dead;
either remove the expectedErrorCode field from the testCases struct and delete
the conditional that inspects it, or set expectedErrorCode values in each test
case (e.g., 3809 for ErrInvalidLateralJoin) and update the test assertions to
compare against that field; locate the testCases declaration and the conditional
that reads expectedErrorCode in lateral_join_test.go and apply one of these two
changes to eliminate the dead code.

118-156: Test assertions don't match the function name's promise.

TestLateralJoinOptimization states it "tests decorrelation and optimization behavior" but only verifies that plans build successfully and have a non-nil schema. Consider either:

  1. Adding assertions that verify actual optimization outcomes (e.g., checking if Apply nodes were preserved/converted), or
  2. Renaming to TestLateralJoinOptimizationBuilds to reflect what it actually tests.

Note: TestLateralJoinDecorrelateWithUSINGAndON (line 536) does proper decorrelation verification and could serve as a model.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/lateral_join_test.go` around lines 118 - 156, The test
TestLateralJoinOptimization currently only asserts that a plan builds and has a
non-nil schema; either rename the test to TestLateralJoinOptimizationBuilds to
reflect that minimal check, or enhance it to assert actual
decorrelation/optimization outcomes by inspecting the logical plan returned from
BuildLogicalPlanForTest: for each case inspect the plan tree to confirm whether
Apply nodes (e.g., LogicalApply/Apply) remain or were converted to Join nodes
(e.g., LogicalJoin/Join) as expected (mirror the approach used in
TestLateralJoinDecorrelateWithUSINGAndON), failing the test when the node types
do not match the expected optimization result.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/parser/ast/dml.go`:
- Around line 553-563: The validation currently only forbids LATERAL and column
lists for *TableName but still allows them on other non-derived sources like
*Join; update the guard in the validation block in the TableSource logic (check
TableSource.Source) so that LATERAL and ColumnNames are permitted only when the
Source is a derived table type: *SelectStmt or *SetOprStmt, and reject them for
any other concrete source (including *Join and *TableName); use the existing
fields n.Lateral, n.ColumnNames and n.AsName.String() to emit the same error
messages when the Source is not one of the allowed derived types.

In `@pkg/planner/core/casetest/rule/testdata/outer2inner_out.json`:
- Around line 675-686: The golden plan's Projection node is missing test.t1.c1
(it lists only test.t1.a1, test.t1.b1, test.t2.a2, Column#9), so either the
planner output was truncated or the test fixture schema baked into
outer2inner_out.json is wrong; update the Projection entry in
outer2inner_out.json (the JSON under "Plan" for the SQL containing "lateral ...
count(*) as cnt") to include test.t1.c1 alongside a1 and b1 (and verify Column#9
remains the aggregate count), or if the planner should emit that column ensure
the test harness captures the full projection from the Projection node rather
than dropping columns produced by the outer TableFullScan/TableReader for
test.t1.

In `@pkg/planner/core/expression_rewriter.go`:
- Around line 2733-2743: The resolver only remaps redundant USING/NATURAL
columns for LogicalJoin, causing qualified names over LATERAL LogicalApply to
bind to names not present in Apply.Schema() and break later index resolution;
update resolveRedundantColumnFromNaturalUsingJoinPlan to also handle
*logicalop.LogicalApply (or detect LogicalJoin embedding) by checking
x.FullSchema / x.FullNames like the new case at the top of
expression_rewriter.go and remapping the redundant column into Apply.Schema()
(or return the corresponding Column/Name pair) so that resolve-indices sees the
correct Apply.Schema() entries; reference
resolveRedundantColumnFromNaturalUsingJoinPlan, logicalop.LogicalApply,
LogicalJoin, FullSchema, FullNames, and Apply.Schema() when making the change.

In `@pkg/planner/core/logical_plan_builder.go`:
- Around line 706-715: The current code sets b.buildingLateralSubquery for the
entire right branch while building a join (flipping it before processing
joinNode.Right), which allows non-LATERAL siblings to bypass recursive-CTE ORDER
BY/LIMIT checks; instead, only set b.buildingLateralSubquery when the
TableSource node is actually a LATERAL (check ast.TableSource{Lateral:true} on
the specific node) and restore it immediately after processing that TableSource.
Locate the block that saves and defers restoring b.buildingLateralSubquery (the
saveBuildingLateral variable and defer that pops outerSchemas/outerNames and
resets flags) and change the scope so the flag is set and reset around building
the specific ast.TableSource LATERAL node (not the whole joinNode.Right),
keeping the other cleanup (outerSchemas/outerNames/lateralOuterCount) intact in
the existing defer.
- Around line 950-959: The right child's FullSchema is being dropped by
unconditionally using rightPlan.Schema()/OutputNames(); instead call
findJoinFullSchema on rightPlan (just like done for leftPlan) and if it returns
non-nil set rFullSchema and rFullNames from that result, otherwise fall back to
rightPlan.Schema() and rightPlan.OutputNames(); reference the existing
findJoinFullSchema function and the rFullSchema/rFullNames variables (and
consider LogicalJoin/LogicalApply as possible rightPlan types that carry
FullSchema).

In `@tests/integrationtest/r/planner/cascades/integration.result`:
- Around line 891-899: The snapshot is asserting volatile columns from
information_schema.processlist (the SELECT that lists USER, DB, COMMAND, TIME,
STATE, INFO, `DIGEST`) which causes flakiness; change the test to only assert
stable columns (e.g., USER, DB, COMMAND, STATE) or normalize/ignore TIME, INFO
and `DIGEST` in the comparison (for example, remove them from the SELECT used
for the snapshot or mask them before asserting) so the MemTableScan/select
expectation no longer depends on timing-sensitive values.

In `@tests/integrationtest/r/planner/core/integration.result`:
- Line 2844: The expected error text currently shows a truncated column
reference 'OUTR.'; update the test fixture to use the full identifier (e.g.,
replace 'OUTR.' with 'OUTR.col2' in the expected output) and/or fix the
error-message serialization routine (e.g., the function that formats SQL errors
or expression references such as serializeExpression/formatSqlError) so it
preserves the column name after the table alias instead of dropping it; ensure
the produced and expected messages match exactly with the full reference
'OUTR.col2'.

In `@tests/integrationtest/r/planner/core/lateral_join.result`:
- Line 6: Stabilize the nondeterministic lateral subqueries by adding an
explicit ORDER BY before LIMIT 1 so the chosen row is deterministic; update the
lateral clauses like "lateral (select b from t2 where t2.a = t1.a limit 1) as
lat" to include a deterministic sort (for example "order by b" or the table's
primary key) in each occurrence (the queries containing the lateral subquery at
the instances shown).

In `@tests/integrationtest/t/planner/cascades/integration.test`:
- Around line 1-282: The result file
tests/integrationtest/r/planner/cascades/integration.result is empty and must be
populated with the expected outputs for the 14 tests defined in
tests/integrationtest/t/planner/cascades/integration.test (e.g.,
TestSimpleProjDual, TestPKIsHandleRangeScan, TestIndexScan, TestBasicShow,
TestSort, TestAggregation, TestPushdownDistinctEnable,
TestPushdownDistinctDisable, TestSimplePlans, TestJoin, TestApply,
TestMemTableScan, TestTopN, TestCascadePlannerHashedPartTable); run the
integration test harness (or the equivalent test runner used by the repo)
against that .test file with the correct environment flags
(tidb_enable_cascades_planner etc.), capture the explain/select outputs, replace
the empty integration.result with the regenerated outputs, and commit the
updated tests/integrationtest/r/planner/cascades/integration.result so it
contains the expected results for each named test case.
- Around line 164-175: TestPushdownDistinctDisable contains nondeterministic
assertions (the query with "a, count(distinct a)" and the several "GROUP BY c"
selects) so mirror the normalization directives used in
TestPushdownDistinctEnable: add the same output-normalization/comments (the
stable result ordering/normalization directives) immediately before the affected
statements in TestPushdownDistinctDisable so the plan/result comparisons are
deterministic for the "select /*+ ... */ a, count(distinct a) ..." case and for
all "group by c" queries; ensure you apply the directives to the explain/select
pairs that include "count(distinct ...)" and "group by c" so they follow the
same normalization rules as TestPushdownDistinctEnable.

---

Outside diff comments:
In `@pkg/planner/core/operator/logicalop/logical_apply.go`:
- Around line 146-174: The NDV population currently always assigns right-side
output columns NDV = leftProfile.RowCount; instead, when la.IsLateral is true
incorporate rightProfile.ColNDVs for right output columns (seed each right
column NDV = min(rightProfile.ColNDVs[colID], rowCount)) and fall back to
leftProfile.RowCount only when a right NDV is missing; update the block that
fills la.StatsInfo().ColNDVs (after la.SetStats) to copy leftProfile.ColNDVs as
before and then for i from childSchema[0].Len() to selfSchema.Len() use
rightProfile.ColNDVs capped by rowCount (mirroring LogicalJoin.DeriveStats
behavior) and ensure the LeftOuterSemi/Anti case still sets the trailing column
to 2.0.

In `@pkg/planner/core/rule_decorrelate.go`:
- Around line 164-205: The loop that walks through nested LogicalApply nodes
(finalResult/nextApply) must stop and avoid pruning if any downstream apply has
IsLateral==true; currently only the top-level apply.IsLateral is checked. Modify
the traversal in the for loop that inspects child := finalResult.Children()[0] /
nextApply := child.(*logicalop.LogicalApply) to check nextApply.IsLateral and if
true return nil, false (or otherwise abort pruning) before assigning finalResult
= nextApply; this ensures any nested lateral apply prevents elimination and
preserves result multiplicity (also keep existing groupByColumn and schema
containment checks intact).

---

Nitpick comments:
In `@pkg/planner/core/casetest/enforcempp/testdata/enforce_mpp_suite_in.json`:
- Line 25: Update the inline SQL comment in the test string that currently reads
"index path huristic rule will prune tiflash path" to correct the typo to "index
path heuristic rule will prune tiflash path" in the SQL literal ("explain
format='brief' select  /*+ READ_FROM_STORAGE(TIFLASH[s]) */ a from s where a =
10 and b is null; -- index path huristic rule will prune tiflash path") so the
comment is spelled correctly for readability and grep-ability.

In `@pkg/planner/core/lateral_join_test.go`:
- Around line 35-41: The tests define an unused struct field expectedErrorCode
in the testCases slice and the follow-up conditional that checks it (referencing
expectedErrorCode) is dead; either remove the expectedErrorCode field from the
testCases struct and delete the conditional that inspects it, or set
expectedErrorCode values in each test case (e.g., 3809 for
ErrInvalidLateralJoin) and update the test assertions to compare against that
field; locate the testCases declaration and the conditional that reads
expectedErrorCode in lateral_join_test.go and apply one of these two changes to
eliminate the dead code.
- Around line 118-156: The test TestLateralJoinOptimization currently only
asserts that a plan builds and has a non-nil schema; either rename the test to
TestLateralJoinOptimizationBuilds to reflect that minimal check, or enhance it
to assert actual decorrelation/optimization outcomes by inspecting the logical
plan returned from BuildLogicalPlanForTest: for each case inspect the plan tree
to confirm whether Apply nodes (e.g., LogicalApply/Apply) remain or were
converted to Join nodes (e.g., LogicalJoin/Join) as expected (mirror the
approach used in TestLateralJoinDecorrelateWithUSINGAndON), failing the test
when the node types do not match the expected optimization result.

In `@tests/integrationtest/t/planner/cascades/integration.test`:
- Around line 199-239: Add explicit LATERAL cases to the existing cascades
planner tests: under the TestApply block add a positive inner/cross LATERAL case
(e.g. "select t1.a, x.b from t1 cross join lateral (select b from t2 where t2.a
= t1.a) as x;" with both explain format="plan_tree" and the select to assert
plan + result), and add unsupported-case assertions for LEFT/RIGHT JOIN LATERAL
(e.g. "select ... from t1 left join lateral (...)" and right join lateral) that
check the planner rejects or produces the expected unsupported-plan/error; place
these near the other apply/join examples so they exercise the same session
settings (tidb_enable_cascades_planner) and use the same test tables t1/t2 to
match existing patterns.

In `@tests/integrationtest/t/planner/core/lateral_join.test`:
- Around line 127-133: Update the two EXPLAIN assertions that are currently
brittle by adding the same --replace_column handling used in earlier tests:
modify the EXPLAIN for "select * from t1 join t2 using(a) join t3 on t2.b=t3.b,
lateral (select count(*) as c from t4 where t4.a=t2.a) as dt" (Line 127) and the
EXPLAIN for "select * from t1 join t2 using(a) join t3 on t2.b=t3.b, lateral
(select sum(t4.c) as s from t4 where t4.a=t2.a) as dt" (Line 133) to invoke
--replace_column for plan-id/estRows columns so the assertions ignore transient
plan identifiers; follow the same --replace_column syntax and column names used
in the nearby tests to ensure consistency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3fff7cea-ceda-4444-965d-0659405aed62

📥 Commits

Reviewing files that changed from the base of the PR and between cd39cb1 and cc21c81.

📒 Files selected for processing (40)
  • errors.toml
  • pkg/errno/errcode.go
  • pkg/errno/errname.go
  • pkg/parser/BUILD.bazel
  • pkg/parser/ast/dml.go
  • pkg/parser/keywords.go
  • pkg/parser/keywords_test.go
  • pkg/parser/lateral_test.go
  • pkg/parser/misc.go
  • pkg/parser/parser.go
  • pkg/parser/parser.y
  • pkg/planner/cascades/cascades_test.go
  • pkg/planner/core/BUILD.bazel
  • pkg/planner/core/casetest/enforcempp/testdata/enforce_mpp_suite_in.json
  • pkg/planner/core/casetest/enforcempp/testdata/enforce_mpp_suite_out.json
  • pkg/planner/core/casetest/enforcempp/testdata/enforce_mpp_suite_xut.json
  • pkg/planner/core/casetest/rule/BUILD.bazel
  • pkg/planner/core/casetest/rule/rule_outer2inner_test.go
  • pkg/planner/core/casetest/rule/testdata/outer2inner_in.json
  • pkg/planner/core/casetest/rule/testdata/outer2inner_out.json
  • pkg/planner/core/casetest/rule/testdata/outer2inner_xut.json
  • pkg/planner/core/expression_rewriter.go
  • pkg/planner/core/lateral_join_test.go
  • pkg/planner/core/logical_plan_builder.go
  • pkg/planner/core/operator/logicalop/hash64_equals_generated.go
  • pkg/planner/core/operator/logicalop/logical_apply.go
  • pkg/planner/core/operator/logicalop/logical_join.go
  • pkg/planner/core/planbuilder.go
  • pkg/planner/core/preprocess_test.go
  • pkg/planner/core/rule_decorrelate.go
  • pkg/planner/core/rule_join_reorder.go
  • pkg/util/dbterror/plannererrors/planner_terror.go
  • tests/integrationtest/r/cte.result
  • tests/integrationtest/r/executor/aggregate.result
  • tests/integrationtest/r/expression/misc.result
  • tests/integrationtest/r/planner/cascades/integration.result
  • tests/integrationtest/r/planner/core/integration.result
  • tests/integrationtest/r/planner/core/lateral_join.result
  • tests/integrationtest/t/planner/cascades/integration.test
  • tests/integrationtest/t/planner/core/lateral_join.test

Comment thread pkg/parser/ast/dml.go
Comment on lines +675 to +686
"SQL": "select * from t1, lateral (select t2.a2, count(*) as cnt from t2 where t2.b2 = t1.a1 group by t2.a2) as dt where dt.cnt > 0 -- LATERAL with aggregate: Apply must not be converted to Join",
"Plan": [
"Projection 64000.00 root test.t1.a1, test.t1.b1, test.t2.a2, Column#9",
"└─Apply 64000.00 root CARTESIAN inner join",
" ├─TableReader(Build) 10000.00 root data:TableFullScan",
" │ └─TableFullScan 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo",
" └─Selection(Probe) 64000.00 root gt(Column#9, 0)",
" └─HashAgg 80000.00 root group by:test.t2.a2, funcs:count(1)->Column#9, funcs:firstrow(test.t2.a2)->test.t2.a2",
" └─TableReader 100000.00 root data:Selection",
" └─Selection 100000.00 cop[tikv] eq(test.t2.b2, test.t1.a1)",
" └─TableFullScan 100000000.00 cop[tikv] table:t2 keep order:false, stats:pseudo"
]

@coderabbitai coderabbitai Bot Mar 17, 2026 •

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

SELECT * is losing test.t1.c1 in this golden plan.

Line 677 projects only test.t1.a1, test.t1.b1, test.t2.a2, Column#9, but the rest of this suite shows test.t1 has a1, b1, c1. If this is the real plan, the LATERAL aggregate path is dropping part of the outer row; if it is not, this fixture is baking in the wrong schema.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/casetest/rule/testdata/outer2inner_out.json` around lines
675 - 686, The golden plan's Projection node is missing test.t1.c1 (it lists
only test.t1.a1, test.t1.b1, test.t2.a2, Column#9), so either the planner output
was truncated or the test fixture schema baked into outer2inner_out.json is
wrong; update the Projection entry in outer2inner_out.json (the JSON under
"Plan" for the SQL containing "lateral ... count(*) as cnt") to include
test.t1.c1 alongside a1 and b1 (and verify Column#9 remains the aggregate
count), or if the planner should emit that column ensure the test harness
captures the full projection from the Projection node rather than dropping
columns produced by the outer TableFullScan/TableReader for test.t1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude code's response:
The golden output is correct. The table is create table t1(a1 int, b1 int) — only two columns. The Projection test.t1.a1, test.t1.b1, test.t2.a2, Column#9 correctly lists all columns from the SELECT *:

  • t1.a1, t1.b1 — both columns of t1
  • t2.a2 — from the LATERAL subquery's select t2.a2, count(*) as cnt
  • Column#9 — the count(*) aggregate

There is no c1 column in the schema, so nothing is missing. This appears to be a false positive from the reviewer — they may have been looking at a different test's table definition (e.g., TestOuter2Inner uses t1(a1 int, b1 int, c1 int) with 3 columns, but TestOuter2InnerLateralSelection creates its own t1(a1 int, b1 int) with only 2).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the test file for TestOuter2InnerLateralSelection to check the table schema
rg -n "TestOuter2InnerLateralSelection" --type=go -A 50 | head -80

Length of output: 5493


@terry1purcell, you're absolutely right — my original comment was a false positive. The script confirms that TestOuter2InnerLateralSelection defines t1(a1 int, b1 int) (Line 186), a two-column table with no c1. The Projection test.t1.a1, test.t1.b1, test.t2.a2, Column#9 correctly captures all columns from SELECT *:

  • t1.a1, t1.b1 — both columns of t1
  • t2.a2 — from the LATERAL subquery's select t2.a2, count(*)
  • Column#9 — the count(*) aggregate

I was incorrectly cross-referencing the schema from TestOuter2Inner (which uses t1(a1, b1, c1)). The golden plan fixture is correct as-is. Apologies for the noise!


🧠 Learnings used
Learnt from: CR
Repo: pingcap/tidb PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-12T14:43:04.903Z
Learning: Applies to pkg/planner/** : For planner rule or logical/physical plan changes, perform targeted planner unit tests and update rule testdata when needed.

Learnt from: CR
Repo: pingcap/tidb PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T04:39:16.731Z
Learning: Follow `.agents/skills/tidb-test-guidelines` for placement, naming, `shard_count` guidance, planner-specific casetest rules, and related testdata conventions

Learnt from: CR
Repo: pingcap/tidb PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-12T12:48:57.065Z
Learning: Applies to pkg/executor/** : For SQL behavior changes in executor, perform targeted unit test plus relevant integration test.

Learnt from: guo-shaoge
Repo: pingcap/tidb PR: 66349
File: tests/integrationtest/r/tpch.result:447-447
Timestamp: 2026-03-11T03:48:51.420Z
Learning: In `pkg/planner/core/joinorder/` (TiDB join reorder), cartesian edges should only be considered in two specific scenarios:
1. After the entire join reorder is complete and there are still disjoint join groups that need to be connected.
2. When a `leading` hint explicitly requires a cartesian join.
PR `#66213` attempted to fix case 2 but accidentally enabled case 1 as well. The correct logic ensures cartesian edges are gated to only these two situations.

Learnt from: cryo-zd
Repo: pingcap/tidb PR: 65532
File: pkg/planner/core/preprocess.go:1906-1910
Timestamp: 2026-03-12T08:03:56.097Z
Learning: In pingcap/tidb (PR `#65532`), `buildSelect` in `pkg/planner/core/logical_plan_builder.go` derives the privilege-check DB for `FOR UPDATE OF` lock targets using `getLowerDB(tName.Schema, ...)`. For alias or unqualified lock targets whose `TableName.Schema` is empty after preprocessing, this falls back to `currentDB` and authorizes against the wrong database. The correct fix is to use `tNameW.DBInfo.Name.L` (with a nil guard) directly, since `tNameW` is already fully resolved by `checkLockClauseTables` in preprocess.go. This is consistent with how privilege checks for INSERT/DELETE use `tnW.DBInfo.Name.L` elsewhere in the codebase.

Learnt from: cryo-zd
Repo: pingcap/tidb PR: 65532
File: pkg/planner/core/preprocess.go:1887-1898
Timestamp: 2026-03-12T08:41:44.509Z
Learning: In pkg/planner/core/preprocess.go (pingcap/tidb PR `#65532`), the `checkLockClauseTables` function intentionally accepts `FOR UPDATE OF <base_table>` even when the FROM item was introduced with an alias (`matched.alias.L != "" && !matchedByAlias`). Rather than returning an error (strict MySQL behavior), TiDB emits a warning to guide users toward the alias form. This backward-compatibility fallback is a deliberate acceptance criterion for the change — do not flag it as a MySQL-incompatibility bug in future reviews.

Comment thread pkg/planner/core/expression_rewriter.go
Comment thread pkg/planner/core/logical_plan_builder.go Outdated
Comment on lines +950 to +959
if fullSchema, fullNames := findJoinFullSchema(leftPlan); fullSchema != nil {
lFullSchema = fullSchema
lFullNames = fullNames
} else {
lFullSchema = leftPlan.Schema()
lFullNames = leftPlan.OutputNames()
}
// For LATERAL, right side is always a derived table (never a LogicalJoin with FullSchema)
rFullSchema = rightPlan.Schema()
rFullNames = rightPlan.OutputNames()

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

Preserve the right child's FullSchema here as well.

rightPlan is not always a plain derived table at this point; parenthesized right operands can already build into a LogicalJoin / LogicalApply with FullSchema from an inner USING / NATURAL join. Falling back to rightPlan.Schema() / OutputNames() drops those redundant columns, so upper qualified references and wildcards resolve against the wrong shape.

♻️ Suggested fix
-	// For LATERAL, right side is always a derived table (never a LogicalJoin with FullSchema)
-	rFullSchema = rightPlan.Schema()
-	rFullNames = rightPlan.OutputNames()
+	if fullSchema, fullNames := findJoinFullSchema(rightPlan); fullSchema != nil {
+		rFullSchema = fullSchema
+		rFullNames = fullNames
+	} else {
+		rFullSchema = rightPlan.Schema()
+		rFullNames = rightPlan.OutputNames()
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/logical_plan_builder.go` around lines 950 - 959, The right
child's FullSchema is being dropped by unconditionally using
rightPlan.Schema()/OutputNames(); instead call findJoinFullSchema on rightPlan
(just like done for leftPlan) and if it returns non-nil set rFullSchema and
rFullNames from that result, otherwise fall back to rightPlan.Schema() and
rightPlan.OutputNames(); reference the existing findJoinFullSchema function and
the rFullSchema/rFullNames variables (and consider LogicalJoin/LogicalApply as
possible rightPlan types that carry FullSchema).

Comment thread tests/integrationtest/r/planner/cascades/integration.result
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=2000001;
explain format='plan_tree' SELECT OUTR . col2 AS X FROM (SELECT INNR . col1 as col1, SUM( INNR . col2 ) as col2 FROM (SELECT INNR . `col_int_not_null` + 1 as col1, INNR . `pk` as col2 FROM BB AS INNR) AS INNR GROUP BY col1) AS OUTR2 INNER JOIN (SELECT INNR . col1 as col1, MAX( INNR . col2 ) as col2 FROM (SELECT INNR . `col_int_not_null` + 1 as col1, INNR . `pk` as col2 FROM BB AS INNR) AS INNR GROUP BY col1) AS OUTR ON OUTR2.col1 = OUTR.col1 GROUP BY OUTR . col1, OUTR2 . col1 HAVING X <> 'b';
Error 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column '.OUTR.' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
Error 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'OUTR.' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

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

Error text appears to drop the referenced column name.

At Line 2844, the expected message uses 'OUTR.', which loses the actual column identifier and makes the diagnostic less actionable. Please keep the full reference (e.g., OUTR.col2) and align the output with that.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integrationtest/r/planner/core/integration.result` at line 2844, The
expected error text currently shows a truncated column reference 'OUTR.'; update
the test fixture to use the full identifier (e.g., replace 'OUTR.' with
'OUTR.col2' in the expected output) and/or fix the error-message serialization
routine (e.g., the function that formats SQL errors or expression references
such as serializeExpression/formatSqlError) so it preserves the column name
after the table alias instead of dropping it; ensure the produced and expected
messages match exactly with the full reference 'OUTR.col2'.

create table t2 (a int, b varchar(10));
insert into t1 values (1), (2), (3);
insert into t2 values (1, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (3, 'e');
select * from t1, lateral (select b from t2 where t2.a = t1.a limit 1) as lat 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

Stabilize LIMIT 1 lateral subqueries to avoid flaky test outputs.

Line 6, Line 26, and Line 114 use LIMIT 1 without ORDER BY, so chosen rows are not deterministic.

Suggested deterministic update
-select * from t1, lateral (select b from t2 where t2.a = t1.a limit 1) as lat order by t1.a;
+select * from t1, lateral (select b from t2 where t2.a = t1.a order by b limit 1) as lat order by t1.a;

-select * from t1 left join lateral (select b from t2 where t2.a = t1.a limit 1) as lat on true order by t1.a;
+select * from t1 left join lateral (select b from t2 where t2.a = t1.a order by b limit 1) as lat on true order by t1.a;

-select * from t1, lateral (select b from t2 limit 1) as lat order by t1.a;
+select * from t1, lateral (select b from t2 order by b limit 1) as lat order by t1.a;

Also applies to: 26-26, 114-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integrationtest/r/planner/core/lateral_join.result` at line 6,
Stabilize the nondeterministic lateral subqueries by adding an explicit ORDER BY
before LIMIT 1 so the chosen row is deterministic; update the lateral clauses
like "lateral (select b from t2 where t2.a = t1.a limit 1) as lat" to include a
deterministic sort (for example "order by b" or the table's primary key) in each
occurrence (the queries containing the lateral subquery at the instances shown).

Comment on lines +1 to +282
# TestSimpleProjDual
set session tidb_enable_cascades_planner = 1;
explain format="plan_tree" select 1;
select 1;

# TestPKIsHandleRangeScan
drop table if exists t;
create table t(a int primary key, b int);
insert into t values(1,2),(3,4),(5,6);
set session tidb_enable_cascades_planner = 1;
explain format="plan_tree" select b from t where a > 1;
select b from t where a > 1;
explain format="plan_tree" select b from t where a > 1 and a < 3;
select b from t where a > 1 and a < 3;
explain format="plan_tree" select b from t where a > 1 and b < 6;
select b from t where a > 1 and b < 6;
explain format="plan_tree" select a from t where a * 3 + 1 > 9 and a < 5;
select a from t where a * 3 + 1 > 9 and a < 5;
explain format="plan_tree" select a from t group by a having sum(b) > 4;
select a from t group by a having sum(b) > 4;

# TestIndexScan
drop table if exists t;
create table t(a int primary key, b int, c int, d int, index idx_b(b), index idx_c_b(c, b));
insert into t values(1,2,3,100),(4,5,6,200),(7,8,9,300);
set session tidb_enable_cascades_planner = 1;
explain format="plan_tree" select b from t;
select b from t;
explain format="plan_tree" select a from t order by b;
select a from t order by b;
explain format="plan_tree" select c from t;
select c from t;
explain format="plan_tree" select a from t order by c;
select a from t order by c;
explain format="plan_tree" select a, b from t where b > 5 order by b;
select a, b from t where b > 5 order by b;
explain format="plan_tree" select a, b, c from t where c = 3 and b > 1 order by b;
select a, b, c from t where c = 3 and b > 1 order by b;
explain format="plan_tree" select a, b from t where c > 1 and b > 1 order by c;
select a, b from t where c > 1 and b > 1 order by c;

# TestBasicShow
drop table if exists t;
create table t(a int primary key, b int);
set session tidb_enable_cascades_planner = 1;
desc t;

# TestSort
drop table if exists t;
create table t(a int primary key, b int);
insert into t values (1, 11), (4, 44), (2, 22), (3, 33);
set session tidb_enable_cascades_planner = 1;
explain format="plan_tree" select a from t order by a;
select a from t order by a;
explain format="plan_tree" select b from t order by b;
select b from t order by b;
explain format="plan_tree" select b from t order by a+b;
select b from t order by a+b;
explain format="plan_tree" select b from t order by b, a+b, a;
select b from t order by b, a+b, a;

# TestAggregation
drop table if exists t;
create table t(a int primary key, b int);
insert into t values (1, 11), (4, 44), (2, 22), (3, 33);
set session tidb_enable_cascades_planner = 1;
set session tidb_executor_concurrency = 4;
set @@session.tidb_hash_join_concurrency = 5;
set @@session.tidb_distsql_scan_concurrency = 15;
explain format="plan_tree" select sum(a) from t;
select sum(a) from t;
explain format="plan_tree" select max(a), min(b) from t;
select max(a), min(b) from t;
explain format="plan_tree" select b, avg(a) from t group by b order by b;
select b, avg(a) from t group by b order by b;
explain format="plan_tree" select b, sum(a) from t group by b order by b;
select b, sum(a) from t group by b order by b;
explain format="plan_tree" select b, avg(a) from t group by b having sum(a) > 1 order by b;
select b, avg(a) from t group by b having sum(a) > 1 order by b;
explain format="plan_tree" select max(a+b) from t;
select max(a+b) from t;
explain format="plan_tree" select sum(a) from t group by a, a+b order by a;
select sum(a) from t group by a, a+b order by a;
explain format="plan_tree" select b, sum(a) from t group by b having b > 1 order by b;
select b, sum(a) from t group by b having b > 1 order by b;
explain format="plan_tree" select c, sum(a) from (select a+b as c, a from t) t1 group by c having c > 1 order by c;
select c, sum(a) from (select a+b as c, a from t) t1 group by c having c > 1 order by c;
explain format="plan_tree" select max(a.a) from t a left join t b on a.a = b.a;
select max(a.a) from t a left join t b on a.a = b.a;
explain format="plan_tree" select avg(a.b) from t a left join t b on a.a = b.a;
select avg(a.b) from t a left join t b on a.a = b.a;
explain format="plan_tree" select t1.a, max(t1.b) from t as t1 left join (select * from t) as t2 on t1.a = t2.a and t1.b = 3 group by t1.a order by a;
select t1.a, max(t1.b) from t as t1 left join (select * from t) as t2 on t1.a = t2.a and t1.b = 3 group by t1.a order by a;
explain format="plan_tree" select t1.a, sum(distinct t1.b) from t as t1 left join (select * from t) as t2 on t1.b = t2.b group by t1.a order by a;
select t1.a, sum(distinct t1.b) from t as t1 left join (select * from t) as t2 on t1.b = t2.b group by t1.a order by a;
explain format="plan_tree" select t2.a, max(t2.b) from t as t1 right join (select * from t) as t2 on t1.a = t2.a group by t2.a order by a;
select t2.a, max(t2.b) from t as t1 right join (select * from t) as t2 on t1.a = t2.a group by t2.a order by a;
explain format="plan_tree" select t3.a, max(t3.b) from (select t1.a, t1.b from t as t1 left join t as t2 on t1.b = t2.b) t3 group by t3.a order by a;
select t3.a, max(t3.b) from (select t1.a, t1.b from t as t1 left join t as t2 on t1.b = t2.b) t3 group by t3.a order by a;
explain format="plan_tree" select max(a) from t;
select max(a) from t;
explain format="plan_tree" select sum(case when a > 0 and a <= 1000 then b else 0 end) from t;
select sum(case when a > 0 and a <= 1000 then b else 0 end) from t;
explain format="plan_tree" select sum(case when a > 0 then (case when a <= 1000 then b end) else 0 end) from t;
select sum(case when a > 0 then (case when a <= 1000 then b end) else 0 end) from t;
explain format="plan_tree" select sum(case when a <= 0 or a > 1000 then 0.0 else b end) from t;
select sum(case when a <= 0 or a > 1000 then 0.0 else b end) from t;
explain format="plan_tree" select count(case when a > 0 and a <= 1000 then b end) from t;
select count(case when a > 0 and a <= 1000 then b end) from t;
explain format="plan_tree" select count(case when a <= 0 or a > 1000 then null else b end) from t;
select count(case when a <= 0 or a > 1000 then null else b end) from t;
explain format="plan_tree" select count(distinct case when a > 0 and a <= 1000 then b end) from t;
select count(distinct case when a > 0 and a <= 1000 then b end) from t;
explain format="plan_tree" select approx_count_distinct(case when a > 0 and a <= 1000 then b end) from t;
select approx_count_distinct(case when a > 0 and a <= 1000 then b end) from t;
explain format="plan_tree" select count(b), sum(b), avg(b), b, max(b), min(b), bit_and(b), bit_or(b), bit_xor(b) from t group by a having sum(b) >= 0 and count(b) >= 0 order by b;
select count(b), sum(b), avg(b), b, max(b), min(b), bit_and(b), bit_or(b), bit_xor(b) from t group by a having sum(b) >= 0 and count(b) >= 0 order by b;
explain format="plan_tree" select group_concat(a, b), min(b), avg(a / b), a from t group by (a+b), a order by a;
select group_concat(a, b), min(b), avg(a / b), a from t group by (a+b), a order by a;
explain format="plan_tree" select approx_percentile(a, 50) from t order by b;
select approx_percentile(a, 50) from t order by b;

# TestPushdownDistinctEnable
drop table if exists t;
create table t(a int, b int, c int, index(c));
insert into t values (1, 1, 1), (1, 1, 3), (1, 2, 3), (2, 1, 3), (1, 2, NULL);
set session sql_mode='';
set session tidb_hashagg_partial_concurrency=1;
set session tidb_hashagg_final_concurrency=1;
set session tidb_enable_cascades_planner = 1;
set @@session.tidb_opt_distinct_agg_push_down = 1;
explain format="plan_tree" select /*+ HASH_AGG() */ avg(distinct a) from t;
select /*+ HASH_AGG() */ avg(distinct a) from t;
explain format="plan_tree" select /*+ HASH_AGG() */ a, count(distinct a) from t;
--replace_column 1 1
select /*+ HASH_AGG() */ a, count(distinct a) from t;
explain format="plan_tree" select /*+ HASH_AGG() */ avg(b), c, avg(b), count(distinct A, B), count(distinct A), count(distinct c), sum(b) from t group by c;
--sorted_result
select /*+ HASH_AGG() */ avg(b), c, avg(b), count(distinct A, B), count(distinct A), count(distinct c), sum(b) from t group by c;
explain format="plan_tree" select /*+ STREAM_AGG() */ count(distinct c) from t group by c;
--sorted_result
select /*+ STREAM_AGG() */ count(distinct c) from t group by c;
explain format="plan_tree" select /*+ STREAM_AGG() */ count(distinct c) from t;
select /*+ STREAM_AGG() */ count(distinct c) from t;
explain format="plan_tree" select /*+ HASH_AGG() */ count(distinct c) from t;
select /*+ HASH_AGG() */ count(distinct c) from t;
explain format="plan_tree" select count(distinct c) from t group by c;
--sorted_result
select count(distinct c) from t group by c;
explain format="plan_tree" select count(distinct c) from t;
select count(distinct c) from t;

# TestPushdownDistinctDisable
drop table if exists t;
create table t(a int, b int, c int, index(c));
insert into t values (1, 1, 1), (1, 1, 3), (1, 2, 3), (2, 1, 3), (1, 2, NULL);
set session sql_mode='';
set session tidb_hashagg_partial_concurrency=1;
set session tidb_hashagg_final_concurrency=1;
set session tidb_enable_cascades_planner = 1;
set @@session.tidb_opt_distinct_agg_push_down = 0;
explain format="plan_tree" select /*+ HASH_AGG(), AGG_TO_COP() */ avg(distinct a) from t;
select /*+ HASH_AGG(), AGG_TO_COP() */ avg(distinct a) from t;
explain format="plan_tree" select /*+ HASH_AGG(), AGG_TO_COP() */ a, count(distinct a) from t;
select /*+ HASH_AGG(), AGG_TO_COP() */ a, count(distinct a) from t;
explain format="plan_tree" select /*+ HASH_AGG(), AGG_TO_COP() */ avg(b), c, avg(b), count(distinct A, B), count(distinct A), count(distinct c), sum(b) from t group by c;
select /*+ HASH_AGG(), AGG_TO_COP() */ avg(b), c, avg(b), count(distinct A, B), count(distinct A), count(distinct c), sum(b) from t group by c;
explain format="plan_tree" select /*+ STREAM_AGG(), AGG_TO_COP() */ count(distinct c) from t group by c;
select /*+ STREAM_AGG(), AGG_TO_COP() */ count(distinct c) from t group by c;
explain format="plan_tree" select /*+ STREAM_AGG(), AGG_TO_COP() */ count(distinct c) from t;
select /*+ STREAM_AGG(), AGG_TO_COP() */ count(distinct c) from t;
explain format="plan_tree" select /*+ HASH_AGG(), AGG_TO_COP() */ count(distinct c) from t;
select /*+ HASH_AGG(), AGG_TO_COP() */ count(distinct c) from t;
explain format="plan_tree" select /*+ AGG_TO_COP() */ count(distinct c) from t group by c;
select /*+ AGG_TO_COP() */ count(distinct c) from t group by c;

# TestSimplePlans
drop table if exists t;
create table t(a int primary key, b int);
insert into t values (1, 11), (4, 44), (2, 22), (3, 33);
set session tidb_enable_cascades_planner = 1;
explain format="plan_tree" select a from t limit 2;
select a from t limit 2;
explain format="plan_tree" select a from t limit 1 offset 2;
select a from t limit 1 offset 2;
explain format="plan_tree" select b from t order by b limit 3;
select b from t order by b limit 3;
explain format="plan_tree" select a from t order by a limit 1 offset 2;
select a from t order by a limit 1 offset 2;
explain format="plan_tree" select * from ((select a as aa from t t1) union all (select b as aa from t t2)) as t3 order by aa;
select * from ((select a as aa from t t1) union all (select b as aa from t t2)) as t3 order by aa;
explain format="plan_tree" select a, b, lag(a,1) over (order by b) from t order by b;
select a, b, lag(a,1) over (order by b) from t order by b;
explain format="plan_tree" select * from (select a+1 as c, a+b as d from t) as t1 order by c+d limit 10;
select * from (select a+1 as c, a+b as d from t) as t1 order by c+d limit 10;
explain format="plan_tree" select t1.a, t1.b from t as t1 left join t as t2 on t1.a = t2.a and t1.b = 3 order by a;
select t1.a, t1.b from t as t1 left join t as t2 on t1.a = t2.a and t1.b = 3 order by a;

# TestJoin
set @@session.tidb_executor_concurrency = 4;
set @@session.tidb_hash_join_concurrency = 5;
set @@session.tidb_distsql_scan_concurrency = 15;
drop table if exists t1;
drop table if exists t2;
create table t1(a int primary key, b int);
create table t2(a int primary key, b int);
insert into t1 values (1, 11), (4, 44), (2, 22), (3, 33);
insert into t2 values (1, 111), (2, 222), (3, 333), (5, 555);
set session tidb_enable_cascades_planner = 1;
explain format="plan_tree" select t1.a, t1.b from t1, t2 where t1.a = t2.a and t1.a > 2;
select t1.a, t1.b from t1, t2 where t1.a = t2.a and t1.a > 2;
explain format="plan_tree" select t1.a, t1.b from t1, t2 where t1.a > t2.a and t2.b > 200;
select t1.a, t1.b from t1, t2 where t1.a > t2.a and t2.b > 200;
explain format="plan_tree" select t1.a, t1.b from t1 left join t2 on t1.a = t2.a where t1.a > 2 and t2.b > 200;
select t1.a, t1.b from t1 left join t2 on t1.a = t2.a where t1.a > 2 and t2.b > 200;
explain format="plan_tree" select t2.a, t2.b from t1 right join t2 on t1.a = t2.a where t1.a > 2 and t2.b > 200;
select t2.a, t2.b from t1 right join t2 on t1.a = t2.a where t1.a > 2 and t2.b > 200;
explain format="plan_tree" select t1.a, t1.b from t1, t2 where t1.a = t2.a order by t1.a;
select t1.a, t1.b from t1, t2 where t1.a = t2.a order by t1.a;
explain format="plan_tree" select * from t1 join t2 on t1.a = t2.a;
select * from t1 join t2 on t1.a = t2.a;
explain format="plan_tree" select * from t1 join t2 on t1.a = t2.a and t1.a > 2;
select * from t1 join t2 on t1.a = t2.a and t1.a > 2;

# TestApply
drop table if exists t1, t2;
create table t1(a int primary key, b int);
create table t2(a int primary key, b int);
insert into t1 values (1, 11), (4, 44), (2, 22), (3, 33);
insert into t2 values (1, 11), (2, 22), (3, 33);
set session tidb_enable_cascades_planner = 1;
explain format="plan_tree" select a = (select a from t2 where t1.b = t2.b order by a limit 1) from t1;
select a = (select a from t2 where t1.b = t2.b order by a limit 1) from t1;
explain format="plan_tree" select sum(a), (select t1.a from t1 where t1.a = t2.a limit 1), (select t1.b from t1 where t1.b = t2.b limit 1) from t2;
select sum(a), (select t1.a from t1 where t1.a = t2.a limit 1), (select t1.b from t1 where t1.b = t2.b limit 1) from t2;
explain format="plan_tree" select /*+ set_var(tidb_hash_join_version=legacy) */ a from t1 where exists(select 1 from t2 where t1.a = t2.a);
explain format="plan_tree" select /*+ set_var(tidb_hash_join_version=optimized) */ a from t1 where exists(select 1 from t2 where t1.a = t2.a);
select a from t1 where exists(select 1 from t2 where t1.a = t2.a);

# TestMemTableScan
set session tidb_enable_cascades_planner = 1;
explain format="plan_tree" select USER, DB, COMMAND, TIME, STATE, INFO, `DIGEST` from information_schema.processlist where DB = "planner__cascades__integration";
select USER, DB, COMMAND, TIME, STATE, INFO, `DIGEST` from information_schema.processlist where DB = "planner__cascades__integration";

# TestTopN
drop table if exists t;
create table t(a int primary key, b int);
insert into t values (1, 11), (4, 44), (2, 22), (3, 33);
set session tidb_enable_cascades_planner = 1;
explain format="plan_tree" select a from (select a from t where b > 2 order by a limit 3 offset 1) as t1 order by a limit 2 offset 1;
select a from (select a from t where b > 2 order by a limit 3 offset 1) as t1 order by a limit 2 offset 1;
explain format="plan_tree" select * from (select * from t order by a limit 3) as t1 order by a limit 5;
select * from (select * from t order by a limit 3) as t1 order by a limit 5;
explain format="plan_tree" select b from (select b from t order by b limit 10 offset 10) as t1 order by b limit 10 offset 5;
select b from (select b from t order by b limit 10 offset 10) as t1 order by b limit 10 offset 5;
explain format="plan_tree" select b from (select b from t order by b limit 10 offset 2) as t1 order by b limit 3 offset 5;
select b from (select b from t order by b limit 10 offset 2) as t1 order by b limit 3 offset 5;
explain format="plan_tree" select a from (select a from t order by a limit 3 offset 5) as t1 order by a limit 3 offset 5;
select a from (select a from t order by a limit 3 offset 5) as t1 order by a limit 3 offset 5;
explain format="plan_tree" select a from (select a from t where b > 2 order by a, b limit 3 offset 1) as t1 order by a limit 2 offset 1;
select a from (select a from t where b > 2 order by a, b limit 3 offset 1) as t1 order by a limit 2 offset 1;
explain format="plan_tree" select * from (select * from t order by a limit 3) as t1 order by a, b limit 5;
select * from (select * from t order by a limit 3) as t1 order by a, b limit 5;


# TestCascadePlannerHashedPartTable
SET SESSION tidb_opt_fix_control = '44262:ON';
drop table if exists pt1;
create table pt1(a bigint, b bigint) partition by hash(a) partitions 4;
insert into pt1 values(1,10);
insert into pt1 values(2,20);
insert into pt1 values(3,30);
insert into pt1 values(4,40);
insert into pt1 values(5,50);
set @@tidb_enable_cascades_planner = 1;
explain format="plan_tree" select * from pt1 order by a;
select * from pt1 order by a;

set session tidb_opt_fix_control = default;
set @@tidb_enable_cascades_planner = default;


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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

TEST_FILE="tests/integrationtest/t/planner/cascades/integration.test"
RESULT_FILE="tests/integrationtest/r/planner/cascades/integration.result"

echo "Checking result file presence..."
fd -p "$(basename "$RESULT_FILE")" tests/integrationtest/r/planner/cascades

python - <<'PY'
import re
from pathlib import Path

test_file = Path("tests/integrationtest/t/planner/cascades/integration.test")
result_file = Path("tests/integrationtest/r/planner/cascades/integration.result")

if not result_file.exists():
    print(f"ERROR: missing {result_file}")
    raise SystemExit(1)

def case_headers(path: Path):
    out = []
    for line in path.read_text().splitlines():
        m = re.match(r"^#\s*(.+)$", line)
        if m:
            out.append(m.group(1).strip())
    return out

tc = case_headers(test_file)
rc = case_headers(result_file)

print(f"test headers:   {len(tc)}")
print(f"result headers: {len(rc)}")

missing = [c for c in tc if c not in rc]
extra = [c for c in rc if c not in tc]

print("missing_in_result:", missing[:20])
print("extra_in_result:", extra[:20])

if missing:
    raise SystemExit(2)
PY

Repository: pingcap/tidb

Length of output: 513


Regenerate the .result file with all test case results.

The .result file at tests/integrationtest/r/planner/cascades/integration.result exists but contains no test case results. All 14 test cases in the corresponding .test file (TestSimpleProjDual, TestPKIsHandleRangeScan, TestIndexScan, TestBasicShow, TestSort, TestAggregation, TestPushdownDistinctEnable, TestPushdownDistinctDisable, TestSimplePlans, TestJoin, TestApply, TestMemTableScan, TestTopN, TestCascadePlannerHashedPartTable) must be regenerated and checked in with their expected output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integrationtest/t/planner/cascades/integration.test` around lines 1 -
282, The result file tests/integrationtest/r/planner/cascades/integration.result
is empty and must be populated with the expected outputs for the 14 tests
defined in tests/integrationtest/t/planner/cascades/integration.test (e.g.,
TestSimpleProjDual, TestPKIsHandleRangeScan, TestIndexScan, TestBasicShow,
TestSort, TestAggregation, TestPushdownDistinctEnable,
TestPushdownDistinctDisable, TestSimplePlans, TestJoin, TestApply,
TestMemTableScan, TestTopN, TestCascadePlannerHashedPartTable); run the
integration test harness (or the equivalent test runner used by the repo)
against that .test file with the correct environment flags
(tidb_enable_cascades_planner etc.), capture the explain/select outputs, replace
the empty integration.result with the regenerated outputs, and commit the
updated tests/integrationtest/r/planner/cascades/integration.result so it
contains the expected results for each named test case.

Comment on lines +164 to +175
explain format="plan_tree" select /*+ HASH_AGG(), AGG_TO_COP() */ a, count(distinct a) from t;
select /*+ HASH_AGG(), AGG_TO_COP() */ a, count(distinct a) from t;
explain format="plan_tree" select /*+ HASH_AGG(), AGG_TO_COP() */ avg(b), c, avg(b), count(distinct A, B), count(distinct A), count(distinct c), sum(b) from t group by c;
select /*+ HASH_AGG(), AGG_TO_COP() */ avg(b), c, avg(b), count(distinct A, B), count(distinct A), count(distinct c), sum(b) from t group by c;
explain format="plan_tree" select /*+ STREAM_AGG(), AGG_TO_COP() */ count(distinct c) from t group by c;
select /*+ STREAM_AGG(), AGG_TO_COP() */ count(distinct c) from t group by c;
explain format="plan_tree" select /*+ STREAM_AGG(), AGG_TO_COP() */ count(distinct c) from t;
select /*+ STREAM_AGG(), AGG_TO_COP() */ count(distinct c) from t;
explain format="plan_tree" select /*+ HASH_AGG(), AGG_TO_COP() */ count(distinct c) from t;
select /*+ HASH_AGG(), AGG_TO_COP() */ count(distinct c) from t;
explain format="plan_tree" select /*+ AGG_TO_COP() */ count(distinct c) from t group by c;
select /*+ AGG_TO_COP() */ count(distinct c) from t group by c;

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

Stabilize nondeterministic assertions in TestPushdownDistinctDisable.

select ... a, count(distinct a) on Line [165] is nondeterministic with sql_mode='', and the GROUP BY c result sets on Lines [167], [169], and [175] can reorder. Please mirror the normalization directives already used in TestPushdownDistinctEnable.

Proposed patch
 explain format="plan_tree" select /*+ HASH_AGG(), AGG_TO_COP() */ a, count(distinct a) from t;
+--replace_column 1 1
 select /*+ HASH_AGG(), AGG_TO_COP() */ a, count(distinct a) from t;
 explain format="plan_tree" select /*+ HASH_AGG(), AGG_TO_COP() */ avg(b), c, avg(b), count(distinct A, B),  count(distinct A), count(distinct c), sum(b) from t group by c;
+--sorted_result
 select /*+ HASH_AGG(), AGG_TO_COP() */ avg(b), c, avg(b), count(distinct A, B),  count(distinct A), count(distinct c), sum(b) from t group by c;
 explain format="plan_tree" select /*+ STREAM_AGG(), AGG_TO_COP() */ count(distinct c) from t group by c;
+--sorted_result
 select /*+ STREAM_AGG(), AGG_TO_COP() */ count(distinct c) from t group by c;
@@
 explain format="plan_tree" select /*+ AGG_TO_COP() */ count(distinct c) from t group by c;
+--sorted_result
 select /*+ AGG_TO_COP() */ count(distinct c) from t group by c;

Based on learnings: Follow .agents/skills/tidb-test-guidelines for planner-specific casetest rules and related testdata conventions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integrationtest/t/planner/cascades/integration.test` around lines 164 -
175, TestPushdownDistinctDisable contains nondeterministic assertions (the query
with "a, count(distinct a)" and the several "GROUP BY c" selects) so mirror the
normalization directives used in TestPushdownDistinctEnable: add the same
output-normalization/comments (the stable result ordering/normalization
directives) immediately before the affected statements in
TestPushdownDistinctDisable so the plan/result comparisons are deterministic for
the "select /*+ ... */ a, count(distinct a) ..." case and for all "group by c"
queries; ensure you apply the directives to the explain/select pairs that
include "count(distinct ...)" and "group by c" so they follow the same
normalization rules as TestPushdownDistinctEnable.

}
if x.AsName.L != "" {
name.TblName = x.AsName
// Clone output names before modifying to avoid mutating shared structs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P2] LATERAL derived-table column alias list parsed but not applied

Why: MySQL compatibility issue - queries using LATERAL ... AS dt(c1, c2) syntax will fail name resolution because the planner never applies the column alias list to the derived table's output fields.

Evidence: The parser sets ts.ColumnNames = $4.([]ast.CIStr) at pkg/parser/parser.y:10408, but the planner at pkg/planner/core/logical_plan_builder.go:500 only rewrites the table alias (TblName) and keeps ColName: name.ColName unchanged, never consulting x.ColumnNames. This means dt.c1 cannot resolve and the column list can't fix duplicate output-name errors.

Impact: Affects LATERAL derived tables using the optional column-list syntax. Workaround: use SELECT ... AS c1, ... AS c2 inside the subquery.

lFullSchema = leftPlan.Schema()
lFullNames = leftPlan.OutputNames()
}
// For LATERAL, right side is always a derived table (never a LogicalJoin with FullSchema)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] buildLateralJoin drops right-side FullSchema/FullNames

Why: Can break redundant-column name resolution (USING/NATURAL joins) when a LATERAL inside a parenthesized right subtree forces an outer Apply. This causes "Unknown column" errors for otherwise-valid SQL.

Evidence: At pkg/planner/core/logical_plan_builder.go:957, the code hardcodes rFullSchema = rightPlan.Schema(); rFullNames = rightPlan.OutputNames() under the assumption that "the right side is always a derived table". However, when the right subtree is a join tree containing a LATERAL node, the right plan can be a LogicalJoin/LogicalApply that already has FullSchema/FullNames with USING/NATURAL redundant columns. Dropping them breaks name resolution.

Minimal reproducer: SELECT t3.a FROM t AS t1 JOIN (t AS t2 JOIN t AS t3 USING(a) JOIN LATERAL (SELECT 1) AS dt ON true) ON true → yields [planner:1054]Unknown column 't3.a' in 'field list' because the redundant t3.a only exists in the right subtree FullSchema, which the outer Apply drops.

Note: By contrast, non-LATERAL joins correctly propagate FullSchema/FullNames at pkg/planner/core/logical_plan_builder.go:779.

… sources

The previous guard only checked *TableName but still allowed LATERAL
and ColumnNames on other non-derived sources like *Join. Switch to
an allowlist: only permit these fields when Source is *SelectStmt or
*SetOprStmt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pkg/parser/ast/dml.go (1)

555-559: Avoid duplicated derived-source checks in Restore.

needParen repeats the same source-type test already used for isDerived. Reuse isDerived to keep one source of truth.

♻️ Suggested cleanup
-	isDerived := false
-	switch n.Source.(type) {
-	case *SelectStmt, *SetOprStmt:
-		isDerived = true
-	}
+	isDerived := false
+	switch n.Source.(type) {
+	case *SelectStmt, *SetOprStmt:
+		isDerived = true
+	}
@@
-	needParen := false
-	switch n.Source.(type) {
-	case *SelectStmt, *SetOprStmt:
-		needParen = true
-	}
+	needParen := isDerived

As per coding guidelines "Code SHOULD remain maintainable for future readers with basic TiDB familiarity; follow existing package-local conventions first and keep style consistent with nearby files."

Also applies to: 570-574

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/parser/ast/dml.go` around lines 555 - 559, The Restore method currently
computes isDerived via a type switch on n.Source and then recomputes the same
test in the needParen logic; update Restore to compute isDerived once (using the
existing type switch for *SelectStmt and *SetOprStmt) and then have needParen
reference that isDerived variable instead of redoing the type assertion, and
remove the duplicate source-type checks (including the secondary block around
the code at the other occurrence between the regions corresponding to lines
~570-574) so there is a single source-of-truth for derived-source detection.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/parser/ast/dml.go`:
- Around line 555-559: The Restore method currently computes isDerived via a
type switch on n.Source and then recomputes the same test in the needParen
logic; update Restore to compute isDerived once (using the existing type switch
for *SelectStmt and *SetOprStmt) and then have needParen reference that
isDerived variable instead of redoing the type assertion, and remove the
duplicate source-type checks (including the secondary block around the code at
the other occurrence between the regions corresponding to lines ~570-574) so
there is a single source-of-truth for derived-source detection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0c4c0141-f729-44e1-9912-e94d0b2b7d64

📥 Commits

Reviewing files that changed from the base of the PR and between cc21c81 and e987506.

📒 Files selected for processing (1)
  • pkg/parser/ast/dml.go

terry1purcell and others added 2 commits March 17, 2026 08:53
…gJoinPlan

LogicalApply embeds LogicalJoin and has the same FullSchema/FullNames
fields for USING/NATURAL column remapping. Without this case, qualified
column names over a LATERAL Apply would bind to names not present in
Apply.Schema(), breaking later index resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ableSource

Move the buildingLateralSubquery flag from buildJoin (where it covers
the entire right branch including non-LATERAL siblings) into
buildResultSetNode's TableSource handler (where x.Lateral is checked).

This prevents non-LATERAL derived tables from bypassing recursive-CTE
ORDER BY/LIMIT checks when they happen to be siblings of a LATERAL
table in the same join tree.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tiprow

tiprow Bot commented Mar 17, 2026

Copy link
Copy Markdown

@terry1purcell: 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 c3d9120 link true /test fast_test_tiprow

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
pkg/planner/core/logical_plan_builder.go (1)

4064-4089: ⚠️ Potential issue | 🟠 Major

Unwrap wrapper plans before expanding qualified *.

Top-level inner joins with ON are usually LogicalSelection over the real join/apply. This direct type switch misses the underlying FullSchema, so tbl.* on top of a USING/NATURAL subtree still drops the redundant table-qualified columns.

♻️ Suggested fix
-	// Extract FullSchema/FullNames from LogicalJoin or LogicalApply (which embeds LogicalJoin).
-	var fullSchema *expression.Schema
-	var fullNames types.NameSlice
-	switch x := p.(type) {
-	case *logicalop.LogicalJoin:
-		fullSchema, fullNames = x.FullSchema, x.FullNames
-	case *logicalop.LogicalApply:
-		fullSchema, fullNames = x.FullSchema, x.FullNames
-	}
+	fullSchema, fullNames := findJoinFullSchema(p)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/logical_plan_builder.go` around lines 4064 - 4089, In
unfoldWildStar, the type switch on p only recognizes *logicalop.LogicalJoin and
*logicalop.LogicalApply and therefore misses wrapped joins (e.g., a
*logicalop.LogicalSelection that wraps the real join), causing qualified `tbl.*`
to lose FullSchema/FullNames; fix by unwrapping simple unary wrapper plans
before the switch (e.g., while p is a *logicalop.LogicalSelection or other
single-child wrapper, set p = p.Children()[0] or use the plan's embedded child
accessor) so that the subsequent switch in unfoldWildStar sees the underlying
*logicalop.LogicalJoin or *logicalop.LogicalApply and can obtain FullSchema and
FullNames correctly.
♻️ Duplicate comments (2)
pkg/planner/core/logical_plan_builder.go (2)

918-963: ⚠️ Potential issue | 🟠 Major

Don't treat the whole right subtree as a plain derived table.

For parenthesized operands that contain a LATERAL source, rightPlan can already be a LogicalJoin/LogicalApply with FullSchema and base-table OutputNames. Using rightPlan.Schema()/OutputNames() here drops redundant USING/NATURAL columns again, and the unconditional DB-name clearing above also de-qualifies those base-table outputs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/logical_plan_builder.go` around lines 918 - 963, The code
incorrectly assumes the entire right subtree is a derived table: when building
outputNames and setting rFullSchema/rFullNames, detect whether rightPlan already
has a join-style FullSchema/FullNames (use the same helper as left, e.g. call
findJoinFullSchema(rightPlan)) and, if present, use that FullSchema/FullNames
and preserve the existing right output FieldName.DBName/qualification instead of
clearing DBName; only clear DBName and treat outputs as derived-table names when
no FullSchema/FullNames are found for rightPlan. Ensure ap.SetOutputNames uses
the chosen name slice consistently and that rFullSchema/rFullNames are set from
the findJoinFullSchema result when non-nil.

505-550: ⚠️ Potential issue | 🟠 Major

Apply the derived-table column alias list here.

This branch only rewrites TblName, so ... AS dt(c1, c2) still exposes the inner ColNames. dt.c1 then cannot resolve, and the duplicate-name check below runs against the wrong names.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/logical_plan_builder.go` around lines 505 - 550, When
cloning output names for an aliased table (the block using x.AsName,
isTableName, p.OutputNames(), and p.SetOutputNames), also apply any
derived-table column alias list so the FieldName.ColName reflects the alias list
instead of the inner names; detect the alias list on the table alias node (e.g.
the derived-table column alias container on x/alias node) and for each i set
clonedNames[i].ColName to the corresponding alias when present (preserving
Hidden, DBName handling, and other flags), then call
p.SetOutputNames(clonedNames) so the duplicate-name check and resolution use the
alias names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/planner/core/logical_plan_builder.go`:
- Around line 771-791: The current logic only preserves FullSchema/FullNames
when leftPlan/rightPlan are directly LogicalJoin/LogicalApply, dropping them
when wrapped (e.g., by LogicalSelection). Update the checks in the join-building
block to also detect a LogicalSelection wrapper: if leftPlan (or rightPlan) is
*logicalop.LogicalSelection and its child is a *logicalop.LogicalJoin or
*logicalop.LogicalApply with non-nil FullSchema, set lFullSchema/lFullNames (or
rFullSchema/rFullNames) from the child.FullSchema/child.FullNames instead of
falling back to Schema()/OutputNames(); otherwise keep the existing fallback.
Ensure you reference leftPlan/rightPlan, logicalop.LogicalSelection,
logicalop.LogicalJoin, logicalop.LogicalApply, FullSchema and FullNames in the
updated checks.

---

Outside diff comments:
In `@pkg/planner/core/logical_plan_builder.go`:
- Around line 4064-4089: In unfoldWildStar, the type switch on p only recognizes
*logicalop.LogicalJoin and *logicalop.LogicalApply and therefore misses wrapped
joins (e.g., a *logicalop.LogicalSelection that wraps the real join), causing
qualified `tbl.*` to lose FullSchema/FullNames; fix by unwrapping simple unary
wrapper plans before the switch (e.g., while p is a *logicalop.LogicalSelection
or other single-child wrapper, set p = p.Children()[0] or use the plan's
embedded child accessor) so that the subsequent switch in unfoldWildStar sees
the underlying *logicalop.LogicalJoin or *logicalop.LogicalApply and can obtain
FullSchema and FullNames correctly.

---

Duplicate comments:
In `@pkg/planner/core/logical_plan_builder.go`:
- Around line 918-963: The code incorrectly assumes the entire right subtree is
a derived table: when building outputNames and setting rFullSchema/rFullNames,
detect whether rightPlan already has a join-style FullSchema/FullNames (use the
same helper as left, e.g. call findJoinFullSchema(rightPlan)) and, if present,
use that FullSchema/FullNames and preserve the existing right output
FieldName.DBName/qualification instead of clearing DBName; only clear DBName and
treat outputs as derived-table names when no FullSchema/FullNames are found for
rightPlan. Ensure ap.SetOutputNames uses the chosen name slice consistently and
that rFullSchema/rFullNames are set from the findJoinFullSchema result when
non-nil.
- Around line 505-550: When cloning output names for an aliased table (the block
using x.AsName, isTableName, p.OutputNames(), and p.SetOutputNames), also apply
any derived-table column alias list so the FieldName.ColName reflects the alias
list instead of the inner names; detect the alias list on the table alias node
(e.g. the derived-table column alias container on x/alias node) and for each i
set clonedNames[i].ColName to the corresponding alias when present (preserving
Hidden, DBName handling, and other flags), then call
p.SetOutputNames(clonedNames) so the duplicate-name check and resolution use the
alias names.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 99c72a7c-48db-4a86-b180-04025d949b0f

📥 Commits

Reviewing files that changed from the base of the PR and between e987506 and c3d9120.

📒 Files selected for processing (2)
  • pkg/planner/core/expression_rewriter.go
  • pkg/planner/core/logical_plan_builder.go

Comment on lines +771 to 791
// Check for LogicalJoin or LogicalApply (which embeds LogicalJoin)
// LogicalApply is used for LATERAL joins and correlated subqueries
if left, ok := leftPlan.(*logicalop.LogicalJoin); ok && left.FullSchema != nil {
lFullSchema = left.FullSchema
lFullNames = left.FullNames
} else if left, ok := leftPlan.(*logicalop.LogicalApply); ok && left.FullSchema != nil {
lFullSchema = left.FullSchema
lFullNames = left.FullNames
} else {
lFullSchema = leftPlan.Schema()
lFullNames = leftPlan.OutputNames()
}
if right, ok := rightPlan.(*logicalop.LogicalJoin); ok && right.FullSchema != nil {
rFullSchema = right.FullSchema
rFullNames = right.FullNames
} else if right, ok := rightPlan.(*logicalop.LogicalApply); ok && right.FullSchema != nil {
rFullSchema = right.FullSchema
rFullNames = right.FullNames
} else {
rFullSchema = rightPlan.Schema()
rFullNames = rightPlan.OutputNames()

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

Preserve wrapped child FullSchema here too.

A child can already be a LogicalSelection over a LogicalJoin/LogicalApply that carries FullSchema from an inner USING/NATURAL join. Falling back to Schema()/OutputNames() drops those redundant columns again, so parent joins lose the shape later name-resolution code expects.

♻️ Suggested fix
-	// Check for LogicalJoin or LogicalApply (which embeds LogicalJoin)
-	// LogicalApply is used for LATERAL joins and correlated subqueries
-	if left, ok := leftPlan.(*logicalop.LogicalJoin); ok && left.FullSchema != nil {
-		lFullSchema = left.FullSchema
-		lFullNames = left.FullNames
-	} else if left, ok := leftPlan.(*logicalop.LogicalApply); ok && left.FullSchema != nil {
-		lFullSchema = left.FullSchema
-		lFullNames = left.FullNames
-	} else {
+	if fullSchema, fullNames := findJoinFullSchema(leftPlan); fullSchema != nil {
+		lFullSchema = fullSchema
+		lFullNames = fullNames
+	} else {
 		lFullSchema = leftPlan.Schema()
 		lFullNames = leftPlan.OutputNames()
 	}
-	if right, ok := rightPlan.(*logicalop.LogicalJoin); ok && right.FullSchema != nil {
-		rFullSchema = right.FullSchema
-		rFullNames = right.FullNames
-	} else if right, ok := rightPlan.(*logicalop.LogicalApply); ok && right.FullSchema != nil {
-		rFullSchema = right.FullSchema
-		rFullNames = right.FullNames
-	} else {
+	if fullSchema, fullNames := findJoinFullSchema(rightPlan); fullSchema != nil {
+		rFullSchema = fullSchema
+		rFullNames = fullNames
+	} else {
 		rFullSchema = rightPlan.Schema()
 		rFullNames = rightPlan.OutputNames()
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/logical_plan_builder.go` around lines 771 - 791, The current
logic only preserves FullSchema/FullNames when leftPlan/rightPlan are directly
LogicalJoin/LogicalApply, dropping them when wrapped (e.g., by
LogicalSelection). Update the checks in the join-building block to also detect a
LogicalSelection wrapper: if leftPlan (or rightPlan) is
*logicalop.LogicalSelection and its child is a *logicalop.LogicalJoin or
*logicalop.LogicalApply with non-nil FullSchema, set lFullSchema/lFullNames (or
rFullSchema/rFullNames) from the child.FullSchema/child.FullNames instead of
falling back to Schema()/OutputNames(); otherwise keep the existing fallback.
Ensure you reference leftPlan/rightPlan, logicalop.LogicalSelection,
logicalop.LogicalJoin, logicalop.LogicalApply, FullSchema and FullNames in the
updated checks.

@ti-chi-bot

ti-chi-bot Bot commented Mar 17, 2026

Copy link
Copy Markdown

@terry1purcell: 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/mysql-test c3d9120 link true /test mysql-test
idc-jenkins-ci-tidb/build c3d9120 link true /test build
pull-build-next-gen c3d9120 link true /test pull-build-next-gen
idc-jenkins-ci-tidb/unit-test c3d9120 link true /test unit-test
idc-jenkins-ci-tidb/check_dev c3d9120 link true /test check-dev
idc-jenkins-ci-tidb/check_dev_2 c3d9120 link true /test check-dev2
pull-unit-test-next-gen c3d9120 link true /test pull-unit-test-next-gen

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.

@terry1purcell

Copy link
Copy Markdown
Contributor Author

Will rebase and create new PR

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

Labels

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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant