Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/planner/core/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ go_test(
"find_best_task_test.go",
"hint_test.go",
"integration_test.go",
"join_reorder_side_effect_test.go",
"lateral_join_test.go",
"logical_plans_test.go",
"main_test.go",
Expand Down
115 changes: 115 additions & 0 deletions pkg/planner/core/join_reorder_side_effect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2026 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package core

import (
"testing"

"github.com/pingcap/tidb/pkg/domain"
"github.com/pingcap/tidb/pkg/expression"
"github.com/pingcap/tidb/pkg/parser/ast"
"github.com/pingcap/tidb/pkg/parser/mysql"
"github.com/pingcap/tidb/pkg/planner/core/base"
"github.com/pingcap/tidb/pkg/planner/core/operator/logicalop"
"github.com/pingcap/tidb/pkg/planner/property"
"github.com/pingcap/tidb/pkg/planner/util/coretestsdk"
"github.com/pingcap/tidb/pkg/types"
"github.com/stretchr/testify/require"
)

func newLegacyTypedLeaf(ctx base.PlanContext, tp byte, count float64) *logicalop.LogicalTableDual {
dual := logicalop.LogicalTableDual{RowCount: 1}.Init(ctx, 0)
dual.SetSchema(expression.NewSchema())
dual.Schema().Append(&expression.Column{
UniqueID: ctx.GetSessionVars().PlanColumnID.Add(1),
RetType: types.NewFieldType(tp),
})
dual.SetStats(&property.StatsInfo{RowCount: count})
return dual
}

func TestGreedyConnectivityProbeKeepsProjectionLeafImmutable(t *testing.T) {
ctx := coretestsdk.MockContext()
defer domain.GetDomain(ctx).StatsHandle().Close()

stringLeaf := newLegacyTypedLeaf(ctx, mysql.TypeVarchar, 1)
intLeaf := newLegacyTypedLeaf(ctx, mysql.TypeLonglong, 1)

// Simulate a projected join key that originally participated in a col=col
// equality edge. When the legacy greedy connectivity probe flips the pair
// direction, the rebuilt equality injects helper expressions. The probe must
// not mutate the original projection leaf in place.
projectedKey := &expression.Column{
UniqueID: ctx.GetSessionVars().PlanColumnID.Add(1),
RetType: types.NewFieldType(mysql.TypeLonglong),
}
projectionLeaf := logicalop.LogicalProjection{
Exprs: []expression.Expression{stringLeaf.Schema().Columns[0]},
}.Init(ctx, 0)
projectionLeaf.SetSchema(expression.NewSchema(projectedKey))
projectionLeaf.SetChildren(stringLeaf)
projectionLeaf.SetStats(&property.StatsInfo{RowCount: stringLeaf.StatsInfo().RowCount})

eqCond, ok := expression.NewFunctionInternal(ctx.GetExprCtx(), ast.EQ, types.NewFieldType(mysql.TypeTiny),
projectedKey, intLeaf.Schema().Columns[0]).(*expression.ScalarFunction)
require.True(t, ok)

// Reversing the join side now requires casts, which exercises the helper
// projection path during speculative join construction.
projectedKey.RetType = types.NewFieldType(mysql.TypeVarchar)

solver := &joinReorderGreedySolver{
allInnerJoin: true,
baseSingleGroupJoinOrderSolver: &baseSingleGroupJoinOrderSolver{
ctx: ctx,
basicJoinGroupInfo: &basicJoinGroupInfo{
eqEdges: []*expression.ScalarFunction{eqCond},
joinTypes: []*joinTypeWithExtMsg{{
JoinType: base.InnerJoin,
}},
},
},
}

origExprCnt := len(projectionLeaf.Exprs)
origSchemaLen := projectionLeaf.Schema().Len()
probe := solver.probeConnection(intLeaf, projectionLeaf)
require.True(t, probe.HasEQEdge())
require.True(t, probe.HasJoinCondition())
require.False(t, probe.IsCartesian())
require.Equal(t, base.InnerJoin, probe.joinType.JoinType)
require.Same(t, intLeaf, probe.leftPlan)
require.Same(t, projectionLeaf, probe.rightPlan)
require.Len(t, projectionLeaf.Exprs, origExprCnt)
require.Equal(t, origSchemaLen, projectionLeaf.Schema().Len())

join, remainOtherConds, err := solver.buildJoinFromProbe(probe)
require.NoError(t, err)
require.Empty(t, remainOtherConds)
_, _, err = join.RecursiveDeriveStats(nil)
require.NoError(t, err)
logicalJoin, ok := join.(*logicalop.LogicalJoin)
require.True(t, ok)
clonedRight, ok := logicalJoin.Children()[1].(*logicalop.LogicalProjection)
require.True(t, ok)
require.NotSame(t, projectionLeaf, clonedRight)
require.Len(t, clonedRight.Exprs, origExprCnt+1)
require.Equal(t, origSchemaLen+1, clonedRight.Schema().Len())
require.NotNil(t, clonedRight.StatsInfo())
_, ok = clonedRight.StatsInfo().ColNDVs[clonedRight.Schema().Columns[origSchemaLen].UniqueID]
require.True(t, ok)
require.Len(t, projectionLeaf.Exprs, origExprCnt)
require.Equal(t, origSchemaLen, projectionLeaf.Schema().Len())
}
16 changes: 15 additions & 1 deletion pkg/planner/core/joinorder/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,25 @@ go_library(
go_test(
name = "joinorder_test",
timeout = "short",
srcs = ["bitset_bench_test.go"],
srcs = [
"bitset_bench_test.go",
"join_order_side_effect_test.go",
],
embed = [":joinorder"],
flaky = True,
shard_count = 2,
deps = [
"//pkg/domain",
"//pkg/expression",
"//pkg/parser/ast",
"//pkg/parser/mysql",
"//pkg/planner/core/base",
"//pkg/planner/core/operator/logicalop",
"//pkg/planner/property",
"//pkg/planner/util/coretestsdk",
"//pkg/types",
"//pkg/util/intset",
"@com_github_bits_and_blooms_bitset//:bitset",
"@com_github_stretchr_testify//require",
],
)
75 changes: 66 additions & 9 deletions pkg/planner/core/joinorder/conflict_detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,55 @@ func (r *CheckConnectionResult) NoEQEdge() bool {
return !r.hasEQCond
}

// HasJoinCondition reports whether the connection is backed by at least one
// real join predicate instead of a pure cartesian edge.
func (r *CheckConnectionResult) HasJoinCondition() bool {
if r == nil || r.node1 == nil || r.node2 == nil {
return false
}
if r.edgeHasJoinCondition(r.appliedNonInnerEdge) {
return true
}
for _, e := range r.appliedInnerEdges {
if r.edgeHasJoinCondition(e) {
return true
}
}
return false
}

func (r *CheckConnectionResult) edgeHasJoinCondition(e *edge) bool {
if e == nil {
return false
}
if len(e.eqConds) > 0 {
return true
}
// Degenerate one-sided predicates still form edges so CD-C can preserve
// correctness constraints, but seed-by-cost should only treat predicates
// that reference both candidate sides as real join conditions.
for _, cond := range e.nonEQConds {
if ExprConnectsBothSides(cond, r.node1.p.Schema(), r.node2.p.Schema()) {
return true
}
}
return false
}

// ExprConnectsBothSides reports whether cond references both the left and right
// schemas instead of being a one-sided predicate.
func ExprConnectsBothSides(cond expression.Expression, leftSchema, rightSchema *expression.Schema) bool {
if cond == nil || leftSchema == nil || rightSchema == nil {
return false
}
mergedSchema := expression.MergeSchema(leftSchema, rightSchema)
if !expression.ExprFromSchema(cond, mergedSchema) {
return false
}
return !expression.ExprFromSchema(cond, leftSchema) &&
!expression.ExprFromSchema(cond, rightSchema)
}

// CheckConnection tests whether any edge can validly connect node1 and node2.
// It's corresponding to the pseudocode for APPLICABLE(b/c) in the paper(Figure-9).
// The basic idea is: It collects all applicable inner edges (there can be many) and at most one
Expand Down Expand Up @@ -724,7 +773,10 @@ func (d *ConflictDetector) MakeJoin(checkResult *CheckConnectionResult, vertexHi
}, nil
}

func alignEQConds(ctx base.PlanContext, left, right base.LogicalPlan, eqConds []*expression.ScalarFunction) (newLeft base.LogicalPlan, newRight base.LogicalPlan, alignedEQConds []*expression.ScalarFunction, err error) {
// AlignEQCondsWithoutMutation aligns eqConds to the provided left/right plans
// and injects helper projections on temporary plan clones when type coercion
// makes a swapped key cease to be a plain column reference.
func AlignEQCondsWithoutMutation(ctx base.PlanContext, left, right base.LogicalPlan, eqConds []*expression.ScalarFunction) (newLeft base.LogicalPlan, newRight base.LogicalPlan, alignedEQConds []*expression.ScalarFunction, err error) {
if len(eqConds) == 0 {
return left, right, nil, nil
}
Expand All @@ -749,10 +801,10 @@ func alignEQConds(ctx base.PlanContext, left, right base.LogicalPlan, eqConds []
lCol := swapped.GetArgs()[0]
rCol := swapped.GetArgs()[1]
if !isCol0 {
left, lCol = logicalop.InjectExpr(left, swapped.GetArgs()[0])
left, lCol = logicalop.InjectExprAvoidingMutation(left, swapped.GetArgs()[0])
}
if !isCol1 {
right, rCol = logicalop.InjectExpr(right, swapped.GetArgs()[1])
right, rCol = logicalop.InjectExprAvoidingMutation(right, swapped.GetArgs()[1])
}
swapped = expression.NewFunctionInternal(ctx.GetExprCtx(), cond.FuncName.L, cond.GetStaticType(),
lCol, rCol).(*expression.ScalarFunction)
Expand All @@ -770,14 +822,14 @@ func makeNonInnerJoin(ctx base.PlanContext, checkResult *CheckConnectionResult,
var alignedEQConds []*expression.ScalarFunction
var err error

checkResult.node1.p, checkResult.node2.p, alignedEQConds, err = alignEQConds(ctx, checkResult.node1.p, checkResult.node2.p, e.eqConds)
// Keep aligned helper projections local to this join construction. During
// seed scoring, the caller may discard this candidate and continue probing
// other pairs, so mutating checkResult.node{1,2}.p here would leak state.
left, right, alignedEQConds, err := AlignEQCondsWithoutMutation(ctx, checkResult.node1.p, checkResult.node2.p, e.eqConds)
if err != nil {
return nil, err
}

left := checkResult.node1.p
right := checkResult.node2.p

join, err := newCartesianJoin(ctx, e.joinType, left, right, vertexHints)
if err != nil {
return nil, err
Expand Down Expand Up @@ -899,15 +951,20 @@ func makeInnerJoin(ctx base.PlanContext, checkResult *CheckConnectionResult, exi
var alignedEQConds []*expression.ScalarFunction
newEqConds := make([]*expression.ScalarFunction, 0, 8)
newOtherConds := make([]expression.Expression, 0, 8)
// Reuse the locally aligned plans across all edges of this candidate join,
// but do not write them back into checkResult.node{1,2}.p for the same
// reason as makeNonInnerJoin() above.
left := checkResult.node1.p
right := checkResult.node2.p
for _, e := range checkResult.appliedInnerEdges {
checkResult.node1.p, checkResult.node2.p, alignedEQConds, err = alignEQConds(ctx, checkResult.node1.p, checkResult.node2.p, e.eqConds)
left, right, alignedEQConds, err = AlignEQCondsWithoutMutation(ctx, left, right, e.eqConds)
if err != nil {
return nil, err
}
newEqConds = append(newEqConds, alignedEQConds...)
newOtherConds = append(newOtherConds, e.nonEQConds...)
}
join, err := newCartesianJoin(ctx, checkResult.appliedInnerEdges[0].joinType, checkResult.node1.p, checkResult.node2.p, vertexHints)
join, err := newCartesianJoin(ctx, checkResult.appliedInnerEdges[0].joinType, left, right, vertexHints)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading