This is an automated email from the ASF dual-hosted git repository.

mbudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new bddcbaf233 [CALCITE-6930] Implementing JoinConditionOrExpansionRule
bddcbaf233 is described below

commit bddcbaf2339c488fea1ece2b39e84d99e81a6ccb
Author: Zhen Chen <[email protected]>
AuthorDate: Fri Apr 11 22:58:01 2025 +0800

    [CALCITE-6930] Implementing JoinConditionOrExpansionRule
---
 .../org/apache/calcite/rel/rules/CoreRules.java    |   4 +
 .../apache/calcite/rel/rules/JoinCommuteRule.java  |   6 +
 .../rel/rules/JoinConditionOrExpansionRule.java    | 385 +++++++++++++++++++++
 .../java/org/apache/calcite/test/JdbcTest.java     | 175 ++++++++++
 .../org/apache/calcite/test/RelOptRulesTest.java   |  87 +++++
 .../org/apache/calcite/test/RelOptRulesTest.xml    | 174 ++++++++++
 6 files changed, 831 insertions(+)

diff --git a/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java 
b/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
index 012ae2ce39..c05f18bbaf 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
@@ -552,6 +552,10 @@ private CoreRules() {}
   public static final FilterJoinRule.JoinConditionPushRule JOIN_CONDITION_PUSH 
=
       
FilterJoinRule.JoinConditionPushRule.JoinConditionPushRuleConfig.DEFAULT.toRule();
 
+  /** Rule that transforms a join with OR conditions into a UNION ALL of 
multiple joins. */
+  public static final JoinConditionOrExpansionRule 
JOIN_CONDITION_OR_EXPANSION_RULE =
+          JoinConditionOrExpansionRule.Config.DEFAULT.toRule();
+
   /** Rule to add a semi-join into a {@link Join}. */
   public static final JoinAddRedundantSemiJoinRule 
JOIN_ADD_REDUNDANT_SEMI_JOIN =
       JoinAddRedundantSemiJoinRule.Config.DEFAULT.toRule();
diff --git 
a/core/src/main/java/org/apache/calcite/rel/rules/JoinCommuteRule.java 
b/core/src/main/java/org/apache/calcite/rel/rules/JoinCommuteRule.java
index f95d4b122e..c037e2fa9d 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/JoinCommuteRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/JoinCommuteRule.java
@@ -137,6 +137,12 @@ public JoinCommuteRule(Class<? extends Join> clazz,
         .build();
   }
 
+  public static RexNode swapJoinCond(RexNode cond, Join join, RexBuilder 
rexBuilder) {
+    final VariableReplacer variableReplacer =
+        new VariableReplacer(rexBuilder, join.getLeft().getRowType(), 
join.getRight().getRowType());
+    return variableReplacer.apply(cond);
+  }
+
   @Override public boolean matches(RelOptRuleCall call) {
     Join join = call.rel(0);
     // SEMI and ANTI join cannot be swapped.
diff --git 
a/core/src/main/java/org/apache/calcite/rel/rules/JoinConditionOrExpansionRule.java
 
b/core/src/main/java/org/apache/calcite/rel/rules/JoinConditionOrExpansionRule.java
new file mode 100644
index 0000000000..a5e2d670e5
--- /dev/null
+++ 
b/core/src/main/java/org/apache/calcite/rel/rules/JoinConditionOrExpansionRule.java
@@ -0,0 +1,385 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.calcite.rel.rules;
+
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.tools.RelBuilder;
+
+import org.immutables.value.Value;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Planner rule that matches a
+ * {@link org.apache.calcite.rel.core.Join}
+ * and expands OR clauses in join conditions.
+ *
+ * <p>This rule expands OR conditions in join clauses into
+ * multiple separate join conditions, allowing the optimizer
+ * to handle these conditions more efficiently.
+ *
+ * <p>The following is an example of inner join,
+ * and other examples for other kinds of joins
+ * can be found in the code below.
+ *
+ * <p>Project[*]
+ *    └── Join[OR(t1.id=t2.id, t1.age=t2.age), inner]
+ *        ├── TableScan[t1]
+ *        └── TableScan[t2]
+ *
+ * <p>into
+ *
+ * <p>Project[*]
+ *    └── UnionAll
+ *        ├── Join[t1.id=t2.id, inner]
+ *        │   ├── TableScan[t1]
+ *        │   └── TableScan[t2]
+ *        └── Join[t1.age=t2.age AND t1.id≠t2.id, inner]
+ *            ├─── TableScan[t1]
+ *            └─── TableScan[t2]
+ *
+ */
[email protected]
+public class JoinConditionOrExpansionRule
+    extends RelRule<JoinConditionOrExpansionRule.Config>
+    implements TransformationRule {
+
+  /** Creates an JoinConditionExpansionOrRule. */
+  protected JoinConditionOrExpansionRule(Config config) {
+    super(config);
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  @Override public boolean matches(RelOptRuleCall call) {
+    Join join = call.rel(0);
+    List<RexNode> orConds = RelOptUtil.disjunctions(join.getCondition());
+    return orConds.size() > 1;
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelBuilder relBuilder = call.builder();
+
+    RelNode expanded;
+    switch (join.getJoinType()) {
+    case INNER:
+      expanded = expandInnerJoin(join, relBuilder);
+      break;
+    case ANTI:
+      expanded = expandAntiJoin(join, relBuilder);
+      break;
+    case LEFT:
+      expanded = expandLeftOrRightJoin(join, true, relBuilder);
+      break;
+    case RIGHT:
+      expanded = expandLeftOrRightJoin(join, false, relBuilder);
+      break;
+    case FULL:
+      expanded = expandFullJoin(join, relBuilder);
+      break;
+    default:
+      return;
+    }
+    call.transformTo(expanded);
+  }
+
+  private List<RexNode> splitCond(Join join) {
+    final RexBuilder builder = join.getCluster().getRexBuilder();
+    final List<RexNode> orConds = RelOptUtil.disjunctions(join.getCondition());
+    final int leftFieldCount = join.getLeft().getRowType().getFieldCount();
+
+    final List<RexNode> result = new ArrayList<>();
+    final List<RexNode> otherBuffer = new ArrayList<>();
+
+    for (RexNode cond : orConds) {
+      if (isValidCond(cond, leftFieldCount)) {
+        if (!otherBuffer.isEmpty()) {
+          result.add(RexUtil.composeDisjunction(builder, otherBuffer));
+          otherBuffer.clear();
+        }
+        result.add(cond);
+      } else {
+        otherBuffer.add(cond);
+      }
+    }
+
+    if (!otherBuffer.isEmpty()) {
+      result.add(RexUtil.composeDisjunction(builder, otherBuffer));
+    }
+
+    return result;
+  }
+
+  private boolean isValidCond(RexNode node, int leftFieldCount) {
+    if (!(node instanceof RexCall)) {
+      return false;
+    }
+
+    RexCall call = (RexCall) node;
+    SqlKind kind = call.getKind();
+    switch (kind) {
+    case EQUALS:
+    case IS_NOT_DISTINCT_FROM:
+      RexNode left = call.getOperands().get(0);
+      RexNode right = call.getOperands().get(1);
+
+      if (left instanceof RexInputRef && right instanceof RexInputRef) {
+        RexInputRef leftRef = (RexInputRef) left;
+        RexInputRef rightRef = (RexInputRef) right;
+        return (leftRef.getIndex() < leftFieldCount && rightRef.getIndex() >= 
leftFieldCount)
+            || (leftRef.getIndex() >= leftFieldCount && rightRef.getIndex() < 
leftFieldCount);
+      }
+      return false;
+    case AND:
+      return call.getOperands().stream()
+          .allMatch(op -> isValidCond(op, leftFieldCount));
+    default:
+      return false;
+    }
+  }
+
+  /**
+   * This method will make the following conversions.
+   *
+   * <p>Project[*]
+   *    └── Join[OR(t1.id=t2.id, t1.age=t2.age), left]
+   *        ├── TableScan[t1]
+   *        └── TableScan[t2]
+   *
+   * <p>into
+   *
+   * <p>Project[*]
+   *    └── UnionAll
+   *        ├── Join[t1.id=t2.id, inner]
+   *        │   ├── TableScan[t1]
+   *        │   └── TableScan[t2]
+   *        ├── Join[t1.age=t2.age AND t1.id≠t2.id, inner]
+   *        │   ├── TableScan[t1]
+   *        │   └── TableScan[t2]
+   *        └── Project[t1-side cols + NULLs]
+   *            └── Join[t1.id=t2.id, anti]
+   *                ├── Join[t1.age=t2.age, anti]
+   *                │   ├── TableScan[t1]
+   *                │   └── TableScan[t2]
+   *                └── TableScan[t2]
+   */
+  private RelNode expandLeftOrRightJoin(Join join, boolean isLeftJoin,
+      RelBuilder relBuilder) {
+    List<RexNode> orConds = splitCond(join);
+    List<RelNode> joins = expandLeftOrRightJoinToRelNodes(join, orConds, 
isLeftJoin, relBuilder);
+    return relBuilder.pushAll(joins)
+        .union(true, joins.size())
+        .build();
+  }
+
+  private List<RelNode> expandLeftOrRightJoinToRelNodes(Join join, 
List<RexNode> orConds,
+      boolean isLeftJoin, RelBuilder relBuilder) {
+    List<RelNode> joins = new ArrayList<>();
+    joins.addAll(expandInnerJoinToRelNodes(join, orConds, relBuilder));
+    joins.add(expandAntiJoinToRelNode(join, orConds, isLeftJoin, true, 
relBuilder));
+    return joins;
+  }
+
+  /**
+   * This method will make the following conversions.
+   *
+   * <p>Project[*]
+   *    └── Join[OR(t1.id=t2.id, t1.age=t2.age), full]
+   *        ├── TableScan[t1]
+   *        └── TableScan[t2]
+   *
+   * <p>into
+   *
+   * <p>Project[*]
+   *    └── UnionAll
+   *        ├── Join[t1.id=t2.id, inner]
+   *        │   ├── TableScan[t1]
+   *        │   └── TableScan[t2]
+   *        ├── Join[t1.age=t2.age AND t1.id≠t2.id, inner]
+   *        │   ├── TableScan[t1]
+   *        │   └── TableScan[t2]
+   *        ├── Project[t1-side cols + NULLs]
+   *        │   └── Join[t1.id=t2.id, anti]
+   *        │       ├── Join[t1.age=t2.age, anti]
+   *        │       │   ├── TableScan[t1]
+   *        │       │   └── TableScan[t2]
+   *        │       └── TableScan[t2]
+   *        └── Project[NULLs + t2-side cols]
+   *            └── Join[t2.id=t1.id, anti]
+   *                ├── Join[t2.age=t1.age, anti]
+   *                │   ├── TableScan[t2]
+   *                │   └── TableScan[t1]
+   *                └── TableScan[t1]
+   */
+  private RelNode expandFullJoin(Join join, RelBuilder relBuilder) {
+    List<RexNode> orConds = splitCond(join);
+    List<RelNode> joins = new ArrayList<>();
+    joins.addAll(expandInnerJoinToRelNodes(join, orConds, relBuilder));
+    joins.add(expandAntiJoinToRelNode(join, orConds, false, true, relBuilder));
+    joins.add(expandAntiJoinToRelNode(join, orConds, true, true, relBuilder));
+
+    relBuilder.pushAll(joins)
+        .union(true, joins.size());
+
+    final List<RexNode> projects = join.getRowType().getFieldList().stream()
+        .map(field -> {
+          RexNode rexNode = relBuilder.field(field.getIndex());
+          return field.getType().equals(rexNode.getType())
+              ? rexNode
+              : relBuilder.getRexBuilder().makeCast(field.getType(), rexNode, 
true, false);
+        }).collect(Collectors.toList());
+
+    return relBuilder.project(projects)
+        .build();
+  }
+
+  /**
+   * This method will make the following conversions.
+   *
+   * <p>Project[*]
+   *    └── Join[OR(t1.id=t2.id, t1.age=t2.age), inner]
+   *        ├── TableScan[t1]
+   *        └── TableScan[t2]
+   *
+   * <p>into
+   *
+   * <p>Project[*]
+   *    └── UnionAll
+   *        ├── Join[t1.id=t2.id, inner]
+   *        │   ├── TableScan[t1]
+   *        │   └── TableScan[t2]
+   *        └── Join[t1.age=t2.age AND t1.id≠t2.id, inner]
+   *            ├─── TableScan[t1]
+   *            └─── TableScan[t2]
+   */
+  private RelNode expandInnerJoin(Join join, RelBuilder relBuilder) {
+    List<RexNode> orConds = splitCond(join);
+    List<RelNode> joins = expandInnerJoinToRelNodes(join, orConds, relBuilder);
+    return relBuilder.pushAll(joins)
+        .union(true, joins.size())
+        .build();
+  }
+
+  private List<RelNode> expandInnerJoinToRelNodes(Join join, List<RexNode> 
orConds,
+      RelBuilder relBuilder) {
+    List<RelNode> joins = new ArrayList<>();
+    for (int i = 0; i < orConds.size(); i++) {
+      RexNode orCond = orConds.get(i);
+      for (int j = 0; j < i; j++) {
+        orCond = relBuilder.and(orCond, relBuilder.not(orConds.get(j)));
+      }
+
+      relBuilder.push(join.getLeft())
+          .push(join.getRight())
+          .join(JoinRelType.INNER, orCond);
+
+      joins.add(relBuilder.build());
+    }
+    return joins;
+  }
+
+  /**
+   * This method will make the following conversions.
+   *
+   * <p>Project[*]
+   *    └── Join[OR(id=id0, age=age0), anti]
+   *        ├── TableScan[tbl]
+   *        └── TableScan[tbl]
+   *
+   * <p>into
+   *
+   * <p>HashJoin[id=id0, anti]
+   *    ├── HashJoin[age=age0, anti]
+   *    │   ├── TableScan[tbl]
+   *    │   └── TableScan[tbl]
+   *    └── TableScan[tbl]
+   */
+  private RelNode expandAntiJoin(Join join, RelBuilder relBuilder) {
+    List<RexNode> orConds = splitCond(join);
+    return expandAntiJoinToRelNode(join, orConds, true, false, relBuilder);
+  }
+
+  private RelNode expandAntiJoinToRelNode(Join join, List<RexNode> orConds,
+      boolean isLeftAnti, boolean isAppendNulls, RelBuilder relBuilder) {
+    RelNode left = isLeftAnti ? join.getLeft() : join.getRight();
+    RelNode right = isLeftAnti ? join.getRight() : join.getLeft();
+
+    RelNode top = left;
+    for (int i = 0; i < orConds.size(); i++) {
+      RexNode orCond = orConds.get(i);
+      relBuilder.push(top)
+          .push(right)
+          .join(JoinRelType.ANTI,
+              isLeftAnti
+                  ? orCond
+                  : JoinCommuteRule.swapJoinCond(orCond, join, 
relBuilder.getRexBuilder()));
+      top = relBuilder.build();
+    }
+
+    if (!isAppendNulls) {
+      return top;
+    }
+
+    relBuilder.push(top);
+    List<RexNode> fields = new ArrayList<>(relBuilder.fields());
+    List<RexNode> nulls = new ArrayList<>();
+    for (int i = 0; i < right.getRowType().getFieldCount(); i++) {
+      nulls.add(
+          relBuilder.getRexBuilder().makeNullLiteral(
+              right.getRowType().getFieldList().get(i).getType()));
+    }
+
+    List<RexNode> projects = isLeftAnti
+        ? Stream.concat(fields.stream(), 
nulls.stream()).collect(Collectors.toList())
+        : Stream.concat(nulls.stream(), 
fields.stream()).collect(Collectors.toList());
+
+    return relBuilder.project(projects)
+        .build();
+  }
+
+  /** Rule configuration. */
+  @Value.Immutable
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = ImmutableJoinConditionOrExpansionRule.Config.of()
+        .withOperandFor(Join.class);
+
+    @Override default JoinConditionOrExpansionRule toRule() {
+      return new JoinConditionOrExpansionRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Join> joinClass) {
+      return withOperandSupplier(b -> b.operand(joinClass).anyInputs())
+          .as(Config.class);
+    }
+  }
+}
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java 
b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index f9d6775a95..95b1266027 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -1975,6 +1975,181 @@ private void checkResultSetMetaData(Connection 
connection, String sql)
             "c0=Drink; c1=Dairy; c2=USA; c3=WA; c4=Bellingham");
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6930";>[CALCITE-6930]
+   * Implementing JoinConditionOrExpansionRule</a>. */
+  @Test void testJoinConditionOrExpansionRule() {
+    final String sql = ""
+        + "SELECT \"t1\".\"deptno\", \"t1\".\"empid\", \"t2\".\"deptno\", 
\"t2\".\"empid\"\n"
+        + "FROM \"hr\".\"emps\" AS \"t1\"\n"
+        + "INNER JOIN (\n"
+        + "    SELECT (\"deptno\" + 10) AS \"deptno\", (\"empid\" + 100) AS 
\"empid\" FROM \"hr\".\"emps\"\n"
+        + ") AS \"t2\"\n"
+        + "ON (\"t1\".\"deptno\" = \"t2\".\"deptno\") OR (\"t1\".\"empid\" = 
\"t2\".\"empid\")";
+
+    String[] returns = new String[] {
+        "deptno=20; empid=200; deptno=20; empid=200",
+        "deptno=20; empid=200; deptno=20; empid=210",
+        "deptno=20; empid=200; deptno=20; empid=250"};
+
+    CalciteAssert.hr()
+        .query(sql)
+        .returnsUnordered(returns);
+
+    CalciteAssert.hr()
+        .query(sql)
+        .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
+          planner.addRule(CoreRules.JOIN_CONDITION_OR_EXPANSION_RULE);
+          planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
+        })
+        .returnsUnordered(returns);
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6930";>[CALCITE-6930]
+   * Implementing JoinConditionOrExpansionRule</a>. */
+  @Test void testJoinConditionOrExpansionRuleMultiOr() {
+    final String sql = ""
+        + "SELECT \"t1\".\"deptno\", \"t1\".\"empid\", \"t2\".\"deptno\", 
\"t2\".\"empid\"\n"
+        + "FROM \"hr\".\"emps\" AS \"t1\"\n"
+        + "INNER JOIN (\n"
+        + "    SELECT (\"deptno\" + 10) AS \"deptno\", (\"empid\" + 100) AS 
\"empid\" FROM \"hr\".\"emps\"\n"
+        + ") AS \"t2\"\n"
+        + "ON (\"t1\".\"deptno\" = \"t2\".\"deptno\")"
+        + "OR (\"t1\".\"salary\" > 8000)"
+        + "OR (\"t1\".\"empid\" = \"t2\".\"empid\")";
+
+    String[] returns = new String[] {
+        "deptno=10; empid=100; deptno=20; empid=200",
+        "deptno=10; empid=100; deptno=20; empid=210",
+        "deptno=10; empid=100; deptno=20; empid=250",
+        "deptno=10; empid=100; deptno=30; empid=300",
+        "deptno=10; empid=110; deptno=20; empid=200",
+        "deptno=10; empid=110; deptno=20; empid=210",
+        "deptno=10; empid=110; deptno=20; empid=250",
+        "deptno=10; empid=110; deptno=30; empid=300",
+        "deptno=20; empid=200; deptno=20; empid=200",
+        "deptno=20; empid=200; deptno=20; empid=210",
+        "deptno=20; empid=200; deptno=20; empid=250"};
+
+    CalciteAssert.hr()
+        .query(sql)
+        .returnsUnordered(returns);
+
+    CalciteAssert.hr()
+        .query(sql)
+        .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
+          planner.addRule(CoreRules.JOIN_CONDITION_OR_EXPANSION_RULE);
+          planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
+        })
+        .returnsUnordered(returns);
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6930";>[CALCITE-6930]
+   * Implementing JoinConditionOrExpansionRule</a>. */
+  @Test void testJoinConditionOrExpansionRuleLeft() {
+    final String sql = ""
+        + "SELECT \"t1\".\"deptno\", \"t1\".\"empid\", \"t2\".\"deptno\", 
\"t2\".\"empid\"\n"
+        + "FROM \"hr\".\"emps\" AS \"t1\"\n"
+        + "LEFT JOIN (\n"
+        + "    SELECT (\"deptno\" + 10) AS \"deptno\", (\"empid\" + 100) AS 
\"empid\" FROM \"hr\".\"emps\"\n"
+        + ") AS \"t2\"\n"
+        + "ON (\"t1\".\"deptno\" = \"t2\".\"deptno\") OR (\"t1\".\"empid\" = 
\"t2\".\"empid\")";
+
+    String[] returns = new String[] {
+        "deptno=10; empid=100; deptno=null; empid=null",
+        "deptno=10; empid=110; deptno=null; empid=null",
+        "deptno=10; empid=150; deptno=null; empid=null",
+        "deptno=20; empid=200; deptno=20; empid=200",
+        "deptno=20; empid=200; deptno=20; empid=210",
+        "deptno=20; empid=200; deptno=20; empid=250"};
+
+    CalciteAssert.hr()
+        .query(sql)
+        .returnsUnordered(returns);
+
+    CalciteAssert.hr()
+        .query(sql)
+        .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
+          planner.addRule(CoreRules.JOIN_CONDITION_OR_EXPANSION_RULE);
+          planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
+        })
+        .returnsUnordered(returns);
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6930";>[CALCITE-6930]
+   * Implementing JoinConditionOrExpansionRule</a>. */
+  @Test void testJoinConditionOrExpansionRuleLeftUnion() {
+    final String sql = ""
+        + "SELECT \"t1\".\"deptno\", \"t1\".\"empid\", \"t2\".\"deptno\", 
\"t2\".\"empid\"\n"
+        + "FROM \"hr\".\"emps\" AS \"t1\"\n"
+        + "LEFT JOIN (\n"
+        + "    SELECT (\"deptno\" + 10) AS \"deptno\", (\"empid\" + 100) AS 
\"empid\" FROM \"hr\".\"emps\"\n"
+        + "    UNION ALL\n"
+        + "    SELECT (\"deptno\" + 10) AS \"deptno\", (\"empid\" + 100) AS 
\"empid\" FROM \"hr\".\"emps\"\n"
+        + ") AS \"t2\"\n"
+        + "ON (\"t1\".\"deptno\" = \"t2\".\"deptno\") OR (\"t1\".\"empid\" = 
\"t2\".\"empid\")";
+
+    String[] returns = new String[] {
+        "deptno=10; empid=100; deptno=null; empid=null",
+        "deptno=10; empid=110; deptno=null; empid=null",
+        "deptno=10; empid=150; deptno=null; empid=null",
+        "deptno=20; empid=200; deptno=20; empid=200",
+        "deptno=20; empid=200; deptno=20; empid=200",
+        "deptno=20; empid=200; deptno=20; empid=210",
+        "deptno=20; empid=200; deptno=20; empid=210",
+        "deptno=20; empid=200; deptno=20; empid=250",
+        "deptno=20; empid=200; deptno=20; empid=250"};
+
+    CalciteAssert.hr()
+        .query(sql)
+        .returnsUnordered(returns);
+
+    CalciteAssert.hr()
+        .query(sql)
+        .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
+          planner.addRule(CoreRules.JOIN_CONDITION_OR_EXPANSION_RULE);
+          planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
+        })
+        .returnsUnordered(returns);
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6930";>[CALCITE-6930]
+   * Implementing JoinConditionOrExpansionRule</a>. */
+  @Test void testJoinConditionOrExpansionRuleLeftWithNull() {
+    final String sql = ""
+        + "SELECT \"t1\".\"deptno\", \"t1\".\"commission\", \"t2\".\"deptno\", 
\"t2\".\"commission\"\n"
+        + "FROM \"hr\".\"emps\" AS \"t1\"\n"
+        + "LEFT JOIN (\n"
+        + "    SELECT (\"deptno\" + 10) AS \"deptno\", \"commission\" FROM 
\"hr\".\"emps\"\n"
+        + ") AS \"t2\"\n"
+        + "ON (\"t1\".\"deptno\" = \"t2\".\"deptno\") OR 
(\"t1\".\"commission\" = \"t2\".\"commission\")";
+
+    String[] returns = new String[] {
+        "deptno=10; commission=1000; deptno=20; commission=1000",
+        "deptno=10; commission=250; deptno=20; commission=250",
+        "deptno=10; commission=null; deptno=null; commission=null",
+        "deptno=20; commission=500; deptno=20; commission=1000",
+        "deptno=20; commission=500; deptno=20; commission=250",
+        "deptno=20; commission=500; deptno=20; commission=null",
+        "deptno=20; commission=500; deptno=30; commission=500"};
+
+    CalciteAssert.hr()
+        .query(sql)
+        .returnsUnordered(returns);
+
+    CalciteAssert.hr()
+        .query(sql)
+        .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
+          planner.addRule(CoreRules.JOIN_CONDITION_OR_EXPANSION_RULE);
+          planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
+        })
+        .returnsUnordered(returns);
+  }
+
   /** Four-way join. Used to take 80 seconds. */
   @Disabled
   @Test void testJoinFiveWay() {
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 7d6233a6f6..47db4459fd 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -8973,6 +8973,93 @@ private RelOptFixture spatial(String sql) {
         .check();
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6930";>[CALCITE-6930]
+   * Implementing JoinConditionOrExpansionRule</a>. */
+  @Test void testJoinConditionOrExpansionRule() {
+    String sql = "select *\n"
+        + "from EMP as p1\n"
+        + "inner join EMP as p2 on p1.empno = p2.empno or p1.mgr = p2.mgr";
+    sql(sql).withRule(CoreRules.JOIN_CONDITION_OR_EXPANSION_RULE)
+        .check();
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6930";>[CALCITE-6930]
+   * Implementing JoinConditionOrExpansionRule</a>. */
+  @Test void testJoinConditionOrExpansionRuleMultiOr() {
+    String sql = "select *\n"
+        + "from EMP as p1\n"
+        + "inner join EMP as p2 on  p1.mgr < p2.mgr or\n"
+        + "p1.empno = p2.empno or p1.sal < 0 or ln(p1.sal) < 10";
+    sql(sql).withRule(CoreRules.JOIN_CONDITION_OR_EXPANSION_RULE)
+        .check();
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6930";>[CALCITE-6930]
+   * Implementing JoinConditionOrExpansionRule</a>. */
+  @Test void testJoinConditionOrExpansionRuleRight() {
+    String sql = "select *\n"
+        + "from EMP as p1\n"
+        + "right join DEPT as p2 on p1.empno = p2.deptno or p1.ename < 
p2.name";
+    sql(sql).withRule(CoreRules.JOIN_CONDITION_OR_EXPANSION_RULE)
+        .check();
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6930";>[CALCITE-6930]
+   * Implementing JoinConditionOrExpansionRule</a>. */
+  @Test void testJoinConditionOrExpansionRuleLeft() {
+    String sql = "select *\n"
+        + "from EMP as p1\n"
+        + "left join EMP as p2 on p1.empno = p2.empno or p1.sal = p2.sal";
+    sql(sql).withRule(CoreRules.JOIN_CONDITION_OR_EXPANSION_RULE)
+        .check();
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6930";>[CALCITE-6930]
+   * Implementing JoinConditionOrExpansionRule</a>. */
+  @Test void testJoinConditionOrExpansionRuleAnti() {
+    final Function<RelBuilder, RelNode> relFn = b -> b
+        .scan("DEPT")
+        .scan("EMP")
+        .antiJoin(
+            b.or(
+              b.equals(
+                  b.field(2, 0, "DEPTNO"),
+                  b.field(2, 1, "DEPTNO")),
+              b.equals(
+                  b.field(2, 0, "DNAME"),
+                  b.field(2, 1, "JOB"))))
+        .build();
+    relFn(relFn)
+        .withRule(CoreRules.JOIN_CONDITION_OR_EXPANSION_RULE)
+        .check();
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6930";>[CALCITE-6930]
+   * Implementing JoinConditionOrExpansionRule</a>. */
+  @Test void testJoinConditionOrExpansionRuleFull() {
+    final Function<RelBuilder, RelNode> relFn = b -> b
+        .scan("DEPT")
+        .scan("EMP")
+        .join(JoinRelType.FULL,
+            b.or(
+                b.equals(
+                    b.field(2, 0, "DEPTNO"),
+                    b.field(2, 1, "DEPTNO")),
+                b.equals(
+                    b.field(2, 0, "DNAME"),
+                    b.field(2, 1, "JOB"))))
+        .build();
+    relFn(relFn)
+        .withRule(CoreRules.JOIN_CONDITION_OR_EXPANSION_RULE)
+        .check();
+  }
+
   @Test void testExchangeRemoveConstantKeysRule() {
     final Function<RelBuilder, RelNode> relFn = b -> b
         .scan("EMP")
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 27cbd80a40..4ff87b444e 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -6627,6 +6627,180 @@ LogicalProject(ADMINS=[$3])
   LogicalJoin(condition=[AND(=(COALESCE($3, CAST(ARRAY()):VARCHAR(5) NOT NULL 
ARRAY NOT NULL), COALESCE($8, CAST(ARRAY()):VARCHAR(5) NOT NULL ARRAY NOT 
NULL)), =(IS NULL($3), IS NULL($8)))], joinType=[inner])
     LogicalTableScan(table=[[CATALOG, SALES, DEPT_NESTED_EXPANDED]])
     LogicalTableScan(table=[[CATALOG, SALES, DEPT_NESTED_EXPANDED]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testJoinConditionOrExpansionRule">
+    <Resource name="sql">
+      <![CDATA[select *
+from EMP as p1
+inner join EMP as p2 on p1.empno = p2.empno or p1.mgr = p2.mgr]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], EMPNO0=[$9], ENAME0=[$10], 
JOB0=[$11], MGR0=[$12], HIREDATE0=[$13], SAL0=[$14], COMM0=[$15], 
DEPTNO0=[$16], SLACKER0=[$17])
+  LogicalJoin(condition=[OR(=($0, $9), =($3, $12))], joinType=[inner])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], EMPNO0=[$9], ENAME0=[$10], 
JOB0=[$11], MGR0=[$12], HIREDATE0=[$13], SAL0=[$14], COMM0=[$15], 
DEPTNO0=[$16], SLACKER0=[$17])
+  LogicalUnion(all=[true])
+    LogicalJoin(condition=[=($0, $9)], joinType=[inner])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalJoin(condition=[AND(=($3, $12), <>($0, $9))], joinType=[inner])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testJoinConditionOrExpansionRuleAnti">
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalJoin(condition=[OR(=($0, $10), =($1, $5))], joinType=[anti])
+  LogicalTableScan(table=[[scott, DEPT]])
+  LogicalTableScan(table=[[scott, EMP]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalJoin(condition=[=($1, $5)], joinType=[anti])
+  LogicalJoin(condition=[=($0, $10)], joinType=[anti])
+    LogicalTableScan(table=[[scott, DEPT]])
+    LogicalTableScan(table=[[scott, EMP]])
+  LogicalTableScan(table=[[scott, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testJoinConditionOrExpansionRuleFull">
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalJoin(condition=[OR(=($0, $10), =($1, $5))], joinType=[full])
+  LogicalTableScan(table=[[scott, DEPT]])
+  LogicalTableScan(table=[[scott, EMP]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalUnion(all=[true])
+  LogicalJoin(condition=[=($0, $10)], joinType=[inner])
+    LogicalTableScan(table=[[scott, DEPT]])
+    LogicalTableScan(table=[[scott, EMP]])
+  LogicalJoin(condition=[AND(=($1, $5), <>($0, $10))], joinType=[inner])
+    LogicalTableScan(table=[[scott, DEPT]])
+    LogicalTableScan(table=[[scott, EMP]])
+  LogicalProject($f0=[null:TINYINT], $f1=[null:VARCHAR(14)], 
$f2=[null:VARCHAR(13)], EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], 
HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7])
+    LogicalJoin(condition=[=($9, $2)], joinType=[anti])
+      LogicalJoin(condition=[=($8, $7)], joinType=[anti])
+        LogicalTableScan(table=[[scott, EMP]])
+        LogicalTableScan(table=[[scott, DEPT]])
+      LogicalTableScan(table=[[scott, DEPT]])
+  LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], $f3=[null:SMALLINT], 
$f4=[null:VARCHAR(10)], $f5=[null:VARCHAR(9)], $f6=[null:SMALLINT], 
$f7=[null:DATE], $f8=[null:DECIMAL(7, 2)], $f9=[null:DECIMAL(7, 2)], 
$f10=[null:TINYINT])
+    LogicalJoin(condition=[=($1, $5)], joinType=[anti])
+      LogicalJoin(condition=[=($0, $10)], joinType=[anti])
+        LogicalTableScan(table=[[scott, DEPT]])
+        LogicalTableScan(table=[[scott, EMP]])
+      LogicalTableScan(table=[[scott, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testJoinConditionOrExpansionRuleLeft">
+    <Resource name="sql">
+      <![CDATA[select *
+from EMP as p1
+left join EMP as p2 on p1.empno = p2.empno or p1.sal = p2.sal]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], EMPNO0=[$9], ENAME0=[$10], 
JOB0=[$11], MGR0=[$12], HIREDATE0=[$13], SAL0=[$14], COMM0=[$15], 
DEPTNO0=[$16], SLACKER0=[$17])
+  LogicalJoin(condition=[OR(=($0, $9), =($5, $14))], joinType=[left])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], EMPNO0=[$9], ENAME0=[$10], 
JOB0=[$11], MGR0=[$12], HIREDATE0=[$13], SAL0=[$14], COMM0=[$15], 
DEPTNO0=[$16], SLACKER0=[$17])
+  LogicalUnion(all=[true])
+    LogicalJoin(condition=[=($0, $9)], joinType=[inner])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalJoin(condition=[AND(=($5, $14), <>($0, $9))], joinType=[inner])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[null:INTEGER], 
$f10=[null:VARCHAR(20)], $f11=[null:VARCHAR(10)], $f12=[null:INTEGER], 
$f13=[null:TIMESTAMP(0)], $f14=[null:INTEGER], $f15=[null:INTEGER], 
$f16=[null:INTEGER], $f17=[null:BOOLEAN])
+      LogicalJoin(condition=[=($5, $14)], joinType=[anti])
+        LogicalJoin(condition=[=($0, $9)], joinType=[anti])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testJoinConditionOrExpansionRuleMultiOr">
+    <Resource name="sql">
+      <![CDATA[select *
+from EMP as p1
+inner join EMP as p2 on  p1.mgr < p2.mgr or
+p1.empno = p2.empno or p1.sal < 0 or ln(p1.sal) < 10]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], EMPNO0=[$9], ENAME0=[$10], 
JOB0=[$11], MGR0=[$12], HIREDATE0=[$13], SAL0=[$14], COMM0=[$15], 
DEPTNO0=[$16], SLACKER0=[$17])
+  LogicalJoin(condition=[OR(<($3, $12), =($0, $9), <($5, 0), <(LN($5), 
10.0E0))], joinType=[inner])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], EMPNO0=[$9], ENAME0=[$10], 
JOB0=[$11], MGR0=[$12], HIREDATE0=[$13], SAL0=[$14], COMM0=[$15], 
DEPTNO0=[$16], SLACKER0=[$17])
+  LogicalUnion(all=[true])
+    LogicalJoin(condition=[<($3, $12)], joinType=[inner])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalJoin(condition=[AND(=($0, $9), >=($3, $12))], joinType=[inner])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalJoin(condition=[AND(OR(<($5, 0), <(LN($5), 10.0E0)), >=($3, $12), 
<>($0, $9))], joinType=[inner])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testJoinConditionOrExpansionRuleRight">
+    <Resource name="sql">
+      <![CDATA[select *
+from EMP as p1
+right join DEPT as p2 on p1.empno = p2.deptno or p1.ename < p2.name]]>
+    </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])
+  LogicalJoin(condition=[OR(=($0, $9), <($1, CAST($10):VARCHAR(20) NOT 
NULL))], joinType=[right])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$9], NAME=[$10])
+  LogicalUnion(all=[true])
+    LogicalJoin(condition=[=($0, $9)], joinType=[inner])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+    LogicalJoin(condition=[AND(<($1, CAST($10):VARCHAR(20) NOT NULL), <>($0, 
$9))], joinType=[inner])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+    LogicalProject($f0=[null:INTEGER], $f1=[null:VARCHAR(20)], 
$f2=[null:VARCHAR(10)], $f3=[null:INTEGER], $f4=[null:TIMESTAMP(0)], 
$f5=[null:INTEGER], $f6=[null:INTEGER], $f7=[null:INTEGER], $f8=[null:BOOLEAN], 
DEPTNO=[$0], NAME=[$1])
+      LogicalJoin(condition=[<($3, CAST($1):VARCHAR(20) NOT NULL)], 
joinType=[anti])
+        LogicalJoin(condition=[=($2, $0)], joinType=[anti])
+          LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
     </Resource>
   </TestCase>


Reply via email to