Repository: apache/calcite
Pull request #4267: [CALCITE-6914] Expand join-dependent predicates from disjunction
Author: login: silundong
Labels: name: LGTM-will-merge-soon
description: Overall PR looks OK. Only minor things left.
Created: 2025-03-26T10:35:32Z | Merged: 2025-04-16T18:05:46Z

For the following SQL:
`
select t1.name from t1, t2 
where t1.id = t2.id 
and ( 
    (t1.id > 20 and t2.height < 50)  
    or 
    (t1.weight < 200 and t2.sales > 100) 
) 
`
The disjunction `(t1.id > 20 and t2.height < 50) or (t1.weight < 200 and t2.sales > 100)` uses t1 and t2, thus cannot be pushed below the join. 
In fact, we can extract redundant `(t1.id > 20 or t1.weight < 200)` , `(t2.height < 50 or t2.sales > 100)` from this predicate by a new rule and push down these two predicates to both side of Join input.
More detail: https://issues.apache.org/jira/browse/CALCITE-6914


Diff:
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 31b47aedf9ca..a42eb6a4e1d2 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
@@ -850,4 +850,16 @@ private CoreRules() {}
   @Experimental
   public static final DphypJoinReorderRule HYPER_GRAPH_OPTIMIZE =
       DphypJoinReorderRule.Config.DEFAULT.toRule();
+
+  /** Rule that expands disjunction in the condition of a {@link Filter}.
+   *
+   * @see #EXPAND_JOIN_DISJUNCTION_GLOBAL */
+  public static final ExpandDisjunctionForTableRule EXPAND_FILTER_DISJUNCTION_GLOBAL =
+      ExpandDisjunctionForTableRule.Config.FILTER.toRule();
+
+  /** Rule that expands disjunction in the condition of a {@link Join}.
+   *
+   * @see #EXPAND_FILTER_DISJUNCTION_GLOBAL */
+  public static final ExpandDisjunctionForTableRule EXPAND_JOIN_DISJUNCTION_GLOBAL =
+      ExpandDisjunctionForTableRule.Config.JOIN.toRule();
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java b/core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
new file mode 100644
index 000000000000..a601f5ed8a95
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
@@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_GLOBAL
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_GLOBAL
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs.
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return The redundant predicate expanded from the condition parameter. It is formed by
+   * concatenating the predicates that can be pushed down to single table with AND.
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // For each table discover the conditions that refer to columns of that
+    // table (and refer to no other tables)
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column is computed by an expression that depends on multiple tables, skip it
+      if (relTableRefs.size() != 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjunctionForTableHelper expandHelper =
+        new ExpandDisjunctionForTableHelper(inputRefToTableRef, relBuilder, config.bloat());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjunctionForTableHelper {
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxNodeCount;
+
+    // Used to record the number of redundant expressions expanded.
+    private int currentCount;
+
+    private ExpandDisjunctionForTableHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxNodeCount) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxNodeCount = maxNodeCount;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  A map from a table to a (combined) predicate that can be pushed down
+     * and only depends on columns of the table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongToOneTable(inputRefs);
+      if (tableRef != null) {
+        // The condition already belongs to one table, return it directly
+        checkExpandCount(condition.nodeCount());
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          combinePredicatesUsingAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          combinePredicatesUsingOr(additionalConditions, operandResult);
+
+          if (additionalConditions.isEmpty()) {
+            break;
+          }
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongToOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (!tableRef.equals(inputBitTableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * Combine predicates that depend on the same table using conjunctions.
+     * The result is returned by modifying baseMap. For example:
+     *
+     * <p>baseMap: {t1: p1, t2: p2}
+     *
+     * <p>forMergeMap: {t1: p11, t3: p3}
+     *
+     * <p>result: {t1: p1 AND p11, t2: p2, t3: p3}
+     *
+     * @param baseMap       Additional predicates that current conjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void combinePredicatesUsingAnd(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      for (Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry : forMergeMap.entrySet()) {
+        RexNode mergedRex =
+            relBuilder.and(
+                entry.getValue(),
+                baseMap.getOrDefault(entry.getKey(), relBuilder.literal(true)));
+        int oriCount = entry.getValue().nodeCount()
+            + (baseMap.containsKey(entry.getKey())
+                ? baseMap.get(entry.getKey()).nodeCount()
+                : 0);
+        checkExpandCount(mergedRex.nodeCount() - oriCount);
+        baseMap.put(entry.getKey(), mergedRex);
+      }
+    }
+
+    /**
+     * Combine predicates that depend on the same table using disjunctions.
+     * The result is returned by modifying baseMap. For example:
+     *
+     * <p>baseMap: {t1: p1, t2: p2}
+     *
+     * <p>forMergeMap: {t1: p11, t3: p3}
+     *
+     * <p>result: {t1: p1 OR p11}
+     *
+     * @param baseMap       Additional predicates that current disjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void combinePredicatesUsingOr(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      if (baseMap.isEmpty()) {
+        return;
+      }
+
+      Iterator<Map.Entry<RexTableInputRef.RelTableRef, RexNode>> iterator =
+          baseMap.entrySet().iterator();
+      while (iterator.hasNext()) {
+        int forMergeNodeCount = 0;
+
+        Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry = iterator.next();
+        if (!forMergeMap.containsKey(entry.getKey())) {
+          checkExpandCount(-entry.getValue().nodeCount());
+          iterator.remove();
+          continue;
+        } else {
+          forMergeNodeCount = forMergeMap.get(entry.getKey()).nodeCount();
+        }
+        RexNode mergedRex =
+            relBuilder.or(
+                entry.getValue(),
+                forMergeMap.get(entry.getKey()));
+        int oriCount = entry.getValue().nodeCount() + forMergeNodeCount;
+        checkExpandCount(mergedRex.nodeCount() - oriCount);
+        baseMap.put(entry.getKey(), mergedRex);
+      }
+    }
+
+    /**
+     * Check whether the number of redundant expressions generated in the expansion
+     * exceeds the limit.
+     */
+    private void checkExpandCount(int changeCount) {
+      currentCount += changeCount;
+      if (maxNodeCount > 0 && currentCount > maxNodeCount) {
+        throw OverLimitException.INSTANCE;
+      }
+    }
+
+    /** Exception to catch when we pass the limit. */
+    private static class OverLimitException extends ControlFlowException {
+      protected static final OverLimitException INSTANCE = new OverLimitException();
+
+      private OverLimitException() {}
+    }
+  }
+
+  /** Rule configuration. */
+  @Value.Immutable(singleton = false)
+  public interface Config extends RelRule.Config {
+    Config FILTER = ImmutableExpandDisjunctionForTableRule.Config.builder()
+        .withMatchHandler(ExpandDisjunctionForTableRule::matchFilter)
+        .build()
+        .withOperandSupplier(b -> b.operand(Filter.class).anyInputs());
+
+    Config JOIN = ImmutableExpandDisjunctionForTableRule.Config.builder()
+        .withMatchHandler(ExpandDisjunctionForTableRule::matchJoin)
+        .build()
+        .withOperandSupplier(b -> b.operand(Join.class).anyInputs());
+
+    @Override default ExpandDisjunctionForTableRule toRule() {
+      return new ExpandDisjunctionForTableRule(this);
+    }
+
+    /**
+     * Limit to the size growth of the condition allowed during expansion.
+     * Default is {@link RelOptUtil#DEFAULT_BLOAT} (100).
+     */
+    default int bloat() {
+      return RelOptUtil.DEFAULT_BLOAT;
+    }
+
+    /** Forwards a call to {@link #onMatch(RelOptRuleCall)}. */
+    MatchHandler<ExpandDisjunctionForTableRule> matchHandler();
+
+    /** Sets {@link #matchHandler()}. */
+    Config withMatchHandler(MatchHandler<ExpandDisjunctionForTableRule> matchHandler);
+  }
+}
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 887f93c9a2e8..da8465b643cd 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -10149,4 +10149,40 @@ private void checkLoptOptimizeJoinRule(LoptOptimizeJoinRule rule) {
       p.addRule(EnumerableRules.ENUMERABLE_PROJECT_RULE);
     }).check();
   }
+
+  @Test void testExpandFilterDisjunctionForTable() {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(CoreRules.EXPAND_FILTER_DISJUNCTION_GLOBAL)
+        .build();
+
+    sql("select e.empno from emp as e, empnullables as en "
+        + "where e.empno = en.empno "
+        + "and "
+        + "( "
+        + "  (e.mgr > 100 and en.sal < 200) "
+        + "  or "
+        + "  e.comm < 50 "
+        + ") ")
+        .withPre(program)
+        .withRule(CoreRules.FILTER_INTO_JOIN)
+        .check();
+  }
+
+  @Test void testExpandJoinDisjunctionForTable() {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(CoreRules.EXPAND_JOIN_DISJUNCTION_GLOBAL)
+        .build();
+
+    sql("select e.empno from emp as e inner join empnullables as en "
+        + "on e.empno = en.empno "
+        + "and "
+        + "( "
+        + "  (e.mgr > 100 and en.sal < 200) "
+        + "  or "
+        + "  (e.comm < 50 and en.deptno > 10) "
+        + ") ")
+        .withPre(program)
+        .withRule(CoreRules.JOIN_CONDITION_PUSH)
+        .check();
+  }
 }
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 c1f7b3c1a453..ae9346e93a80 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -4233,6 +4233,29 @@ LogicalProject(EMPNO=[$0])
               LogicalProject(DEPTNO=[$0])
                 LogicalFilter(condition=[=($1, 'dept2')])
                   LogicalTableScan(table=[[CATALOG, SALES, DEPTNULLABLES]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testExpandFilterDisjunctionForTable">
+    <Resource name="sql">
+      <![CDATA[select e.empno from emp as e, empnullables as en where e.empno = en.empno and (   (e.mgr > 100 and en.sal < 200)   or   e.comm < 50 ) ]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(EMPNO=[$0])
+  LogicalFilter(condition=[AND(=($0, $9), OR(AND(>($3, 100), <($14, 200)), <($6, 50)), OR(>($3, 100), <($6, 50)))])
+    LogicalJoin(condition=[true], joinType=[inner])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(EMPNO=[$0])
+  LogicalJoin(condition=[AND(=($0, $9), OR(AND(>($3, 100), <($14, 200)), <($6, 50)))], joinType=[inner])
+    LogicalFilter(condition=[OR(>($3, 100), <($6, 50))])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
 ]]>
     </Resource>
   </TestCase>
@@ -4625,6 +4648,29 @@ LogicalProject(EMPNO=[$0])
           LogicalProject(DEPTNO=[$7])
             LogicalFilter(condition=[>($0, 100)])
               LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testExpandJoinDisjunctionForTable">
+    <Resource name="sql">
+      <![CDATA[select e.empno from emp as e inner join empnullables as en on e.empno = en.empno and (   (e.mgr > 100 and en.sal < 200)   or   (e.comm < 50 and en.deptno > 10) ) ]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(EMPNO=[$0])
+  LogicalJoin(condition=[AND(=($0, $9), OR(AND(>($3, 100), <($14, 200)), AND(<($6, 50), >($16, 10))), OR(<($14, 200), >($16, 10)), OR(>($3, 100), <($6, 50)))], joinType=[inner])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(EMPNO=[$0])
+  LogicalJoin(condition=[AND(=($0, $9), OR(AND(>($3, 100), <($14, 200)), AND(<($6, 50), >($16, 10))))], joinType=[inner])
+    LogicalFilter(condition=[OR(>($3, 100), <($6, 50))])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+    LogicalFilter(condition=[OR(<($5, 200), >($7, 10))])
+      LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
 ]]>
     </Resource>
   </TestCase>


Reviews:
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T17:39:11Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T06:54:15Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T06:55:21Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T07:11:57Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T07:22:13Z
user: login: asolimando
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T10:54:51Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T09:47:22Z
user: login: asolimando
body: @silundong I don't see anything trying to prevent the rule to be applied indefinitely. Now that we have the maximum expansion it might stop at some point but this won't prevent the rule to be tried. I have seen lots of similar problems leading to OOMs in the past, I think it's dangerous to integrate such a rule as-is, and most definitely I wouldn't put it into the standard core rules which get applied by default.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-31T10:53:17Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T18:27:39Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T18:29:14Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T18:53:49Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T21:26:36Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T00:56:22Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T01:03:45Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T03:07:40Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T03:10:33Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T03:12:07Z
user: login: asolimando
body: @silundong left a few comments, can you please take a look?
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-02T17:49:18Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T01:14:31Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T01:42:19Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T02:37:12Z
user: login: asolimando
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T09:25:27Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T08:29:55Z
user: login: mihaibudiu
body: I think this is pretty good, I mostly made comments about the documentation.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T18:38:10Z
user: login: mihaibudiu
body: My comments are only about comments, but otherwise this looks good to merge.
I apologize if my previous suggestions were not clear enough.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T18:43:42Z
user: login: xuzifu666
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T06:57:32Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T07:00:43Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T07:09:38Z
user: login: xuzifu666
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T07:11:39Z
user: login: silundong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T07:19:36Z
user: login: asolimando
body: LGTM, all the important changes I asked were applied already
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-16T07:55:57Z

Review comments:
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder);
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expandDeep(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: mihaibudiu
body: The rule parameter seems to be unused. Same in matchJoin below.
created_at: 2025-03-27T17:19:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: mihaibudiu
body: Since you have JavaDoc, I would describe the non-obvious fields. fieldList is the most important one.
created_at: 2025-03-27T17:23:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder);
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expandDeep(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: mihaibudiu
body: what is the complexity of this expansion?
the case I worry about is a deep expression that generates an amount of work which is exponential in the depth of the expression.
created_at: 2025-03-27T17:32:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: mihaibudiu
body: why does the input have to come from a table?
you can pull predicates in joins even if they come from a single JOIN input, not a single table input.
created_at: 2025-03-27T17:36:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder);
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expandDeep(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: Yes, it wasn't used. The interface MatchHandler extends BiConsumer:
`MatchHandler<R extends RelOptRule> extends BiConsumer<R, RelOptRuleCall>`
It must have two parameters: RelOptRule and RelOptRuleCall. I hope to use MatchHandler to easily match Filter and Join, but I don't need the RelOptRule parameter. 
If the rule parameter must be deleted here, do you have any good suggestions?
created_at: 2025-03-28T06:54:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: I will update it.
created_at: 2025-03-28T06:55:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder);
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expandDeep(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: The complexity here depends on the size of the expression tree with AND and OR as non-leaf nodes and other expressions as leaf nodes. I don't think the expression tree would be particularly deep for usual SQL. The code currently has an early return logic, that is, the columns used in the current expression are all from the same table.
created_at: 2025-03-28T07:11:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: Taking matching to Filter as an example, I don’t know the structure of Joins under the current Filter, which columns belong to which Join, I only know which table a column comes from, so at this time I can only expand the additional predicates that belong to a single table from the disjunction and push them down to TableScan later.
created_at: 2025-03-28T07:22:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder);
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expandDeep(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: In similar cases, whenever we risk a combinatorial explosion, we have a parameter to control it, because what everyone of us consider usual SQL varies a lot in my experience, and there are machine-generated SQL statements that can be 10MB in query text, we must have a way to impose a limit.

Does something like [this](https://github.com/search?q=repo%3Aapache%2Fcalcite%20bloat&type=code) would be applicable here?
created_at: 2025-03-28T10:54:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder);
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expandDeep(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: Thank you for your further explanation, it seems that restriction are necessary, I will update.
created_at: 2025-03-30T09:47:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: ```suggestion
   *                    in the predicate as RexInputRef and find out which table the RexInputRef
```
created_at: 2025-03-31T10:06:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder);
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expandDeep(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: mihaibudiu
body: If the parameter has to be there, just call it unusedCall.
This is a private function, I don't understand how it helps implement BiConsumer.
created_at: 2025-03-31T18:27:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: mihaibudiu
body: Indeed, for filter this is true, but for joins it may be useful.
This could be a second optimization rule that you could add, which does not use tables to separate conditions, but only the join inputs. Would you consider implementing that? I think it could be very useful.

created_at: 2025-03-31T18:29:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -841,4 +841,16 @@ private CoreRules() {}
   @Experimental
   public static final DphypJoinReorderRule HYPER_GRAPH_OPTIMIZE =
       DphypJoinReorderRule.Config.DEFAULT.toRule();
+
+  /** Rule that expand disjuction in condition of a {@link Filter}.
+   *
+   * @see #EXPAND_JOIN_DISJUCTION */
+  public static final ExpandDisjuctionRule EXPAND_FILTER_DISJUCTION =
path: core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
user: login: mihaibudiu
body: the word is DISJUNCTION, with an N
created_at: 2025-03-31T18:53:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder);
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expandDeep(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: mihaibudiu
body: Are you still planning to add a blot control?
Please use new commits, to make reviewing easier, and then request a new review
created_at: 2025-03-31T21:26:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder);
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expandDeep(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: `matchFilter` and `matchJoin` are called by `withMatchHandler` in Config. The parameter of `withMatchHandler` is `MatchHandler`, which inherits `BiConsumer`.
But now `matchFilter` and `matchJoin` have used the rule parameter, so I think this problem has been solved.
created_at: 2025-04-01T00:56:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder);
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expandDeep(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: @mihaibudiu I have added a restriction. As @asolimando  said, I directly force pushed and requested your second review. I will avoid force pushing for future commits. Sorry for the difficulty in reviewing this time.
created_at: 2025-04-01T01:03:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,335 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   */
+  private static RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: Yes, it could be another new rule. This new rule :
1. Only matches Join and expands the disjunction in the condition for both inputs.
2. Same as the notes for ExpandDisjunctionRule, it must be applied before FilterIntoJoinRule and only once (described in [jira 6914](https://issues.apache.org/jira/browse/CALCITE-6914)).

If the above two points are ok, I will try to implement it in the current PR. @mihaibudiu 
created_at: 2025-04-01T03:07:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: I will update it.
created_at: 2025-04-01T03:10:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -841,4 +841,16 @@ private CoreRules() {}
   @Experimental
   public static final DphypJoinReorderRule HYPER_GRAPH_OPTIMIZE =
       DphypJoinReorderRule.Config.DEFAULT.toRule();
+
+  /** Rule that expand disjuction in condition of a {@link Filter}.
+   *
+   * @see #EXPAND_JOIN_DISJUCTION */
+  public static final ExpandDisjuctionRule EXPAND_FILTER_DISJUCTION =
path: core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
user: login: silundong
body: I will update it.
created_at: 2025-04-01T03:12:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: I know it's more verbose, but I think it's easier to follow if we show the rewrite proposed for the whole query, can you please add that here?

It's 100% clear upon reading the code, but people should be able to get a precise understanding of what to expect from the doc alone.
created_at: 2025-04-02T16:52:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: ```suggestion
   *                    comes from {@link RelMetadataQuery#getExpressionLineage}
```
created_at: 2025-04-02T16:55:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: ```suggestion
      // If the column comes from multiple tables, skip it
```
created_at: 2025-04-02T17:02:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: Prefer interface to concrete classes unless there is a good reason not to, you might consider using the `final` modifier to stress the fact that the variable is not going to be overwritten later on (the method is long enough that such hint helps), in this and other cases where relevant.
created_at: 2025-04-02T17:03:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: Nit: you should group together (i.e., no newline) lines that make sense to be considered together (like a variable used in a loop coming right after it, or a variable used in the following statement), and split groups of lines that are conceptually separated, like "extract input ref and their mappings to tables" and "computing the new filter conditions".

I know it's not a deal breaker, but I find it quite hard to read as-is, it would be nice if you could do a pass over the code to improve readability.

Here, for instance, I'd put a newline between 112 and 113 (which can go with the `if` that follows as they are strictly related).

For formatting, you can generally take a look around at similar code to take inspiration.
created_at: 2025-04-02T17:21:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxProcessLimitNodes) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxProcessLimitNodes = maxProcessLimitNodes;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      incrementAndCheck();
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: Nit: can't you handle the case for element 0 in the for loop as well?
created_at: 2025-04-02T17:29:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxProcessLimitNodes) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxProcessLimitNodes = maxProcessLimitNodes;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      incrementAndCheck();
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          mergeOr(additionalConditions, operandResult);
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongOneTable(
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: Nit: inputRefsBelongToOneTable?
created_at: 2025-04-02T17:30:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxProcessLimitNodes) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxProcessLimitNodes = maxProcessLimitNodes;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      incrementAndCheck();
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          mergeOr(additionalConditions, operandResult);
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (inputBitTableRef == null || !inputBitTableRef.equals(tableRef)) {
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: ```suggestion
        if (!tableRef.equals(inputBitTableRef)) {
```
you know from line 275 that `tableRef` is not null, `equals` will return `false` in case `inputBitTableRef` is `null` or different, so you can handle both cases in a single shot
created_at: 2025-04-02T17:34:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxProcessLimitNodes) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxProcessLimitNodes = maxProcessLimitNodes;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      incrementAndCheck();
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          mergeOr(additionalConditions, operandResult);
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (inputBitTableRef == null || !inputBitTableRef.equals(tableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * For additional predicates of each operand in conjuction, all of them should be retained and
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: conjuNction (I saw Mihai spotted a similar mispelling for disjunction), can you do a massive grep on this?
created_at: 2025-04-02T17:35:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxProcessLimitNodes) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxProcessLimitNodes = maxProcessLimitNodes;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      incrementAndCheck();
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          mergeOr(additionalConditions, operandResult);
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (inputBitTableRef == null || !inputBitTableRef.equals(tableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * For additional predicates of each operand in conjuction, all of them should be retained and
+     * use 'AND' to combine expressions.
+     *
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: Can you provide a simple example in the javadoc explaining what the method does? similarly for `mergeOr`.
created_at: 2025-04-02T17:36:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxProcessLimitNodes) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxProcessLimitNodes = maxProcessLimitNodes;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      incrementAndCheck();
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          mergeOr(additionalConditions, operandResult);
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (inputBitTableRef == null || !inputBitTableRef.equals(tableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * For additional predicates of each operand in conjuction, all of them should be retained and
+     * use 'AND' to combine expressions.
+     *
+     * @param baseMap       Additional predicates that current conjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeAnd(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      for (Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry : forMergeMap.entrySet()) {
+        RexNode mergeExpression =
+            relBuilder.and(
+                entry.getValue(),
+                baseMap.getOrDefault(entry.getKey(), relBuilder.literal(true)));
+        baseMap.put(entry.getKey(), mergeExpression);
+      }
+    }
+
+    /**
+     * Only if all operands in disjunction have additional predicates for table t1, we use 'OR'
+     * to combine expressions and retain them as additional predicates of table t1.
+     *
+     * @param baseMap       Additional predicates that current disjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeOr(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      if (baseMap.isEmpty()) {
+        return;
+      }
+
+      Iterator<Map.Entry<RexTableInputRef.RelTableRef, RexNode>> iterator =
+          baseMap.entrySet().iterator();
+      while (iterator.hasNext()) {
+        Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry = iterator.next();
+        if (!forMergeMap.containsKey(entry.getKey())) {
+          iterator.remove();
+          continue;
+        }
+        RexNode mergedRex =
+            relBuilder.or(
+                entry.getValue(),
+                forMergeMap.get(entry.getKey()));
+        baseMap.put(entry.getKey(), mergedRex);
+      }
+    }
+
+    private void incrementAndCheck() {
+      if (maxProcessLimitNodes > 0 && ++currentCount > maxProcessLimitNodes) {
+        throw OverLimitException.INSTANCE;
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: throwing creates a stacktrace, which is a very expensive operation, we should use a boolean guard (possibly in an array if mutating it in other places is needed) to indicate that we should "exit", you can probably take inspiration from how [RexShuttle does it](https://github.com/apache/calcite/blob/b394abca407eea06ee8f8b50f30bd443f286bb42/core/src/main/java/org/apache/calcite/rex/RexShuttle.java#L42)
created_at: 2025-04-02T17:42:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: This reminds me somewhat a `RexVisitor`, can you take a look at some implementations in the project to see if it can fit your needs?
created_at: 2025-04-02T17:45:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxProcessLimitNodes) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxProcessLimitNodes = maxProcessLimitNodes;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      incrementAndCheck();
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          mergeOr(additionalConditions, operandResult);
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (inputBitTableRef == null || !inputBitTableRef.equals(tableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * For additional predicates of each operand in conjuction, all of them should be retained and
+     * use 'AND' to combine expressions.
+     *
+     * @param baseMap       Additional predicates that current conjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeAnd(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      for (Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry : forMergeMap.entrySet()) {
+        RexNode mergeExpression =
+            relBuilder.and(
+                entry.getValue(),
+                baseMap.getOrDefault(entry.getKey(), relBuilder.literal(true)));
+        baseMap.put(entry.getKey(), mergeExpression);
+      }
+    }
+
+    /**
+     * Only if all operands in disjunction have additional predicates for table t1, we use 'OR'
+     * to combine expressions and retain them as additional predicates of table t1.
+     *
+     * @param baseMap       Additional predicates that current disjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeOr(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      if (baseMap.isEmpty()) {
+        return;
+      }
+
+      Iterator<Map.Entry<RexTableInputRef.RelTableRef, RexNode>> iterator =
+          baseMap.entrySet().iterator();
+      while (iterator.hasNext()) {
+        Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry = iterator.next();
+        if (!forMergeMap.containsKey(entry.getKey())) {
+          iterator.remove();
+          continue;
+        }
+        RexNode mergedRex =
+            relBuilder.or(
+                entry.getValue(),
+                forMergeMap.get(entry.getKey()));
+        baseMap.put(entry.getKey(), mergedRex);
+      }
+    }
+
+    private void incrementAndCheck() {
+      if (maxProcessLimitNodes > 0 && ++currentCount > maxProcessLimitNodes) {
+        throw OverLimitException.INSTANCE;
+      }
+    }
+
+    /** Exception to catch when we pass the limit. */
+    private static class OverLimitException extends ControlFlowException {
+      protected static final OverLimitException INSTANCE = new OverLimitException();
+
+      private OverLimitException() {}
+    }
+  }
+
+  /** Rule configuration. */
+  @Value.Immutable(singleton = false)
+  public interface Config extends RelRule.Config {
+    Config FILTER = ImmutableExpandDisjuctionRule.Config.builder()
+        .withMatchHandler(ExpandDisjuctionRule::matchFilter)
+        .build()
+        .withOperandSupplier(b ->
+            b.operand(Filter.class).anyInputs());
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: Nit: If you fit into 80 chars you can always place it in a single line
created_at: 2025-04-02T17:46:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxProcessLimitNodes) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxProcessLimitNodes = maxProcessLimitNodes;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      incrementAndCheck();
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          mergeOr(additionalConditions, operandResult);
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (inputBitTableRef == null || !inputBitTableRef.equals(tableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * For additional predicates of each operand in conjuction, all of them should be retained and
+     * use 'AND' to combine expressions.
+     *
+     * @param baseMap       Additional predicates that current conjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeAnd(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      for (Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry : forMergeMap.entrySet()) {
+        RexNode mergeExpression =
+            relBuilder.and(
+                entry.getValue(),
+                baseMap.getOrDefault(entry.getKey(), relBuilder.literal(true)));
+        baseMap.put(entry.getKey(), mergeExpression);
+      }
+    }
+
+    /**
+     * Only if all operands in disjunction have additional predicates for table t1, we use 'OR'
+     * to combine expressions and retain them as additional predicates of table t1.
+     *
+     * @param baseMap       Additional predicates that current disjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeOr(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      if (baseMap.isEmpty()) {
+        return;
+      }
+
+      Iterator<Map.Entry<RexTableInputRef.RelTableRef, RexNode>> iterator =
+          baseMap.entrySet().iterator();
+      while (iterator.hasNext()) {
+        Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry = iterator.next();
+        if (!forMergeMap.containsKey(entry.getKey())) {
+          iterator.remove();
+          continue;
+        }
+        RexNode mergedRex =
+            relBuilder.or(
+                entry.getValue(),
+                forMergeMap.get(entry.getKey()));
+        baseMap.put(entry.getKey(), mergedRex);
+      }
+    }
+
+    private void incrementAndCheck() {
+      if (maxProcessLimitNodes > 0 && ++currentCount > maxProcessLimitNodes) {
+        throw OverLimitException.INSTANCE;
+      }
+    }
+
+    /** Exception to catch when we pass the limit. */
+    private static class OverLimitException extends ControlFlowException {
+      protected static final OverLimitException INSTANCE = new OverLimitException();
+
+      private OverLimitException() {}
+    }
+  }
+
+  /** Rule configuration. */
+  @Value.Immutable(singleton = false)
+  public interface Config extends RelRule.Config {
+    Config FILTER = ImmutableExpandDisjuctionRule.Config.builder()
+        .withMatchHandler(ExpandDisjuctionRule::matchFilter)
+        .build()
+        .withOperandSupplier(b ->
+            b.operand(Filter.class).anyInputs());
+
+    Config JOIN = ImmutableExpandDisjuctionRule.Config.builder()
+        .withMatchHandler(ExpandDisjuctionRule::matchJoin)
+        .build()
+        .withOperandSupplier(b ->
+            b.operand(Join.class).anyInputs());
+
+    @Override default ExpandDisjuctionRule toRule() {
+      return new ExpandDisjuctionRule(this);
+    }
+
+    default int processLimit() {
+      return 10000;
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: This is configurable, right? 10k is just the default?
created_at: 2025-04-02T17:47:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxProcessLimitNodes) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxProcessLimitNodes = maxProcessLimitNodes;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      incrementAndCheck();
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: For the operands of Or, I want to take the common set of single table among them. For example : 
Or
  (t1.p1, t2.p1, t3.p1)
  (t2.p2, t3.p2, t4.p2)
  (t2.p3, t3.p3, t5.p3)

t2, t3 are tables that exist for each operand, so I can extract the predicates involved.
Handle the 0th element first, that is to get the initial state of the common set. Subsequent loop will only reduce the number of elements in this common set.
created_at: 2025-04-03T01:14:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxProcessLimitNodes) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxProcessLimitNodes = maxProcessLimitNodes;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      incrementAndCheck();
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          mergeOr(additionalConditions, operandResult);
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (inputBitTableRef == null || !inputBitTableRef.equals(tableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * For additional predicates of each operand in conjuction, all of them should be retained and
+     * use 'AND' to combine expressions.
+     *
+     * @param baseMap       Additional predicates that current conjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeAnd(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      for (Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry : forMergeMap.entrySet()) {
+        RexNode mergeExpression =
+            relBuilder.and(
+                entry.getValue(),
+                baseMap.getOrDefault(entry.getKey(), relBuilder.literal(true)));
+        baseMap.put(entry.getKey(), mergeExpression);
+      }
+    }
+
+    /**
+     * Only if all operands in disjunction have additional predicates for table t1, we use 'OR'
+     * to combine expressions and retain them as additional predicates of table t1.
+     *
+     * @param baseMap       Additional predicates that current disjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeOr(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      if (baseMap.isEmpty()) {
+        return;
+      }
+
+      Iterator<Map.Entry<RexTableInputRef.RelTableRef, RexNode>> iterator =
+          baseMap.entrySet().iterator();
+      while (iterator.hasNext()) {
+        Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry = iterator.next();
+        if (!forMergeMap.containsKey(entry.getKey())) {
+          iterator.remove();
+          continue;
+        }
+        RexNode mergedRex =
+            relBuilder.or(
+                entry.getValue(),
+                forMergeMap.get(entry.getKey()));
+        baseMap.put(entry.getKey(), mergedRex);
+      }
+    }
+
+    private void incrementAndCheck() {
+      if (maxProcessLimitNodes > 0 && ++currentCount > maxProcessLimitNodes) {
+        throw OverLimitException.INSTANCE;
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: Here I refer to the usage of [RexUtil#CnfHelper](https://github.com/apache/calcite/blob/b394abca407eea06ee8f8b50f30bd443f286bb42/core/src/main/java/org/apache/calcite/rex/RexUtil.java#L2644). OverLimitException extends ControlFlowException, which overrides the fillInStackTrace method and no longer fills the stack trace.
created_at: 2025-04-03T01:42:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxProcessLimitNodes) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxProcessLimitNodes = maxProcessLimitNodes;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      incrementAndCheck();
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          mergeOr(additionalConditions, operandResult);
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (inputBitTableRef == null || !inputBitTableRef.equals(tableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * For additional predicates of each operand in conjuction, all of them should be retained and
+     * use 'AND' to combine expressions.
+     *
+     * @param baseMap       Additional predicates that current conjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeAnd(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      for (Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry : forMergeMap.entrySet()) {
+        RexNode mergeExpression =
+            relBuilder.and(
+                entry.getValue(),
+                baseMap.getOrDefault(entry.getKey(), relBuilder.literal(true)));
+        baseMap.put(entry.getKey(), mergeExpression);
+      }
+    }
+
+    /**
+     * Only if all operands in disjunction have additional predicates for table t1, we use 'OR'
+     * to combine expressions and retain them as additional predicates of table t1.
+     *
+     * @param baseMap       Additional predicates that current disjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeOr(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      if (baseMap.isEmpty()) {
+        return;
+      }
+
+      Iterator<Map.Entry<RexTableInputRef.RelTableRef, RexNode>> iterator =
+          baseMap.entrySet().iterator();
+      while (iterator.hasNext()) {
+        Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry = iterator.next();
+        if (!forMergeMap.containsKey(entry.getKey())) {
+          iterator.remove();
+          continue;
+        }
+        RexNode mergedRex =
+            relBuilder.or(
+                entry.getValue(),
+                forMergeMap.get(entry.getKey()));
+        baseMap.put(entry.getKey(), mergedRex);
+      }
+    }
+
+    private void incrementAndCheck() {
+      if (maxProcessLimitNodes > 0 && ++currentCount > maxProcessLimitNodes) {
+        throw OverLimitException.INSTANCE;
+      }
+    }
+
+    /** Exception to catch when we pass the limit. */
+    private static class OverLimitException extends ControlFlowException {
+      protected static final OverLimitException INSTANCE = new OverLimitException();
+
+      private OverLimitException() {}
+    }
+  }
+
+  /** Rule configuration. */
+  @Value.Immutable(singleton = false)
+  public interface Config extends RelRule.Config {
+    Config FILTER = ImmutableExpandDisjuctionRule.Config.builder()
+        .withMatchHandler(ExpandDisjuctionRule::matchFilter)
+        .build()
+        .withOperandSupplier(b ->
+            b.operand(Filter.class).anyInputs());
+
+    Config JOIN = ImmutableExpandDisjuctionRule.Config.builder()
+        .withMatchHandler(ExpandDisjuctionRule::matchJoin)
+        .build()
+        .withOperandSupplier(b ->
+            b.operand(Join.class).anyInputs());
+
+    @Override default ExpandDisjuctionRule toRule() {
+      return new ExpandDisjuctionRule(this);
+    }
+
+    default int processLimit() {
+      return 10000;
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: Yes. This value currently means the upper limit of the recursive call to `expandDeep`. It will be changed to the upper limit of the number of new expressions as mentioned by Mihaibudiu.
created_at: 2025-04-03T02:37:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxProcessLimitNodes) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxProcessLimitNodes = maxProcessLimitNodes;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      incrementAndCheck();
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          mergeOr(additionalConditions, operandResult);
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (inputBitTableRef == null || !inputBitTableRef.equals(tableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * For additional predicates of each operand in conjuction, all of them should be retained and
+     * use 'AND' to combine expressions.
+     *
+     * @param baseMap       Additional predicates that current conjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeAnd(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      for (Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry : forMergeMap.entrySet()) {
+        RexNode mergeExpression =
+            relBuilder.and(
+                entry.getValue(),
+                baseMap.getOrDefault(entry.getKey(), relBuilder.literal(true)));
+        baseMap.put(entry.getKey(), mergeExpression);
+      }
+    }
+
+    /**
+     * Only if all operands in disjunction have additional predicates for table t1, we use 'OR'
+     * to combine expressions and retain them as additional predicates of table t1.
+     *
+     * @param baseMap       Additional predicates that current disjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeOr(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      if (baseMap.isEmpty()) {
+        return;
+      }
+
+      Iterator<Map.Entry<RexTableInputRef.RelTableRef, RexNode>> iterator =
+          baseMap.entrySet().iterator();
+      while (iterator.hasNext()) {
+        Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry = iterator.next();
+        if (!forMergeMap.containsKey(entry.getKey())) {
+          iterator.remove();
+          continue;
+        }
+        RexNode mergedRex =
+            relBuilder.or(
+                entry.getValue(),
+                forMergeMap.get(entry.getKey()));
+        baseMap.put(entry.getKey(), mergedRex);
+      }
+    }
+
+    private void incrementAndCheck() {
+      if (maxProcessLimitNodes > 0 && ++currentCount > maxProcessLimitNodes) {
+        throw OverLimitException.INSTANCE;
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: asolimando
body: I missed that, thank for pointing that out, it's fine then
created_at: 2025-04-08T09:25:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,379 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjuction in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * t1.id > 20 or t1.weight < 200
+ * t2.height < 50 or t2.sales > 100
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>This rule should only be applied once to avoid generate same redundant expression and
+ * it should be used before {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUCTION
+ * @see CoreRules#EXPAND_JOIN_DISJUCTION
+ */
+@Value.Enclosing
+public class ExpandDisjuctionRule
+    extends RelRule<ExpandDisjuctionRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjuctionRule.
+   */
+  protected ExpandDisjuctionRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and  find out which table the RexInputRef
+   *                    comes from via {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    HashMap<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column come from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjuctionHelper expandHelper =
+        new ExpandDisjuctionHelper(inputRefToTableRef, relBuilder, config.processLimit());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjuctionRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjuctionHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxProcessLimitNodes;
+
+    private int currentCount;
+
+    private ExpandDisjuctionHelper(
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjuctionRule.java
user: login: silundong
body: I tried to extends `RexVisitorImpl`. The logic of `expandDeep` can be transplanted to `visitCall`, but other methods (visitInputRef, visitLiteral...) may also need to be overridden, such as expressions like` id > 30 or booleanInputRef`. However, `expandDeep` does not need to consider these, which feels more convenient.
created_at: 2025-04-10T08:29:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -853,13 +853,13 @@ private CoreRules() {}
 
   /** Rule that expand disjuction in condition of a {@link Filter}.
    *
-   * @see #EXPAND_JOIN_DISJUCTION */
-  public static final ExpandDisjuctionRule EXPAND_FILTER_DISJUCTION =
-      ExpandDisjuctionRule.Config.FILTER.toRule();
+   * @see #EXPAND_JOIN_DISJUNCTION_FOR_TABLE */
path: core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
user: login: mihaibudiu
body: I see what FOR_TABLE means, but I am not sure it's the best name.
How about EXPAND_JOIN_DISJUNCTION_GLOBAL?

created_at: 2025-04-10T18:29:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_FOR_TABLE
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_FOR_TABLE
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: The first phrase is good, but the second does not really have anything to do with the description of this parameter
created_at: 2025-04-10T18:30:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_FOR_TABLE
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_FOR_TABLE
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and find out which table the RexInputRef
+   *                    comes from {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: You return a single node, so it must be a single predicate, which perhaps has a specific form.
created_at: 2025-04-10T18:31:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_FOR_TABLE
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_FOR_TABLE
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and find out which table the RexInputRef
+   *                    comes from {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column comes from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjunctionForTableHelper expandHelper =
+        new ExpandDisjunctionForTableHelper(inputRefToTableRef, relBuilder, config.bloat());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjunctionForTableHelper {
+
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: no need for so many empty lines here
created_at: 2025-04-10T18:32:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_FOR_TABLE
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_FOR_TABLE
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and find out which table the RexInputRef
+   *                    comes from {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column comes from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjunctionForTableHelper expandHelper =
+        new ExpandDisjunctionForTableHelper(inputRefToTableRef, relBuilder, config.bloat());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjunctionForTableHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxNodeCount;
+
+    // Used to record the number of redundant expressions expanded.
+    private int currentCount;
+
+    private ExpandDisjunctionForTableHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxNodeCount) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxNodeCount = maxNodeCount;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: a map from a table to a (combined) predicate that can be pushed down and only depends on columns of the table
created_at: 2025-04-10T18:33:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_FOR_TABLE
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_FOR_TABLE
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and find out which table the RexInputRef
+   *                    comes from {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column comes from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjunctionForTableHelper expandHelper =
+        new ExpandDisjunctionForTableHelper(inputRefToTableRef, relBuilder, config.bloat());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjunctionForTableHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxNodeCount;
+
+    // Used to record the number of redundant expressions expanded.
+    private int currentCount;
+
+    private ExpandDisjunctionForTableHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxNodeCount) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxNodeCount = maxNodeCount;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongToOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: this comment should be after the if
created_at: 2025-04-10T18:34:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_FOR_TABLE
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_FOR_TABLE
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and find out which table the RexInputRef
+   *                    comes from {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column comes from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjunctionForTableHelper expandHelper =
+        new ExpandDisjunctionForTableHelper(inputRefToTableRef, relBuilder, config.bloat());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjunctionForTableHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxNodeCount;
+
+    // Used to record the number of redundant expressions expanded.
+    private int currentCount;
+
+    private ExpandDisjunctionForTableHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxNodeCount) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxNodeCount = maxNodeCount;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongToOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        checkExpandCount(condition.nodeCount());
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          mergeOr(additionalConditions, operandResult);
+
+          if (additionalConditions.isEmpty()) {
+            break;
+          }
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongToOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (!tableRef.equals(inputBitTableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * For additional predicates of each operand in conjunction, all of them should be retained and
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: combine predicates that depend on the same table using conjunctions
specify that the result is returned by modifying baseMap
(you could also rename the function to combinePredicatesUsingAnd)
created_at: 2025-04-10T18:36:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_FOR_TABLE
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_FOR_TABLE
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs. Build the referenced columns
+   *                    in the predicate as RexInputRef and find out which table the RexInputRef
+   *                    comes from {@link RelMetadataQuery#getExpressionLineage}
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return Expanded predicates
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column comes from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjunctionForTableHelper expandHelper =
+        new ExpandDisjunctionForTableHelper(inputRefToTableRef, relBuilder, config.bloat());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjunctionForTableHelper {
+
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxNodeCount;
+
+    // Used to record the number of redundant expressions expanded.
+    private int currentCount;
+
+    private ExpandDisjunctionForTableHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxNodeCount) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxNodeCount = maxNodeCount;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  Additional predicates that can be pushed down for each table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongToOneTable(inputRefs);
+      // The condition already belongs to one table, return it directly
+      if (tableRef != null) {
+        checkExpandCount(condition.nodeCount());
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          mergeAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          mergeOr(additionalConditions, operandResult);
+
+          if (additionalConditions.isEmpty()) {
+            break;
+          }
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongToOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (!tableRef.equals(inputBitTableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * For additional predicates of each operand in conjunction, all of them should be retained and
+     * use 'AND' to combine expressions. For example:
+     *
+     * <p>baseMap: {t1: p1, t2: p2}
+     *
+     * <p>forMergeMap: {t1: p11, t3: p3}
+     *
+     * <p>result: {t1: p1 AND p11, t2: p2, t3: p3}
+     *
+     * @param baseMap       Additional predicates that current conjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void mergeAnd(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      for (Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry : forMergeMap.entrySet()) {
+        RexNode mergedRex =
+            relBuilder.and(
+                entry.getValue(),
+                baseMap.getOrDefault(entry.getKey(), relBuilder.literal(true)));
+        int oriCount = entry.getValue().nodeCount()
+            + (baseMap.containsKey(entry.getKey())
+                ? baseMap.get(entry.getKey()).nodeCount()
+                : 0);
+        checkExpandCount(mergedRex.nodeCount() - oriCount);
+        baseMap.put(entry.getKey(), mergedRex);
+      }
+    }
+
+    /**
+     * Only if all operands in disjunction have additional predicates for table t1, we use 'OR'
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: see my comments for javadoc above
created_at: 2025-04-10T18:37:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -850,4 +850,16 @@ private CoreRules() {}
   @Experimental
   public static final DphypJoinReorderRule HYPER_GRAPH_OPTIMIZE =
       DphypJoinReorderRule.Config.DEFAULT.toRule();
+
+  /** Rule that expand disjuction in condition of a {@link Filter}.
path: core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
user: login: mihaibudiu
body: this "disjunction" is still misspelled
it should also be "expands disjunction in the condition of a filter"
same problems in the comment below
created_at: 2025-04-15T18:14:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_GLOBAL
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_GLOBAL
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs.
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return The redundant predicate expanded from the condition parameter. It is formed by
+   * concatenating the predicates that can be pushed down to single table with AND.
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // Trace which table does the column referenced in the condition come from
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: I think this is easier to read if written as 
"for each table discover the conditions that refer to columns of that table (and refer to no other tables)"
created_at: 2025-04-15T18:36:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_GLOBAL
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_GLOBAL
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs.
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return The redundant predicate expanded from the condition parameter. It is formed by
+   * concatenating the predicates that can be pushed down to single table with AND.
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column comes from multiple tables, skip it
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: What does it mean for a column to "come" from multiple tables?
You mean, it is computed by an expression that depends on multiple tables?
created_at: 2025-04-15T18:38:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_GLOBAL
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_GLOBAL
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs.
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return The redundant predicate expanded from the condition parameter. It is formed by
+   * concatenating the predicates that can be pushed down to single table with AND.
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column comes from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjunctionForTableHelper expandHelper =
+        new ExpandDisjunctionForTableHelper(inputRefToTableRef, relBuilder, config.bloat());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjunctionForTableHelper {
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxNodeCount;
+
+    // Used to record the number of redundant expressions expanded.
+    private int currentCount;
+
+    private ExpandDisjunctionForTableHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxNodeCount) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxNodeCount = maxNodeCount;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  A map from a table to a (combined) predicate that can be pushed down
+     * and only depends on columns of the table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongToOneTable(inputRefs);
+      if (tableRef != null) {
+        // The condition already belongs to one table, return it directly
+        checkExpandCount(condition.nodeCount());
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          combinePredicatesUsingAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          combinePredicatesUsingOr(additionalConditions, operandResult);
+
+          if (additionalConditions.isEmpty()) {
+            break;
+          }
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongToOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (!tableRef.equals(inputBitTableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * Combine predicates that depend on the same table using conjunctions, and
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: Combine predicates that depend on the same table using conjunctions.
The result is returned by modifying baseMap.
created_at: 2025-04-15T18:40:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_GLOBAL
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_GLOBAL
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs.
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return The redundant predicate expanded from the condition parameter. It is formed by
+   * concatenating the predicates that can be pushed down to single table with AND.
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column comes from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjunctionForTableHelper expandHelper =
+        new ExpandDisjunctionForTableHelper(inputRefToTableRef, relBuilder, config.bloat());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjunctionForTableHelper {
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxNodeCount;
+
+    // Used to record the number of redundant expressions expanded.
+    private int currentCount;
+
+    private ExpandDisjunctionForTableHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxNodeCount) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxNodeCount = maxNodeCount;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  A map from a table to a (combined) predicate that can be pushed down
+     * and only depends on columns of the table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongToOneTable(inputRefs);
+      if (tableRef != null) {
+        // The condition already belongs to one table, return it directly
+        checkExpandCount(condition.nodeCount());
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          combinePredicatesUsingAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          combinePredicatesUsingOr(additionalConditions, operandResult);
+
+          if (additionalConditions.isEmpty()) {
+            break;
+          }
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongToOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (!tableRef.equals(inputBitTableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * Combine predicates that depend on the same table using conjunctions, and
+     * specify that the result is returned by modifying baseMap. For example:
+     *
+     * <p>baseMap: {t1: p1, t2: p2}
+     *
+     * <p>forMergeMap: {t1: p11, t3: p3}
+     *
+     * <p>result: {t1: p1 AND p11, t2: p2, t3: p3}
+     *
+     * @param baseMap       Additional predicates that current conjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void combinePredicatesUsingAnd(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      for (Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry : forMergeMap.entrySet()) {
+        RexNode mergedRex =
+            relBuilder.and(
+                entry.getValue(),
+                baseMap.getOrDefault(entry.getKey(), relBuilder.literal(true)));
+        int oriCount = entry.getValue().nodeCount()
+            + (baseMap.containsKey(entry.getKey())
+                ? baseMap.get(entry.getKey()).nodeCount()
+                : 0);
+        checkExpandCount(mergedRex.nodeCount() - oriCount);
+        baseMap.put(entry.getKey(), mergedRex);
+      }
+    }
+
+    /**
+     * Combine predicates that depend on the same table using disjunctions, and
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: same change as above.
created_at: 2025-04-15T18:41:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_GLOBAL
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_GLOBAL
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs.
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return The redundant predicate expanded from the condition parameter. It is formed by
+   * concatenating the predicates that can be pushed down to single table with AND.
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // Trace which table does the column referenced in the condition come from
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column comes from multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
+        continue;
+      }
+      inputRefToTableRef.put(columnBit, relTableRefs.iterator().next());
+    }
+
+    ExpandDisjunctionForTableHelper expandHelper =
+        new ExpandDisjunctionForTableHelper(inputRefToTableRef, relBuilder, config.bloat());
+    Map<RexTableInputRef.RelTableRef, RexNode> expandResult = expandHelper.expand(condition);
+
+    RexNode newCondition = condition;
+    for (RexNode expandCondition : expandResult.values()) {
+      newCondition = relBuilder.and(newCondition, expandCondition);
+    }
+    return newCondition;
+  }
+
+  private static void matchFilter(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Filter filter = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    RexNode newCondition =
+        rule.apply(
+            filter.getCondition(),
+            filter,
+            filter.getRowType().getFieldList(),
+            relBuilder,
+            mq);
+    if (newCondition.equals(filter.getCondition())) {
+      return;
+    }
+    Filter newFilter = filter.copy(filter.getTraitSet(), filter.getInput(), newCondition);
+    call.transformTo(newFilter);
+  }
+
+  private static void matchJoin(ExpandDisjunctionForTableRule rule, RelOptRuleCall call) {
+    Join join = call.rel(0);
+    RelMetadataQuery mq = call.getMetadataQuery();
+    RelBuilder relBuilder = call.builder();
+
+    List<RelDataTypeField> fieldList =
+        Lists.newArrayList(join.getLeft().getRowType().getFieldList());
+    fieldList.addAll(join.getRight().getRowType().getFieldList());
+    RexNode newCondition =
+        rule.apply(
+            join.getCondition(),
+            join,
+            fieldList,
+            relBuilder,
+            mq);
+    if (newCondition.equals(join.getCondition())) {
+      return;
+    }
+    Join newJoin =
+        join.copy(
+            join.getTraitSet(),
+            newCondition,
+            join.getLeft(),
+            join.getRight(),
+            join.getJoinType(),
+            join.isSemiJoinDone());
+    call.transformTo(newJoin);
+  }
+
+  /**
+   * Helper class to expand predicates.
+   */
+  private static class ExpandDisjunctionForTableHelper {
+    private final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef;
+
+    private final RelBuilder relBuilder;
+
+    private final int maxNodeCount;
+
+    // Used to record the number of redundant expressions expanded.
+    private int currentCount;
+
+    private ExpandDisjunctionForTableHelper(
+        Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef,
+        RelBuilder relBuilder,
+        int maxNodeCount) {
+      this.inputRefToTableRef = inputRefToTableRef;
+      this.relBuilder = relBuilder;
+      this.maxNodeCount = maxNodeCount;
+    }
+
+    private Map<RexTableInputRef.RelTableRef, RexNode> expand(RexNode condition) {
+      try {
+        this.currentCount = 0;
+        return expandDeep(condition);
+      } catch (OverLimitException e) {
+        return new HashMap<>();
+      }
+    }
+
+    /**
+     * Expand predicates recursively that can be pushed down to single table.
+     *
+     * @param condition   Predicate to be expanded
+     * @return  A map from a table to a (combined) predicate that can be pushed down
+     * and only depends on columns of the table
+     */
+    private Map<RexTableInputRef.RelTableRef, RexNode> expandDeep(RexNode condition) {
+      Map<RexTableInputRef.RelTableRef, RexNode> additionalConditions = new HashMap<>();
+
+      ImmutableBitSet inputRefs = RelOptUtil.InputFinder.bits(condition);
+      if (inputRefs.isEmpty()) {
+        return additionalConditions;
+      }
+
+      RexTableInputRef.RelTableRef tableRef = inputRefsBelongToOneTable(inputRefs);
+      if (tableRef != null) {
+        // The condition already belongs to one table, return it directly
+        checkExpandCount(condition.nodeCount());
+        additionalConditions.put(tableRef, condition);
+        return additionalConditions;
+      }
+
+      // Recursively expand the expression according to whether it is a conjunction
+      // or a disjunction. If it is neither a disjunction nor a conjunction, it cannot
+      // be expanded further and an empty Map is returned.
+      switch (condition.getKind()) {
+      case AND:
+        List<RexNode> andOperands = RexUtil.flattenAnd(((RexCall) condition).getOperands());
+        for (RexNode andOperand : andOperands) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult = expandDeep(andOperand);
+          combinePredicatesUsingAnd(additionalConditions, operandResult);
+        }
+        break;
+      case OR:
+        List<RexNode> orOperands = RexUtil.flattenOr(((RexCall) condition).getOperands());
+        additionalConditions.putAll(expandDeep(orOperands.get(0)));
+        for (int i = 1; i < orOperands.size(); i++) {
+          Map<RexTableInputRef.RelTableRef, RexNode> operandResult =
+              expandDeep(orOperands.get(i));
+          combinePredicatesUsingOr(additionalConditions, operandResult);
+
+          if (additionalConditions.isEmpty()) {
+            break;
+          }
+        }
+        break;
+      default:
+        break;
+      }
+
+      return additionalConditions;
+    }
+
+    private RexTableInputRef.@Nullable RelTableRef inputRefsBelongToOneTable(
+        ImmutableBitSet inputRefs) {
+      RexTableInputRef.RelTableRef tableRef = inputRefToTableRef.get(inputRefs.nth(0));
+      if (tableRef == null) {
+        return null;
+      }
+      for (int inputBit : inputRefs.asList()) {
+        RexTableInputRef.RelTableRef inputBitTableRef = inputRefToTableRef.get(inputBit);
+        if (!tableRef.equals(inputBitTableRef)) {
+          return null;
+        }
+      }
+      return tableRef;
+    }
+
+    /**
+     * Combine predicates that depend on the same table using conjunctions, and
+     * specify that the result is returned by modifying baseMap. For example:
+     *
+     * <p>baseMap: {t1: p1, t2: p2}
+     *
+     * <p>forMergeMap: {t1: p11, t3: p3}
+     *
+     * <p>result: {t1: p1 AND p11, t2: p2, t3: p3}
+     *
+     * @param baseMap       Additional predicates that current conjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void combinePredicatesUsingAnd(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      for (Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry : forMergeMap.entrySet()) {
+        RexNode mergedRex =
+            relBuilder.and(
+                entry.getValue(),
+                baseMap.getOrDefault(entry.getKey(), relBuilder.literal(true)));
+        int oriCount = entry.getValue().nodeCount()
+            + (baseMap.containsKey(entry.getKey())
+                ? baseMap.get(entry.getKey()).nodeCount()
+                : 0);
+        checkExpandCount(mergedRex.nodeCount() - oriCount);
+        baseMap.put(entry.getKey(), mergedRex);
+      }
+    }
+
+    /**
+     * Combine predicates that depend on the same table using disjunctions, and
+     * specify that the result is returned by modifying baseMap. For example:
+     *
+     * <p>baseMap: {t1: p1, t2: p2}
+     *
+     * <p>forMergeMap: {t1: p11, t3: p3}
+     *
+     * <p>result: {t1: p1 OR p11}
+     *
+     * @param baseMap       Additional predicates that current disjunction has already saved
+     * @param forMergeMap   Additional predicates that current operand has expanded
+     */
+    private void combinePredicatesUsingOr(
+        Map<RexTableInputRef.RelTableRef, RexNode> baseMap,
+        Map<RexTableInputRef.RelTableRef, RexNode> forMergeMap) {
+      if (baseMap.isEmpty()) {
+        return;
+      }
+
+      Iterator<Map.Entry<RexTableInputRef.RelTableRef, RexNode>> iterator =
+          baseMap.entrySet().iterator();
+      while (iterator.hasNext()) {
+        int forMergeNodeCount = 0;
+
+        Map.Entry<RexTableInputRef.RelTableRef, RexNode> entry = iterator.next();
+        if (!forMergeMap.containsKey(entry.getKey())) {
+          checkExpandCount(-entry.getValue().nodeCount());
+          iterator.remove();
+          continue;
+        } else {
+          forMergeNodeCount = forMergeMap.get(entry.getKey()).nodeCount();
+        }
+        RexNode mergedRex =
+            relBuilder.or(
+                entry.getValue(),
+                forMergeMap.get(entry.getKey()));
+        int oriCount = entry.getValue().nodeCount() + forMergeNodeCount;
+        checkExpandCount(mergedRex.nodeCount() - oriCount);
+        baseMap.put(entry.getKey(), mergedRex);
+      }
+    }
+
+    /**
+     * Check whether the number of redundant expressions generated in the expansion
+     * exceeds the limit.
+     */
+    private void checkExpandCount(int changeCount) {
+      currentCount += changeCount;
+      if (maxNodeCount > 0 && currentCount > maxNodeCount) {
+        throw OverLimitException.INSTANCE;
+      }
+    }
+
+    /** Exception to catch when we pass the limit. */
+    private static class OverLimitException extends ControlFlowException {
+      protected static final OverLimitException INSTANCE = new OverLimitException();
+
+      private OverLimitException() {}
+    }
+  }
+
+  /** Rule configuration. */
+  @Value.Immutable(singleton = false)
+  public interface Config extends RelRule.Config {
+    Config FILTER = ImmutableExpandDisjunctionForTableRule.Config.builder()
+        .withMatchHandler(ExpandDisjunctionForTableRule::matchFilter)
+        .build()
+        .withOperandSupplier(b -> b.operand(Filter.class).anyInputs());
+
+    Config JOIN = ImmutableExpandDisjunctionForTableRule.Config.builder()
+        .withMatchHandler(ExpandDisjunctionForTableRule::matchJoin)
+        .build()
+        .withOperandSupplier(b -> b.operand(Join.class).anyInputs());
+
+    @Override default ExpandDisjunctionForTableRule toRule() {
+      return new ExpandDisjunctionForTableRule(this);
+    }
+
+    /**
+     * Limit how much complexity can increase during expanding.
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: Limit to the size growth of the condition allowed during expansion.
created_at: 2025-04-15T18:43:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_GLOBAL
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_GLOBAL
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs.
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return The redundant predicate expanded from the condition parameter. It is formed by
+   * concatenating the predicates that can be pushed down to single table with AND.
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // For each table discover the conditions that refer to columns of that
+    // table (and refer to no other tables)
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column is computed by an expression that depends on multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: xuzifu666
body: Here maybe just use  **(refTableRefs != null)**  is ok?
created_at: 2025-04-16T06:57:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_GLOBAL
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_GLOBAL
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs.
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return The redundant predicate expanded from the condition parameter. It is formed by
+   * concatenating the predicates that can be pushed down to single table with AND.
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // For each table discover the conditions that refer to columns of that
+    // table (and refer to no other tables)
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column is computed by an expression that depends on multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: mihaibudiu
body: if relTableRefs could be null this would have failed in CI due to error check
created_at: 2025-04-16T07:00:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_GLOBAL
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_GLOBAL
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs.
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return The redundant predicate expanded from the condition parameter. It is formed by
+   * concatenating the predicates that can be pushed down to single table with AND.
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // For each table discover the conditions that refer to columns of that
+    // table (and refer to no other tables)
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column is computed by an expression that depends on multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: silundong
body: > Here maybe just use **(refTableRefs != null)** is ok?

`RexUtil.gatherTableReferences` will not return null.

created_at: 2025-04-16T07:09:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_GLOBAL
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_GLOBAL
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs.
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return The redundant predicate expanded from the condition parameter. It is formed by
+   * concatenating the predicates that can be pushed down to single table with AND.
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // For each table discover the conditions that refer to columns of that
+    // table (and refer to no other tables)
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column is computed by an expression that depends on multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: xuzifu666
body: Yes, my mistake, (relTableRefs.size != 1) equivalent to (relTableRefs.isEmpty() || relTableRefs.size() > 1)? or keep it as current~
created_at: 2025-04-16T07:11:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,430 @@
+/*
+ * 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.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexTableInputRef;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ControlFlowException;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.Lists;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.immutables.value.Value;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rule to expand disjunction for single table in condition of a {@link Filter} or {@link Join},
+ * It makes sense to make this optimization part of the predicate pushdown. For example:
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * }</pre></blockquote>
+ *
+ * <p>we can expand to obtain the condition
+ *
+ * <blockquote><pre>{@code
+ * select t1.name from t1, t2
+ * where t1.id = t2.id
+ * and (
+ *  (t1.id > 20 and t2.height < 50)
+ *  or
+ *  (t1.weight < 200 and t2.sales > 100)
+ * )
+ * and (t1.id > 20 or t1.weight < 200)
+ * and (t2.height < 50 or t2.sales > 100)
+ * }</pre></blockquote>
+ *
+ * <p>new generated predicates are redundant, but they could be pushed down to
+ * scan operator of t1/t2 and reduce the cardinality.
+ *
+ * <p>Note: This rule should only be applied once and it should be used before
+ * {@link CoreRules#FILTER_INTO_JOIN} and {@link CoreRules#JOIN_CONDITION_PUSH}.
+ * In order to avoid it interacting with other expression-related rules and causing
+ * an infinite loop, it is recommended to use it alone in HepPlanner first,
+ * and then do predicate pushdown.
+ *
+ * @see CoreRules#EXPAND_FILTER_DISJUNCTION_GLOBAL
+ * @see CoreRules#EXPAND_JOIN_DISJUNCTION_GLOBAL
+ */
+@Value.Enclosing
+public class ExpandDisjunctionForTableRule
+    extends RelRule<ExpandDisjunctionForTableRule.Config>
+    implements TransformationRule {
+
+  /**
+   * Creates a ExpandDisjunctionForTableRule.
+   */
+  protected ExpandDisjunctionForTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
+  }
+
+  /**
+   * Expand predicates for condition of a {@link Filter} or {@link Join}.
+   *
+   * @param condition   Condition to be expanded in Filter or Join
+   * @param relNode     The Filter or Join node
+   * @param fieldList   The field list of the Filter or Join inputs.
+   * @param relBuilder  Builder
+   * @param mq          RelMetadataQuery, which is used to get the expression lineage
+   *
+   * @return The redundant predicate expanded from the condition parameter. It is formed by
+   * concatenating the predicates that can be pushed down to single table with AND.
+   */
+  private RexNode apply(
+      RexNode condition,
+      RelNode relNode,
+      List<RelDataTypeField> fieldList,
+      RelBuilder relBuilder,
+      RelMetadataQuery mq) {
+    final Map<Integer, RexTableInputRef.RelTableRef> inputRefToTableRef = new HashMap<>();
+
+    ImmutableBitSet columnBits = RelOptUtil.InputFinder.bits(condition);
+    if (columnBits.isEmpty()) {
+      return condition;
+    }
+
+    // For each table discover the conditions that refer to columns of that
+    // table (and refer to no other tables)
+    for (int columnBit : columnBits.asList()) {
+      Set<RexNode> exprLineage =
+          mq.getExpressionLineage(
+              relNode,
+              RexInputRef.of(columnBit, fieldList));
+      // If mq.getExpressionLineage cannot get result, skip it
+      if (exprLineage == null) {
+        continue;
+      }
+      Set<RexTableInputRef.RelTableRef> relTableRefs =
+          RexUtil.gatherTableReferences(Lists.newArrayList(exprLineage));
+      // If the column is computed by an expression that depends on multiple tables, skip it
+      if (relTableRefs.isEmpty() || relTableRefs.size() > 1) {
path: core/src/main/java/org/apache/calcite/rel/rules/ExpandDisjunctionForTableRule.java
user: login: silundong
body: Yes, `(relTableRefs.size != 1)` is more concise, I will update it.
created_at: 2025-04-16T07:19:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: mihaibudiu
created_at: 2025-03-26T20:18:03Z
body: You can test your changes locally using `./gradlew build`
author_association: CONTRIBUTOR
user: login: silundong
created_at: 2025-03-27T01:24:29Z
body: > You can test your changes locally using `./gradlew build`

Thank you for your suggestion, the format of javadoc was wrong before. Could you please take a look at this PR?
author_association: CONTRIBUTOR
user: login: asolimando
created_at: 2025-03-31T08:44:44Z
body: @silundong unless strictly necessary (CI changes from `main` that can't be picked up otherwise, etc.) please avoid force-pushing while the review is in progress as it makes it difficult for reviewers to do incremental diff via GitHub UI. You can push as many commits as needed, you will be required to squash them before merging.
author_association: MEMBER
user: login: silundong
created_at: 2025-03-31T09:16:06Z
body: > @silundong unless strictly necessary (CI changes from `main` that can't be picked up otherwise, etc.) please avoid force-pushing while the review is in progress as it makes it difficult for reviewers to do incremental diff via GitHub UI. You can push as many commits as needed, you will be required to squash them before merging.

@asolimando Sorry for making the review difficult. Should I revert the commit to the state of the last review and update the changes to the new commit?
author_association: CONTRIBUTOR
user: login: asolimando
created_at: 2025-03-31T09:24:03Z
body: > > @silundong unless strictly necessary (CI changes from `main` that can't be picked up otherwise, etc.) please avoid force-pushing while the review is in progress as it makes it difficult for reviewers to do incremental diff via GitHub UI. You can push as many commits as needed, you will be required to squash them before merging.
> 
> @asolimando Sorry for making the review difficult. Should I revert the commit to the state of the last review and update the changes to the new commit?

No worries, it's not a big deal, you can keep the existing as-is and just add more commits in the future, thanks for your understanding!
author_association: MEMBER
user: login: silundong
created_at: 2025-03-31T09:30:39Z
body: I have made some changes, could you please review it again? @asolimando @mihaibudiu 
author_association: CONTRIBUTOR
user: login: mihaibudiu
created_at: 2025-04-01T03:48:29Z
body: Please request another review when this is ready
author_association: CONTRIBUTOR
user: login: silundong
created_at: 2025-04-01T05:56:39Z
body: > @silundong I don't see anything trying to prevent the rule to be applied indefinitely. Now that we have the maximum expansion it might stop at some point but this won't prevent the rule to be tried. I have seen lots of similar problems leading to OOMs in the past, I think it's dangerous to integrate such a rule as-is, and most definitely I wouldn't put it into the standard core rules which get applied by default.

@asolimando I have checked in `matchFilter` and `matchJoin` whether the expanded newCondition is equal to the original condition. If they are the same, it will be returned directly. 
For example the rule is matched for the first time and the condition is successfully expanded, a new Filter/Join is generated. When the rule is matched for the second time, the conditions that have been successfully expanded will not change, so it will be returned directly. I don't think it will be applied infinitely. Is there something else I haven't considered?
author_association: CONTRIBUTOR
user: login: mihaibudiu
created_at: 2025-04-01T06:32:19Z
body: The concern is not infinite loops, but large increases.
For example, converting a boolean formula to CNF form can produce expressions that are exponential in the size of the original expression. Even a quadratic increase can be very large: if the original formula has 1000 terms, the result can have 1 million.
I have seen discussions about very large formulas that can arise in SQL programs.
author_association: CONTRIBUTOR
user: login: silundong
created_at: 2025-04-01T06:50:10Z
body: > The concern is not infinite loops, but large increases. For example, converting a boolean formula to CNF form can produce expressions that are exponential in the size of the original expression. Even a quadratic increase can be very large: if the original formula has 1000 terms, the result can have 1 million. I have seen discussions about very large formulas that can arise in SQL programs.

Thanks for your explanation. So it should count and check the upper bound every time it gets a new redundant predicate, right?

author_association: CONTRIBUTOR
user: login: mihaibudiu
created_at: 2025-04-01T06:52:56Z
body: If you can compute the size of the final formula based on the structure of the original formulas, then you can do that upfront and avoid expansion altogether. That's why I asked about the complexity. An alternative is to do the expansion work and if the result becomes too big abort and undo the transformation. @asolimando pointed to a BLOAT parameter which can control the maximum size.

author_association: CONTRIBUTOR
user: login: silundong
created_at: 2025-04-01T07:03:32Z
body: > If you can compute the size of the final formula based on the structure of the original formulas, then you can do that upfront and avoid expansion altogether. That's why I asked about the complexity. An alternative is to do the expansion work and if the result becomes too big abort and undo the transformation. @asolimando pointed to a BLOAT parameter which can control the maximum size.

Thank you, I will make changes in the next commit and try to add the rule we mentioned.
author_association: CONTRIBUTOR
user: login: asolimando
created_at: 2025-04-01T12:49:12Z
body: > > @silundong I don't see anything trying to prevent the rule to be applied indefinitely. Now that we have the maximum expansion it might stop at some point but this won't prevent the rule to be tried. I have seen lots of similar problems leading to OOMs in the past, I think it's dangerous to integrate such a rule as-is, and most definitely I wouldn't put it into the standard core rules which get applied by default.
> 
> @asolimando I have checked in `matchFilter` and `matchJoin` whether the expanded newCondition is equal to the original condition. If they are the same, it will be returned directly. For example the rule is matched for the first time and the condition is successfully expanded, a new Filter/Join is generated. When the rule is matched for the second time, the conditions that have been successfully expanded will not change, so it will be returned directly. I don't think it will be applied infinitely. Is there something else I haven't considered?

I have in mind the rules like `JoinPushTransitivePredicatesRule` which, in combination with the present rules (and possibly others like `FilterMergeRule` but not only) can keep "inferring" predicates and apply them indefinitely.

The problematic pattern could be as follows:
1) the present rule produces new predicates that can be pushed down
2) `JoinPushTransitivePredicatesRule` infers new predicates/filters that are then transposed and merged with existing ones
3) the present rule will match again as the filter is not the same as before (due to the merging), so the planner will apply the rule again, and we can end up in a cycle

I have looked up similar issues we had in the past around this subject:
Calcite:
- CALCITE-2200
- CALCITE-3505
- CALCITE-6432
- CALCITE-6363

Hive:
- [HIVE-25275](https://issues.apache.org/jira/browse/HIVE-25275)
- [HIVE-25758](https://issues.apache.org/jira/browse/HIVE-25758) (covered in a past Calcite meetup, [video](https://youtu.be/_phzRNCWJfw?feature=shared&t=1745), [slides](https://www.slideshare.net/StamatisZampetakis/debugging-planning-issues-using-calcites-builtin-loggers), sorry for the self-citation but it goes in-depth on that example so you can easily get an idea)

The Hive ones (which uses Calcite) I have had direct experience with, were happening often due to the presence of disjunctive filters and limitations in `RexSimplify` which at some point was unable to simplify expressions futher.

Rules like `JoinPushTransitivePredicatesRule` rely on `RexSimplify` to simplify expressions to reach a fixpoint, in cases where you can infer and merge filters, and the resulting expressions can't be simplified to its original (equivalent) version, it keeps getting recognized as a new RelNode where the same rule has never been applied, so the planner will explore it with all matching rules.

I am convinced your proposed optimization is interesting and useful, but I'd rather not have it added to core right away and leave it as an optional rule for the reason highlighted above, unless we have some guarantees that the extra predicates won't lead to cases like the one mentioned above.

Mihai is also right about the bloat parameter I have suggested, another source of problems is the complexity of the expansion in some cases, in the best case we are able to determine the size of the expanded expression even before starting it, by looking at the "shape" of the input expression, in that case we would have a "cheap" guard that would let us bail out when the transformation would be too costly.
author_association: MEMBER
user: login: silundong
created_at: 2025-04-02T02:49:56Z
body: @asolimando Got it. Your concerns are also what I have considered. As the notes I described in jira, this rule must be guaranteed to be applied only once to avoid generating same redundant expression. A simple example is that it cannot be in the same rule set as `FilterJoinRule`. We need to apply this rule first and then `FilterJoinRule`. Of course, as you said, other rules may also interact with this rule and cause an infinite loop, especially additional operations for expressions.

My intention is that it is applied in HepPlanner and avoids interaction with other rules related to expression processing. It is applied separately, and then performs predicate pushdown, expression simplification, etc. This is indeed more strict, but I think it is safe and effective.

So should I remove it from `CoreRules` now (is that what 'not have it added to core right away and leave it as an optional rule' means?), or is there a better solution?
author_association: CONTRIBUTOR
user: login: asolimando
created_at: 2025-04-02T16:48:25Z
body: > @asolimando Got it. Your concerns are also what I have considered. As the notes I described in jira, this rule must be guaranteed to be applied only once to avoid generating same redundant expression. A simple example is that it cannot be in the same rule set as `FilterJoinRule`. We need to apply this rule first and then `FilterJoinRule`. Of course, as you said, other rules may also interact with this rule and cause an infinite loop, especially additional operations for expressions.
> 
> My intention is that it is applied in HepPlanner and avoids interaction with other rules related to expression processing. It is applied separately, and then performs predicate pushdown, expression simplification, etc. This is indeed more strict, but I think it is safe and effective.
> 
> So should I remove it from `CoreRules` now (is that what 'not have it added to core right away and leave it as an optional rule' means?), or is there a better solution?

I apologize because my comment was indeed ambiguous, what I meant was not to remove it from `CoreRules`, but to make sure the rule doesn't end up in one the sets of standard rules as defined in `RelOptRules`, this is not the case at the moment.

I think that a word of caution could be added as a comment in the javadoc of the rule itself, briefly describing known issues (what rules could be problematic when paired with this one, how to limit the danger via the bloat parameter, etc.), this will help users understanding the implications of adding this rule, especially in combination with some others known to cause infinite loops.

In the current javadoc, it is correctly suggested to use this rule before `CoreRules#FILTER_INTO_JOIN` and `CoreRules#JOIN_CONDITION_PUSH`, maybe we can be more explicit suggesting the use of the `HepPlanner` too?

It can help for discussions like [this one](https://lists.apache.org/thread/0yotdv62r6b3wg1qztftdpnkzpc4dvzv) in the mailing list to have better description of what rules to add and how they are expected to be used.
author_association: MEMBER
user: login: silundong
created_at: 2025-04-03T02:47:44Z
body: > I apologize because my comment was indeed ambiguous, what I meant was not to remove it from `CoreRules`, but to make sure the rule doesn't end up in one the sets of standard rules as defined in `RelOptRules`, this is not the case at the moment.
> 
> I think that a word of caution could be added as a comment in the javadoc of the rule itself, briefly describing known issues (what rules could be problematic when paired with this one, how to limit the danger via the bloat parameter, etc.), this will help users understanding the implications of adding this rule, especially in combination with some others known to cause infinite loops.
> 
> In the current javadoc, it is correctly suggested to use this rule before `CoreRules#FILTER_INTO_JOIN` and `CoreRules#JOIN_CONDITION_PUSH`, maybe we can be more explicit suggesting the use of the `HepPlanner` too?
> 
> It can help for discussions like [this one](https://lists.apache.org/thread/0yotdv62r6b3wg1qztftdpnkzpc4dvzv) in the mailing list to have better description of what rules to add and how they are expected to be used.

No worries, thank you for your suggestions and comments. I responded to some of your comments that I think may be fine, and I will update the others and rule that Mihaibudiu hopes for in the next commit.
author_association: CONTRIBUTOR
user: login: silundong
created_at: 2025-04-09T06:11:54Z
body: @mihaibudiu I have updated the current rule and am ready to push. Would you like me to create a new PR to develop the rule you want to add?
author_association: CONTRIBUTOR
user: login: mihaibudiu
created_at: 2025-04-09T17:30:10Z
body: I think it's simpler to make a new rule based on a new JIRA ticket and a new PR.
If it will reuse code from this, maybe we should get this merged first.
author_association: CONTRIBUTOR
user: login: silundong
created_at: 2025-04-10T08:30:50Z
body: > I think it's simpler to make a new rule based on a new JIRA ticket and a new PR. If it will reuse code from this, maybe we should get this merged first.

Ok, I have created a new [jira ticket](https://issues.apache.org/jira/browse/CALCITE-6946), and the new rule will be developed based on it.
I have updated based on the previous comments. Could you please review it again?
author_association: CONTRIBUTOR
user: login: silundong
created_at: 2025-04-11T05:58:19Z
body: > I think this is pretty good, I mostly made comments about the documentation.

Think you for your suggestion, I will update it.
author_association: CONTRIBUTOR
user: login: mihaibudiu
created_at: 2025-04-14T17:40:43Z
body: can you also please fix "disjunction" in the jira title, PR title and commit message (the last one when you squash the commits)?
author_association: CONTRIBUTOR
user: login: mihaibudiu
created_at: 2025-04-15T00:10:46Z
body: When this is ready for review please request a new one
author_association: CONTRIBUTOR
user: login: silundong
created_at: 2025-04-15T01:39:02Z
body: > When this is ready for review please request a new one

Thanks for you review. I have updated it.
author_association: CONTRIBUTOR
user: login: mihaibudiu
created_at: 2025-04-15T18:48:17Z
body: @asolimando were your comments addressed?
I'd like to merge this (after my last batch of comments are addressed as well), it looks very useful.
author_association: CONTRIBUTOR
user: login: silundong
created_at: 2025-04-16T02:33:16Z
body: > My comments are only about comments, but otherwise this looks good to merge. I apologize if my previous suggestions were not clear enough.

Sorry for the spelling and grammar errors, I have updated them.
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-16T08:00:33Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=apache_calcite&pullRequest=4267) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=apache_calcite&pullRequest=4267&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=apache_calcite&pullRequest=4267&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=apache_calcite&pullRequest=4267&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [90.2% Coverage on New Code](https://sonarcloud.io/component_measures?id=apache_calcite&pullRequest=4267&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=apache_calcite&pullRequest=4267&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=apache_calcite&pullRequest=4267)


author_association: NONE
user: login: mihaibudiu
created_at: 2025-04-16T18:04:07Z
body: > > My comments are only about comments, but otherwise this looks good to merge. I apologize if my previous suggestions were not clear enough.
> 
> Sorry for the spelling and grammar errors, I have updated them.

Especially in large PRs, please add new commits until the reviews are finished, because otherwise it's very hard to see what is changed. The commits can be squashed before merging
author_association: CONTRIBUTOR