From 74f96d9f46b06bc3785dc1bd660e4bb3f3a723ae Mon Sep 17 00:00:00 2001 From: bvolpato Date: Thu, 3 Sep 2026 13:15:12 -0400 Subject: [PATCH] SEMI JOIN should not duplicate a singleton left row --- .../rules/SingleValuesOptimizationRules.java | 2 ++ .../apache/calcite/test/RelOptRulesTest.java | 21 +++++++++++++++++++ .../apache/calcite/test/RelOptRulesTest.xml | 19 +++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/core/src/main/java/org/apache/calcite/rel/rules/SingleValuesOptimizationRules.java b/core/src/main/java/org/apache/calcite/rel/rules/SingleValuesOptimizationRules.java index d0e53be44d12..8c134ce1af39 100644 --- a/core/src/main/java/org/apache/calcite/rel/rules/SingleValuesOptimizationRules.java +++ b/core/src/main/java/org/apache/calcite/rel/rules/SingleValuesOptimizationRules.java @@ -284,8 +284,10 @@ static Predicate isJoinTransformable(boolean isLeft) { || jn.getJoinType() == JoinRelType.FULL; if (isLeft) { + // A semi join must emit the left row only once, even if several right rows match. return jn -> !(jn.getJoinType() == JoinRelType.LEFT || jn.getJoinType() == JoinRelType.LEFT_MARK + || jn.getJoinType() == JoinRelType.SEMI || isFullOrAntiJoin.test(jn)); } else { return jn -> !(jn.getJoinType() == JoinRelType.RIGHT || isFullOrAntiJoin.test(jn)); diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java index 76ff0a831335..822894f0a0ea 100644 --- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java @@ -5625,6 +5625,27 @@ RelOptFixture checkDynamicFunctions(boolean treatDynamicCallsAsConstant) { .check(); } + @Test void testSemiJoinWithConstantRowOnLeft() { + relFn(builder -> builder + .values(new String[]{"l"}, 1) + .values(new String[]{"r"}, 1, 1) + .semiJoin(builder.equals(builder.field(2, 0, 0), builder.field(2, 1, 0))) + .build()) + .withRule(SingleValuesOptimizationRules.JOIN_LEFT_INSTANCE) + .checkUnchanged(); + } + + @Test void testSemiJoinWithProjectOnConstantRowOnLeft() { + relFn(builder -> builder + .values(new String[]{"l"}, 1) + .projectPlus(builder.call(SqlStdOperatorTable.CURRENT_TIMESTAMP)) + .values(new String[]{"r"}, 1, 1) + .semiJoin(builder.equals(builder.field(2, 0, 0), builder.field(2, 1, 0))) + .build()) + .withRule(SingleValuesOptimizationRules.JOIN_LEFT_PROJECT_INSTANCE) + .checkUnchanged(); + } + @Test void testRightJoinWithConstantRowOnLeft() { final String sql = "select e.empno, e.ename, c.*" + " from (select 5 as nm) c right join emp e on e.empno = c.nm"; diff --git a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml index 35dc172427d2..4ae5fd499184 100644 --- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml @@ -18935,6 +18935,25 @@ LogicalProject(DEPTNO=[$0]) LogicalTableScan(table=[[CATALOG, SALES, EMP]]) LogicalProject(ACCTNO=[$0]) LogicalTableScan(table=[[CATALOG, CUSTOMER, ACCOUNT]]) +]]> + + + + + + + + + +