Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,11 @@ public MultiJoinOptimizeBushyRule(RelFactories.JoinFactory joinFactory,
final RelMetadataQuery mq = call.getMetadataQuery();

final LoptMultiJoin multiJoin = new LoptMultiJoin(multiJoinRel);
for (int i = 0; i < multiJoin.getNumJoinFactors(); i++) {
ImmutableBitSet outerJoinFactors = multiJoin.getOuterJoinFactors(i);
if (outerJoinFactors == null) {
continue;
}
if (!outerJoinFactors.isEmpty()) {
// Refuse to apply this rule to a multijoin with outer joins,
// since this rule cannot handle outer joins.
return;
}

if (multiJoinRel.isFullOuterJoin() || multiJoinRel.containsOuter()) {
// Refuse to apply this rule to a multijoin with outer joins,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this rule rebuild all joins as INNER joins? If so, could you mention this in the comment?

@ehds ehds Oct 4, 2026 •

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.

Yes. This rule rebuilds all join factors from LoptMultiJoin as INNER joins, so it cannot preserve outer-join semantics.
The issue was frist reported by https://issues.apache.org/jira/browse/CALCITE-5289

// since this rule cannot handle outer joins.
return;
}

final List<Vertex> vertexes = new ArrayList<>();
Expand Down
18 changes: 18 additions & 0 deletions core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4656,6 +4656,24 @@ private void checkPushJoinThroughUnionOnRightDoesNotMatchSemiOrAntiJoin(JoinRelT
sql(sql).withPre(preProgram).withProgram(program).check();
}

/** Test case for <a href="https://issues.apache.org/jira/browse/CALCITE-7833">[CALCITE-7833]
* MULTI_JOIN_OPTIMIZE_BUSHY may turn an outer join into an inner join</a>. */
@Test void testBushyJoinRuleSkipsFullOuterJoin() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you please add a comment referencing the jira above these test cases?

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.

Added the Jira reference to these test cases.

sql("select * from emp full join dept on emp.deptno = dept.deptno")
.withPreRule(CoreRules.JOIN_TO_MULTI_JOIN)
.withRule(CoreRules.MULTI_JOIN_OPTIMIZE_BUSHY)
.checkUnchanged();
}

/** Test case for <a href="https://issues.apache.org/jira/browse/CALCITE-7833">[CALCITE-7833]
* MULTI_JOIN_OPTIMIZE_BUSHY may turn an outer join into an inner join</a>. */
@Test void testBushyJoinRuleSkipsLeftJoinOnTrue() {
sql("select * from emp left join dept on true")
.withPreRule(CoreRules.JOIN_TO_MULTI_JOIN)
.withRule(CoreRules.MULTI_JOIN_OPTIMIZE_BUSHY)
.checkUnchanged();
}

@Test void testConvertMultiJoinRule() {
final String sql = "select e1.ename from emp e1, dept d, emp e2\n"
+ "where e1.deptno = d.deptno and d.deptno = e2.deptno";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1894,6 +1894,32 @@ LogicalProject(ENAME=[$1])
MultiJoin(joinFilter=[true], isFullOuterJoin=[false], joinTypes=[[INNER, LEFT]], outerJoinConditions=[[NULL, =($1, $10)]], projFields=[[ALL, ALL]])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
]]>
</Resource>
</TestCase>
<TestCase name="testBushyJoinRuleSkipsFullOuterJoin">
<Resource name="sql">
<![CDATA[select * from emp full join dept on emp.deptno = dept.deptno]]>
</Resource>
<Resource name="planBefore">
<![CDATA[
LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$9], NAME=[$10])
MultiJoin(joinFilter=[=($7, $9)], isFullOuterJoin=[true], joinTypes=[[INNER, INNER]], outerJoinConditions=[[NULL, NULL]], projFields=[[ALL, ALL]])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
]]>
</Resource>
</TestCase>
<TestCase name="testBushyJoinRuleSkipsLeftJoinOnTrue">
<Resource name="sql">
<![CDATA[select * from emp left join dept on true]]>
</Resource>
<Resource name="planBefore">
<![CDATA[
LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$9], NAME=[$10])
MultiJoin(joinFilter=[true], isFullOuterJoin=[false], joinTypes=[[INNER, LEFT]], outerJoinConditions=[[NULL, true]], projFields=[[ALL, ALL]])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
]]>
</Resource>
</TestCase>
Expand Down
Loading