Skip to content

Fix planner panic when pushing predicates into a UNION of literals - #20853

Open
vatsalpatel wants to merge 1 commit into
vitessio:mainfrom
vatsalpatel:fix-union-literal-branch-panic
Open

Fix planner panic when pushing predicates into a UNION of literals#20853
vatsalpatel wants to merge 1 commit into
vitessio:mainfrom
vatsalpatel:fix-union-literal-branch-panic

Conversation

@vatsalpatel

Copy link
Copy Markdown

Description

Joining a table to a derived table built from literal only SELECTs kills the planner:

select c.customer_id, r.ordinal
from customer as c
join (select 0 as ordinal, 1 as customer_id union all select 1, 2) as r on c.customer_id = r.customer_id
where r.ordinal <= 30000;
ERROR 1815 (HY000): VT13001: [BUG] expected all sources of the UNION to be horizons

Union.AddPredicate rewrites a predicate once per source, swapping every column for the expression at the same offset in that branch. On a literal only branch, r.ordinal <= 30000 becomes 0 <= 30000, which has no table dependency left, so Horizon.AddPredicate has nowhere to push it and wraps the branch in a Filter. Union.GetSelectFor needs every source to be a horizon, so the next predicate pushed at the same union panics. A join sends two predicates at it, the join condition and the WHERE, so this shape fails every time.

The union now checks that every source can take its rewritten predicate before pushing any of them. If one can't, nothing is pushed and the predicate stays in a Filter on top of the horizon. That's where the two bail-outs already in Horizon.AddPredicate put their filters, and it's the only placement that works: putting it between the horizon and the union panics later in the SQL builder, since a Filter inside a union derived table has no WHERE clause to be written into.

The check is "would this source stop being a horizon" rather than "does the rewritten predicate have no dependencies", so it also catches the two other shapes that panic identically today, a branch predicate that resolves across several tables and one that resolves to an aggregate.

The query above now plans as a single scatter, with the predicate pushed into the same route it would have reached anyway:

select u.id, r.ordinal from `user` as u, (select 0 as ordinal, 1 as pid from dual union all select 1, 2 from dual) as r where r.ordinal <= 30000 and u.id = r.pid

Related Issue(s)

Fixes #20610

Checklist

  • "Backport to:" labels have been added if this change should be back-ported to release branches
  • If this change is to be back-ported to previous releases, a justification is included in the PR description
  • Tests were added or are not required
  • Did the new or modified tests pass consistently locally and on CI?
  • Documentation was added or is not required

Two cases added to union_cases.json. Verified the first fails without the fix, with the exact VT13001 from the issue. No existing plan expectation in any testdata file changed.

Deployment Notes

Queries that failed with VT13001: [BUG] expected all sources of the UNION to be horizons now plan and run. Predicate placement changes for derived tables whose UNION branches select only literals: the predicate is applied above the derived table rather than inside each branch. Results are unchanged.

AI Disclosure

I used Claude Code to understand the package and to add the tests, and implemented the fix myself.

`Union.AddPredicate` rewrites a predicate once per source, swapping every
column for the expression found at the same offset in that branch. On a
branch that selects only literals, the rewritten predicate has no table
dependency left, so `Horizon.AddPredicate` can't push it anywhere and
wraps the branch in a `Filter` instead. That breaks the invariant
`Union.GetSelectFor` relies on, and the next predicate pushed into the
same union panics with:

VT13001: [BUG] expected all sources of the UNION to be horizons

A join against such a derived table sends two predicates at the union,
the join condition and the WHERE, so it fails every time.

The union now checks that every source can take its rewritten predicate
before pushing any of them. When one can't, nothing is pushed and the
predicate stays in a `Filter` on top of the horizon, which is where the
two existing bail-outs in `Horizon.AddPredicate` already put it. The
check also covers the two other shapes that end up under a `Filter` for
the same reason and panic identically today: a branch predicate that
resolves across several tables, and one that resolves to an aggregate.

Two planner test cases are added. The first reproduces the panic without
the fix. No existing plan expectation changed.

Fixes vitessio#20610

Signed-off-by: vatsalpatel <vatsalpatel.me@gmail.com>
Copilot AI balanced review requested due to automatic review settings August 17, 2026 13:29
@github-actions github-actions Bot added this to the v25.0.0 milestone Aug 17, 2026
@vitess-bot vitess-bot Bot added NeedsWebsiteDocsUpdate What it says NeedsDescriptionUpdate The description is not clear or comprehensive enough, and needs work NeedsIssue A linked issue is missing for this Pull Request NeedsBackportReason If backport labels have been applied to a PR, a justification is required labels Aug 17, 2026
@vitess-bot

vitess-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Review Checklist

Hello reviewers! 👋 Please follow this checklist when reviewing this Pull Request.

General

  • Ensure that the Pull Request has a descriptive title.
  • Ensure there is a link to an issue (except for internal cleanup and flaky test fixes), new features should have an RFC that documents use cases and test cases.

Tests

  • Bug fixes should have at least one unit or end-to-end test, enhancement and new features should have a sufficient number of tests.

Documentation

  • Apply the release notes (needs details) label if users need to know about this change.
  • 新建 features should be documented.
  • There should be some code comments as to why things are implemented the way they are.
  • There should be a comment at the top of each new or modified test to explain what the test does.

新建 flags

  • Is this flag really necessary?
  • Flag names must be clear and intuitive, use dashes (-), and have a clear help text.

If a workflow is added or modified:

  • Each item in Jobs should be named in order to mark it as required.
  • If the workflow needs to be marked as required, the maintainer team must be notified.

Backward compatibility

  • Protobuf changes should be wire-compatible.
  • Changes to _vt tables and RPCs need to be backward compatible.
  • RPC changes should be compatible with vitess-operator
  • If a flag is removed, then it should also be removed from vitess-operator and arewefastyet, if used there.
  • vtctl command output order should be stable and awk-able.

Copilot AI left a comment

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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Extends UNION predicate pushdown handling so derived-table predicates over UNIONs are only pushed when all UNION sources can absorb them, preventing UNION sources from being wrapped in Filters and breaking planning invariants.

Changes:

  • Add Union.canPushPredicate() and refactor predicate-rewrite logic into helpers (columnOffsets, predicateForSource).
  • Update Horizon.AddPredicate() to conditionally push predicates into UNIONs or fall back to wrapping in a Filter.
  • Add plan test cases for joins against derived tables containing UNIONs of literal-only SELECTs with predicates on the derived table.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
go/vt/vtgate/planbuilder/testdata/union_cases.json Adds regression test cases covering predicate pushdown with derived tables containing UNIONs.
go/vt/vtgate/planbuilder/operators/union.go Introduces pushability check and refactors UNION predicate rewriting per source.
go/vt/vtgate/planbuilder/operators/horizon.go Changes predicate pushdown behavior to avoid breaking UNION horizon invariants; adds absorb/rewrite helpers.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +86 to 89
if !union.canPushPredicate(ctx, expr) {
return newFilter(h, expr)
}
// If we have a derived table on top of a UNION, we can let the UNION do the expression rewriting

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I tried folding it into one pass and it doesn't work, because the filter has to be placed above the Horizon and Union.AddPredicate can't put it there. It has no reference to its parent, and returning newFilter(u, expr) puts the filter between the Horizon and the UNION instead.

I built the suggestion as two variants and ran the planner tests on both. The test that separates them is already in this PR in union_cases.json

Comment on lines 97 to +106
func (u *Union) AddPredicate(ctx *plancontext.PlanningContext, expr sqlparser.Expr) Operator {
offsets := make(map[string]int)
offsets := u.columnOffsets()

exprPerSource := u.predicatePerSource(ctx, expr, offsets)
for i, src := range u.Sources {
u.Sources[i] = src.AddPredicate(ctx, exprPerSource[i])
}

return u
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I tried returning a Filter wrapping the UNION but it didn't work. It is the second variant above, and it panics in the SQL builder with cant add WHERE to *sqlparser.Union at SQL_builder.go:151, because a Filter inside a union derived table cannot be serialized back into SQL. The filter has to go above the Horizon, which is where the two existing bail-outs in Horizon.AddPredicate already put theirs.

注册 for free to join this conversation on GitHub. Already have an account? 登录 to comment

标签

Component: VTGate NeedsBackportReason If backport labels have been applied to a PR, a justification is required NeedsDescriptionUpdate The description is not clear or comprehensive enough, and needs work NeedsIssue A linked issue is missing for this Pull Request NeedsWebsiteDocsUpdate What it says

项目

None yet

Development

Successfully merging this pull request may close these issues.

Bug Report: Join with a UNION of literal-only SELECTs panics the planner (VT13001 expected all sources of the UNION to be horizons)

2 participants