Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ This file provides guidance to agents when working with code in this repository.
- Notes rules: update existing sections when topics overlap; append new sections only for new topics. Purpose: capture decisions, pitfalls, and test patterns.
- Planner rule notes: `docs/note/planner/rule/rule_ai_notes.md`.
- If a single notes file exceeds 2000 lines, split by functionality into multiple markdown files and update references here.
- Predicate pushdown testdata (`pkg/planner/core/casetest/rule/testdata/predicate_pushdown_suite_in.json`) should contain SQL-only cases; put DDL in the test setup to avoid `EXPLAIN` parsing DDL during record runs.
- Integration test recording uses `./run-tests.sh -r <name>` (not `-record`).

## Building

Expand Down
17 changes: 17 additions & 0 deletions docs/note/planner/rule/rule_ai_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,20 @@ Additional notes:
- `pushNotAcrossExpr` already eliminates `not(not(expr))` when `expr` is a logical operator, because `wrapWithIsTrue` returns logical ops unchanged.
- An explicit double-NOT special case is optional for logical expressions; non-logical expressions should continue to use `IsTruthWithNull` semantics.
- Plan regression (CARTESIAN + other cond) is easier to trigger than result differences on small datasets.

## 2026-02-04 - OR-constant in outer join other conditions (issue #65994)

Background:
- A rewrite introduced `LEFT JOIN ... ON (a = b OR 0)` and the OR constant prevented join key extraction, leading to a cartesian-like join behavior and wrong results.

Key takeaways:
- Outer join `OtherConditions` are not simplified by predicate pushdown, so trivial OR/AND constants must be normalized before `updateEQCond`.
- Applying `ApplyPredicateSimplificationForJoin` on `OtherConditions` is sufficient to remove `OR 0` and keep equality keys.

Implementation choice:
- In `LogicalJoin.normalizeJoinConditionsForOuterJoin`, call `ApplyPredicateSimplificationForJoin` with `propagateConstant=false` on `OtherConditions`.

Test and verification:
- Add SQL-only case to `predicate_pushdown_suite_in.json`; keep DDL in the test setup, otherwise `explain` will try to run `DROP/CREATE`.
- Record with: `go test ./pkg/planner/core/casetest/rule -run TestConstantPropagateWithCollation --tags=intest -record`.
- Add integration test to `tests/integrationtest/t/select.test` and record via `./run-tests.sh -r select` (integration tests use `-r`, not `-record`).
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ func TestConstantPropagateWithCollation(t *testing.T) {
tk.MustExec("create table t2 (k0 int, k1 int)")
tk.MustExec("create table t3 (k0 int, d0 decimal(12,2))")
tk.MustExec("create table t4 (k0 int)")
tk.MustExec("drop table if exists t1_65994")
tk.MustExec("drop table if exists t2_65994")
tk.MustExec("create table t1_65994 (a int, b int)")
tk.MustExec("create table t2_65994 (a int, b int)")
tk.MustExec("insert into t0 values (1, 10), (2, 20), (3, 30)")
tk.MustExec("insert into t2 values (1, 100), (3, 300), (4, 400)")
runPredicatePushdownTestDataWithResult(t, tk, cascades, "TestConstantPropagateWithCollation")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"select /* double_not_semi_join */ t0.k0 from t0 where exists (select 1 from t2 where not not (t0.k0 = t2.k0))",
"select /* double_not_left_join_exec */ t0.k0, t2.k0 from t0 left join t2 on not not (t0.k0 = t2.k0) order by t0.k0, t2.k0",
"select /* double_not_right_join_exec */ t0.k0, t2.k0 from t0 right join t2 on not not (t0.k0 = t2.k0) order by t2.k0, t0.k0",
"select /* double_not_semi_join_exec */ t0.k0 from t0 where exists (select 1 from t2 where not not (t0.k0 = t2.k0)) order by t0.k0"
"select /* double_not_semi_join_exec */ t0.k0 from t0 where exists (select 1 from t2 where not not (t0.k0 = t2.k0)) order by t0.k0",
Copy link
Contributor

Choose a reason for hiding this comment

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

What's double_not_semi_join_exec a label for this test case?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's more like a comment or a prompt.

Copy link
Member Author

Choose a reason for hiding this comment

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

where not not (t0.k0 = t2.k0))

"select /* issue:65994 */ * from t1_65994 left join t2_65994 on (t1_65994.a = t2_65994.a or 0)"
]
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,19 @@
"3"
],
"Warning": null
},
{
"SQL": "select /* issue:65994 */ * from t1_65994 left join t2_65994 on (t1_65994.a = t2_65994.a or 0)",
"Plan": [
"HashJoin 12487.50 root left outer join, left side:TableReader, equal:[eq(test.t1_65994.a, test.t2_65994.a)]",
"├─TableReader(Build) 9990.00 root data:Selection",
"│ └─Selection 9990.00 cop[tikv] not(isnull(test.t2_65994.a))",
"│ └─TableFullScan 10000.00 cop[tikv] table:t2_65994 keep order:false, stats:pseudo",
"└─TableReader(Probe) 10000.00 root data:TableFullScan",
" └─TableFullScan 10000.00 cop[tikv] table:t1_65994 keep order:false, stats:pseudo"
],
"Result": null,
"Warning": null
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,19 @@
"3"
],
"Warning": null
},
{
"SQL": "select /* issue:65994 */ * from t1_65994 left join t2_65994 on (t1_65994.a = t2_65994.a or 0)",
"Plan": [
"HashJoin 12487.50 root left outer join, left side:TableReader, equal:[eq(test.t1_65994.a, test.t2_65994.a)]",
"├─TableReader(Build) 9990.00 root data:Selection",
"│ └─Selection 9990.00 cop[tikv] not(isnull(test.t2_65994.a))",
"│ └─TableFullScan 10000.00 cop[tikv] table:t2_65994 keep order:false, stats:pseudo",
"└─TableReader(Probe) 10000.00 root data:TableFullScan",
" └─TableFullScan 10000.00 cop[tikv] table:t1_65994 keep order:false, stats:pseudo"
],
"Result": null,
"Warning": null
}
]
}
Expand Down
18 changes: 10 additions & 8 deletions pkg/planner/core/operator/logicalop/logical_join.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,14 +273,16 @@ func (p *LogicalJoin) normalizeJoinConditionsForOuterJoin() {
return
}
// Outer join ON conditions are not simplified through predicate pushdown.
// Normalize only double NOT here to avoid cartesian joins caused by other conditions.
exprCtx := p.SCtx().GetExprCtx()
for i := range p.OtherConditions {
if !expression.ContainOuterNot(p.OtherConditions[i]) {
continue
}
p.OtherConditions[i] = expression.PushDownNot(exprCtx, p.OtherConditions[i])
}
// However, we still need to eliminate obvious logical constants in OtherConditions
// (e.g. "a = b OR 0") to avoid losing join keys.
p.OtherConditions = ruleutil.ApplyPredicateSimplificationForJoin(
p.SCtx(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need to call this function for other join types, like semi/anti/inner?
Also we can move this function call inside PredicatePushDown, becase there is no much logic, and a new function seems meanless.

Copy link
Collaborator

Choose a reason for hiding this comment

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

other LGTM

Copy link
Member Author

@hawkingrei hawkingrei Feb 4, 2026

Choose a reason for hiding this comment

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

Great, I have rafactored this code.

p.OtherConditions,
p.Children()[0].Schema(),
p.Children()[1].Schema(),
false,
nil,
)
}

// simplifyOuterJoin transforms "LeftOuterJoin/RightOuterJoin" to "InnerJoin" if possible.
Expand Down
6 changes: 6 additions & 0 deletions references/planner-case-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Planner Case Map

## pkg/planner/core/casetest/rule

- predicate_pushdown_suite_in.json
- TestConstantPropagateWithCollation: predicate simplification + results; includes outer join OR-constant case (`/* issue:65994 */`).
26 changes: 26 additions & 0 deletions tests/integrationtest/r/select.result
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,32 @@ HashJoin root CARTESIAN left outer join, left side:HashJoin, left cond:[eq(sele
│ └─TableFullScan cop[tikv] table:t2 keep order:false, stats:pseudo
└─TableReader(Probe) root data:TableFullScan
└─TableFullScan cop[tikv] table:t1 keep order:false, stats:pseudo
drop table if exists t1_65994;
drop table if exists t2_65994;
create table t1_65994 (a int, b int);
create table t2_65994 (a int, b int);
insert into t1_65994 values (1, 10), (2, 20), (3, 30), (null, 40);
insert into t2_65994 values (1, 100), (2, 200), (4, 400), (null, 500);
explain format = 'plan_tree' select /* issue:65994 */ * from t1_65994 left join t2_65994 on (t1_65994.a = t2_65994.a or 0);
id task access object operator info
HashJoin root left outer join, left side:TableReader, equal:[eq(select.t1_65994.a, select.t2_65994.a)]
├─TableReader(Build) root data:Selection
│ └─Selection cop[tikv] not(isnull(select.t2_65994.a))
│ └─TableFullScan cop[tikv] table:t2_65994 keep order:false, stats:pseudo
└─TableReader(Probe) root data:TableFullScan
└─TableFullScan cop[tikv] table:t1_65994 keep order:false, stats:pseudo
select /* issue:65994 */ t1_65994.a, t1_65994.b, t2_65994.b from t1_65994 left join t2_65994 on (t1_65994.a = t2_65994.a) order by t1_65994.a, t1_65994.b, t2_65994.b;
a b b
NULL 40 NULL
1 10 100
2 20 200
3 30 NULL
select /* issue:65994 */ t1_65994.a, t1_65994.b, t2_65994.b from t1_65994 left join t2_65994 on (t1_65994.a = t2_65994.a or 0) order by t1_65994.a, t1_65994.b, t2_65994.b;
a b b
NULL 40 NULL
1 10 100
2 20 200
3 30 NULL
drop table if exists t;
create table t(a bigint primary key, b bigint);
desc select * from t where a = 1;
Expand Down
11 changes: 11 additions & 0 deletions tests/integrationtest/t/select.test
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ drop table if exists t;
create table t (id int primary key, a int, b int);
explain format = 'plan_tree' select * from (t t1 left join t t2 on t1.a = t2.a) left join (t t3 left join t t4 on t3.a = t4.a) on t2.b = 1;

# test outer join other condition simplification, issue #65994
drop table if exists t1_65994;
drop table if exists t2_65994;
create table t1_65994 (a int, b int);
create table t2_65994 (a int, b int);
insert into t1_65994 values (1, 10), (2, 20), (3, 30), (null, 40);
insert into t2_65994 values (1, 100), (2, 200), (4, 400), (null, 500);
explain format = 'plan_tree' select /* issue:65994 */ * from t1_65994 left join t2_65994 on (t1_65994.a = t2_65994.a or 0);
select /* issue:65994 */ t1_65994.a, t1_65994.b, t2_65994.b from t1_65994 left join t2_65994 on (t1_65994.a = t2_65994.a) order by t1_65994.a, t1_65994.b, t2_65994.b;
select /* issue:65994 */ t1_65994.a, t1_65994.b, t2_65994.b from t1_65994 left join t2_65994 on (t1_65994.a = t2_65994.a or 0) order by t1_65994.a, t1_65994.b, t2_65994.b;

drop table if exists t;
create table t(a bigint primary key, b bigint);
desc select * from t where a = 1;
Expand Down