Skip to content

planner: align binary literal agg elimination with MySQL - #67343

Closed
hawkingrei wants to merge 11 commits into
pingcap:masterfrom
hawkingrei:issue-67039-repro
Closed

planner: align binary literal agg elimination with MySQL#67343
hawkingrei wants to merge 11 commits into
pingcap:masterfrom
hawkingrei:issue-67039-repro

Conversation

@hawkingrei

@hawkingrei hawkingrei commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #67039

Problem Summary:

For MIN(b'101010') in a derived query with HAVING c1, TiDB could return different results depending on whether aggregation elimination happened. The eliminated branch could expose the raw binary literal in HAVING, which diverged from MySQL.

What changed and how does it work?

  • Keep CAST for binary literals in aggregation elimination even when the argument type already compares equal to the target type.
  • Drop the MAX/MIN(binary literal) TypeBit override from this PR.
  • Add a planner regression in TestIssue66619 for both with-PK and without-PK query shapes.
  • Assert the MySQL-compatible behavior:
    • select hex(c1) ... returns 2A
    • select hex(c1) ... having c1 returns Empty set

Check List

Tests

  • Unit test
  • Integration test
  • Manual test (add detailed scripts or steps below)
  • No need to test

Manual test:

  • MySQL 5.7.24
  • create table t70(c0 char, primary key(c0)); insert into t70(c0) values (1);
  • select hex(c1) from (select 1 as c0, min(b'101010') as c1 from t70 group by t70.c0, t70.c0) s;
    • returns 2A
  • select hex(c1) from (select 1 as c0, min(b'101010') as c1 from t70 group by t70.c0, t70.c0 having c1) s;
    • returns Empty set
  • the same HAVING result is also Empty set without the primary key

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

None

@ti-chi-bot

ti-chi-bot Bot commented Mar 27, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@ti-chi-bot ti-chi-bot Bot added do-not-merge/needs-triage-completed release-note-none Denotes a PR that doesn't merit a release note. do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. labels Mar 27, 2026
@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds regression tests for inconsistent MIN on binary literals with HAVING when a table has a PRIMARY KEY, and updates aggregation-elimination cast wrapping to force casts for binary literals even when types appear equal.

Changes

Cohort / File(s) Summary
Test Case Addition
pkg/planner/core/integration_test.go
Adds regression checks in TestIssue66619 that run the derived-aggregation HAVING query against a table with and without a PRIMARY KEY, asserting results on MIN(b'101010').
Aggregation Elimination Logic
pkg/planner/core/rule_aggregation_elimination.go
Adjusts wrapCastFunction to avoid returning the original argument for binary literals when target type equals argument type, forcing BuildCastFunction to be applied in aggregation-elimination rewrites.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

size/M

Suggested reviewers

  • qw4990
  • AilinKid
  • guo-shaoge

Poem

🐰 I hopped through bytes and casting light,
A MIN that vanished in the night.
I nudged the cast to do its part,
Tests now catch the tricky art,
Hooray — the aggregates reunite! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR fully addresses issue #67039 by preserving BIT type semantics for MIN/MAX over binary-literal constants and adding regression tests for both PK and non-PK table scenarios.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing issue #67039: a planner fix in rule_aggregation_elimination.go and test additions in integration_test.go; no unrelated modifications detected.
Title check ✅ Passed The title clearly and concisely describes the main change: aligning binary literal aggregation elimination with MySQL behavior.
Description check ✅ Passed The PR description comprehensively addresses the problem, changes, test coverage, and follows the template structure with all required sections completed.

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

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

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

❤️ Share

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

@tiprow

tiprow Bot commented Mar 27, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@ti-chi-bot ti-chi-bot Bot added size/M Denotes a PR that changes 30-99 lines, ignoring generated files. sig/planner SIG: Planner labels Mar 27, 2026
@hawkingrei

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 27, 2026
@codecov

codecov Bot commented Mar 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.4344%. Comparing base (d3360c6) to head (2cb6ba8).
⚠️ Report is 45 commits behind head on master.

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #67343        +/-   ##
================================================
- Coverage   77.8039%   77.4344%   -0.3696%     
================================================
  Files          2023       1943        -80     
  Lines        556185     549063      -7122     
================================================
- Hits         432734     425164      -7570     
- Misses       121706     123533      +1827     
+ Partials       1745        366      -1379     
Flag Coverage Δ
integration 40.9276% <100.0000%> (-7.1994%) ⬇️

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

Components Coverage Δ
dumpling 61.5065% <ø> (ø)
parser ∅ <ø> (∅)
br 48.5356% <ø> (-12.3093%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@hawkingrei
hawkingrei marked this pull request as ready for review March 27, 2026 06:19
@ti-chi-bot ti-chi-bot Bot removed do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. do-not-merge/needs-triage-completed labels Mar 27, 2026
@hawkingrei hawkingrei added the AI-Correction Bugfix by AI label Mar 27, 2026

@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/expression/aggregation/base_func.go (1)

334-339: Consider using the existing IsBinaryLiteral helper for consistency.

The codebase has an expression.IsBinaryLiteral helper function at pkg/expression/expression.go:1167-1171 that performs the same check. Using it would reduce duplication and improve maintainability.

The Flen calculation (len(binaryLiteral) * 8) is correct—binary literal byte length multiplied by 8 gives the bit length, which aligns with mysql.TypeBit semantics.

♻️ Optional: Use existing helper
 		if a.Name == ast.AggFuncMax || a.Name == ast.AggFuncMin {
-			if con, ok := a.Args[0].(*expression.Constant); ok && con.Value.Kind() == types.KindBinaryLiteral {
+			if expression.IsBinaryLiteral(a.Args[0]) {
+				con := a.Args[0].(*expression.Constant)
 				a.RetTp.SetType(mysql.TypeBit)
 				a.RetTp.SetFlen(len(con.Value.GetBinaryLiteral()) * 8)
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/expression/aggregation/base_func.go` around lines 334 - 339, Replace the
in-place binary-literal check in the aggregation special-case with the existing
helper: instead of testing "if con, ok := a.Args[0].(*expression.Constant); ok
&& con.Value.Kind() == types.KindBinaryLiteral" use the
expression.IsBinaryLiteral helper (passing a.Args[0]) to detect binary literals
and then set return type and Flen as before via a.RetTp.SetType(mysql.TypeBit)
and a.RetTp.SetFlen(...). Update references around function/method names a.Name,
ast.AggFuncMax/ast.AggFuncMin, expression.Constant, expression.IsBinaryLiteral
and retain the Flen calculation (binary length * 8).
🤖 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/expression/aggregation/base_func.go`:
- Around line 334-339: Replace the in-place binary-literal check in the
aggregation special-case with the existing helper: instead of testing "if con,
ok := a.Args[0].(*expression.Constant); ok && con.Value.Kind() ==
types.KindBinaryLiteral" use the expression.IsBinaryLiteral helper (passing
a.Args[0]) to detect binary literals and then set return type and Flen as before
via a.RetTp.SetType(mysql.TypeBit) and a.RetTp.SetFlen(...). Update references
around function/method names a.Name, ast.AggFuncMax/ast.AggFuncMin,
expression.Constant, expression.IsBinaryLiteral and retain the Flen calculation
(binary length * 8).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 87b08d5c-360a-454e-8e11-7a79b5e90eb6

📥 Commits

Reviewing files that changed from the base of the PR and between 3742691 and 0e2cba2.

📒 Files selected for processing (3)
  • pkg/expression/aggregation/base_func.go
  • pkg/expression/aggregation/base_func_test.go
  • pkg/planner/core/integration_test.go

@hawkingrei

Copy link
Copy Markdown
Contributor Author

/retest

@ti-chi-bot ti-chi-bot Bot added size/S Denotes a PR that changes 10-29 lines, ignoring generated files. and removed size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels Mar 27, 2026

@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/planner/core/integration_test.go (1)

2356-2356: Consider using DROP TABLE IF EXISTS for consistency.

Line 2356 uses drop table t70 without IF EXISTS, while line 2346 uses drop table if exists t70. Using IF EXISTS consistently makes the test more robust against potential failures in earlier statements.

-	tk.MustExec("drop table t70")
+	tk.MustExec("drop table if exists t70")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/planner/core/integration_test.go` at line 2356, Replace the unconditional
drop with a safe conditional drop: locate the tk.MustExec call that executes
"drop table t70" (the statement using tk.MustExec in integration_test.go) and
change the SQL to use "DROP TABLE IF EXISTS t70" so the test remains robust and
consistent with the earlier "drop table if exists t70" usage.
🤖 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/planner/core/integration_test.go`:
- Line 2356: Replace the unconditional drop with a safe conditional drop: locate
the tk.MustExec call that executes "drop table t70" (the statement using
tk.MustExec in integration_test.go) and change the SQL to use "DROP TABLE IF
EXISTS t70" so the test remains robust and consistent with the earlier "drop
table if exists t70" usage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ffa1cff2-9400-44c7-8494-b1551cc9a548

📥 Commits

Reviewing files that changed from the base of the PR and between 61c3193 and 76e7185.

📒 Files selected for processing (2)
  • pkg/planner/core/integration_test.go
  • pkg/planner/core/rule_aggregation_elimination.go

@hawkingrei

Copy link
Copy Markdown
Contributor Author

/retest

@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

🤖 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/integration_test.go`:
- Around line 2349-2354: The test is asserting empty results but should assert
the preserved BIT value; update the assertions for the queries using hex(c1) and
min(b'101010') (the two checks around the comment "issue:67039 with-pk" and its
non-PK counterpart) to expect "2A" instead of empty rows so the regression
verifies BIT semantics survive the HAVING filter (i.e., replace testkit.Rows()
with testkit.Rows("2A") for both occurrences).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5709390e-ff9d-4c68-bc74-4fb05519973a

📥 Commits

Reviewing files that changed from the base of the PR and between 76e7185 and 64b8e95.

📒 Files selected for processing (1)
  • pkg/planner/core/integration_test.go

Comment thread pkg/planner/core/integration_test.go Outdated
Comment thread pkg/planner/core/rule_aggregation_elimination.go Outdated
@ti-chi-bot ti-chi-bot Bot added size/M Denotes a PR that changes 30-99 lines, ignoring generated files. and removed size/S Denotes a PR that changes 10-29 lines, ignoring generated files. labels Mar 27, 2026
@ti-chi-bot ti-chi-bot Bot added the needs-1-more-lgtm Indicates a PR needs 1 more LGTM. label Mar 27, 2026
@hawkingrei
hawkingrei requested a review from qw4990 March 28, 2026 15:18
@ti-chi-bot ti-chi-bot Bot added lgtm and removed needs-1-more-lgtm Indicates a PR needs 1 more LGTM. labels Mar 29, 2026
@ti-chi-bot

ti-chi-bot Bot commented Mar 29, 2026

Copy link
Copy Markdown

[LGTM Timeline notifier]

Timeline:

  • 2026-03-27 17:41:02.716765391 +0000 UTC m=+549258.752835651: ☑️ agreed by winoros.
  • 2026-03-29 14:22:46.338807607 +0000 UTC m=+102171.544167665: ☑️ agreed by qw4990.

Comment thread pkg/expression/aggregation/base_func.go Outdated
a.RetTp.DelFlag(mysql.NotNullFlag)
if (a.Name == ast.AggFuncMax || a.Name == ast.AggFuncMin) && expression.IsBinaryLiteral(a.Args[0]) {
con := a.Args[0].(*expression.Constant)
a.RetTp.SetType(mysql.TypeBit)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As shown in L332, a.RetTp is a clone of a.Args[0] type, i think the clone will also clone the flag information, so why need to add it here again?

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.

Good point. Clone() already preserves the original flags here, so there is no need to add them again.

In the current version of this patch I only override the logical type to TypeBit and set Flen from the binary literal width. The existing flag/charset/collation information still comes from the cloned argument type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I mean the type should not be overriden

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.

Thanks, I retried this direction in a detached worktree to check whether the planner-side CAST preservation alone would be enough.

It turns out it is not enough. The important point here is that this override is not re-applying cloned flags; it is intentionally correcting the logical return type for MIN/MAX over binary literals.

More concretely:

  1. Clone() does preserve the existing flag / charset / collation metadata from a.Args[0]. I agree we do not need to add those again.

  2. But after the clone, the aggregate return type is still string-like. For a binary literal input, that is not sufficient for the aggregate result semantics we need here. This branch still needs to override the logical type to mysql.TypeBit and set Flen from the binary literal width.

  3. I validated the reviewer-suggested direction directly: keep the planner-side CAST guard, but remove only the TypeBit override in base_func.go. That immediately regresses two places:

    • ./tools/check/failpoint-go-test.sh pkg/expression/aggregation -run '^TestBaseFunc_InferAggRetType$' -count=1
      • the inferred type falls back to TypeVarString
    • ./tools/check/failpoint-go-test.sh pkg/planner/core -run '^TestIssue66619$' -count=1
      • issue:67039 with PK returns empty again
  4. So the problem is earlier than the planner rule. Once aggregate type inference falls back to TypeVarString, the binary-literal semantics are already wrong before the planner-side CAST guard can help. The planner guard only preserves one downstream planner path; it cannot repair an already-wrong aggregate return type.

  5. That is why the current split is still needed:

    • clone the original type to preserve the existing metadata
    • override the logical type/width to TypeBit for the binary-literal MIN/MAX result itself

So I do not think we can remove the type override as a narrow change in this PR. If we want to eliminate this override entirely, that likely needs a broader binary-literal typing cleanup instead of only relying on the planner-side CAST preservation.

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.

I tried removing only this TypeBit override while keeping the planner-side CAST preservation. That regresses both ./tools/check/failpoint-go-test.sh pkg/expression/aggregation -run ^TestBaseFunc_InferAggRetType$ -count=1 (MAX/MIN(binary literal) falls back to TypeVarString) and ./tools/check/failpoint-go-test.sh pkg/planner/core -run ^TestIssue66619$ -count=1 (the issue:67039 with-pk case becomes empty again instead of returning 2A). Since binary literals are globally typed as TypeVarString today, the planner-side CAST guard alone cannot recover BIT semantics; avoiding this override would need a broader binary-literal typing redesign rather than a local fix in this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In my understanding, b'101010' is a literal of type bit, so in TiDB the expected behavior will be that the input of MIN is type bit, and the output of MIN should also be type bit, it is not related to string like type, b'101010' is a Bit literal, i don't understand why in TiDB it will be treated as string like type.

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.

You're right to question the TypeBit override here. I rechecked the behavior against MySQL 5.7, and that override is not the right fix for this PR.

In TiDB today, binary literals still flow through this path with a string-like field type. Changing that globally would be a broader typing cleanup, but it is not needed for issue #67039. The actual MySQL-compatible fix here is planner-side only: keep the CAST during aggregation elimination so the eliminated path does not expose the raw literal in HAVING.

With that change, both query shapes now match MySQL:

  • select hex(c1) ... returns 2A
  • select hex(c1) ... having c1 returns Empty set

I have removed the base_func.go override in the latest update and rewrote the regression accordingly.

@hawkingrei hawkingrei changed the title expression, planner: preserve bit type for min/max binary literals planner: align binary literal agg elimination with MySQL Apr 2, 2026
@ti-chi-bot

ti-chi-bot Bot commented Apr 2, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: qw4990, winoros

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

The pull request process is described 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

@ti-chi-bot ti-chi-bot Bot added the approved label Apr 2, 2026
@hawkingrei

Copy link
Copy Markdown
Contributor Author

/hold

@ti-chi-bot ti-chi-bot Bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Apr 2, 2026
@ti-chi-bot

ti-chi-bot Bot commented Apr 2, 2026

Copy link
Copy Markdown

@hawkingrei: 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
pull-unit-test-next-gen 045d21a link true /test pull-unit-test-next-gen
pull-integration-e2e-test 2cb6ba8 link true /test pull-integration-e2e-test
idc-jenkins-ci-tidb/unit-test 2cb6ba8 link true /test unit-test
idc-jenkins-ci-tidb/check_dev_2 2cb6ba8 link true /test check-dev2

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.

@hawkingrei

Copy link
Copy Markdown
Contributor Author

Closing this PR because current master already covers this case by skipping aggregation elimination for MAX/MIN(binary literal) in #67394. With that change, the query shape here follows the same behavior as MySQL, so the extra wrapCastFunction special case is no longer needed.

@hawkingrei hawkingrei closed this Apr 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-Correction Bugfix by AI approved do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. lgtm 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/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unexpected Result Difference in HAVING Clause and MIN Aggregation with PRIMARY KEY

4 participants