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

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


The following commit(s) were added to refs/heads/main by this push:
     new f7a074248f [CALCITE-5285] CalcReduceExpressionsRule may transform to 
same rel
f7a074248f is described below

commit f7a074248f1b7ec610e58cdc45f6ba6f4dd14cc5
Author: Yu Xu <[email protected]>
AuthorDate: Sat Oct 24 17:37:38 2026 +0800

    [CALCITE-5285] CalcReduceExpressionsRule may transform to same rel
---
 .../calcite/rel/rules/ReduceExpressionsRule.java   | 37 ++++++++++++++++++++++
 .../org/apache/calcite/test/RelOptRulesTest.java   | 25 +++++++++++++++
 .../org/apache/calcite/test/RelOptRulesTest.xml    | 18 +++++++++++
 3 files changed, 80 insertions(+)

diff --git 
a/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java 
b/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java
index db93802118..ae00a377ee 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java
@@ -471,9 +471,46 @@ public CalcReduceExpressionsRule(Class<? extends Calc> 
calcClass,
       for (RexNode expr : exprList) {
         expandedExprList.add(expr.accept(shuttle));
       }
+
+      final List<RexNode> origProjectExprs = new ArrayList<>();
+      for (RexLocalRef projectExpr : program.getProjectList()) {
+        origProjectExprs.add(expandedExprList.get(projectExpr.getIndex()));
+      }
+      final @Nullable RexLocalRef origConditionRef = program.getCondition();
+      final @Nullable RexNode origConditionExpr =
+          origConditionRef != null
+              ? expandedExprList.get(origConditionRef.getIndex())
+              : null;
+
       final RelOptPredicateList predicates = RelOptPredicateList.EMPTY;
       if (reduceExpressions(calc, expandedExprList, predicates, false,
           config.matchNullability(), config.treatDynamicCallsAsConstant())) {
+
+        // Check whether the output expressions actually changed.
+        // reduceExpressions may return true because it simplified an
+        // intermediate expression, but the output (projects + condition)
+        // may still be equivalent to the original.
+        boolean outputChanged = false;
+        final List<RexLocalRef> projectList = program.getProjectList();
+        assert projectList.size() == origProjectExprs.size()
+            : "projectList and origProjectExprs must have the same size";
+        for (int i = 0; i < projectList.size(); i++) {
+          if (!expandedExprList.get(projectList.get(i).getIndex())
+              .equals(origProjectExprs.get(i))) {
+            outputChanged = true;
+            break;
+          }
+        }
+        if (!outputChanged && origConditionRef != null) {
+          if (!expandedExprList.get(origConditionRef.getIndex())
+              .equals(origConditionExpr)) {
+            outputChanged = true;
+          }
+        }
+        if (!outputChanged) {
+          return;
+        }
+
         final RexProgramBuilder builder =
             new RexProgramBuilder(
                 calc.getInput().getRowType(),
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 1d5224a0af..7ca0dc464b 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -5044,6 +5044,31 @@ private void 
checkPushJoinThroughUnionOnRightDoesNotMatchSemiOrAntiJoin(JoinRelT
         .check();
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-5285";>[CALCITE-5285]
+   * CalcReduceExpressionsRule creates a Calc equivalent to the input when
+   * reduceExpressions returns true but the output expressions have not
+   * changed</a>. The rule should not fire if only intermediate (non-output)
+   * expressions were simplified. */
+  @Test void testCalcReduceExpressionsNoChangeInOutput() {
+    HepProgram program = new HepProgramBuilder()
+        .addRuleInstance(CoreRules.PROJECT_TO_CALC)
+        .addRuleInstance(CoreRules.CALC_REDUCE_EXPRESSIONS)
+        .build();
+    // FLOOR(CAST(sal AS BIGINT) / 1): reduceExpressions simplifies the
+    // intermediate '/ 1' sub-expression but does not rewrite the FLOOR
+    // node that is the project output, so the output is unchanged and
+    // the rule must not fire a second time.
+    final String sql = "select floor(cast(sal as bigint) / 1)\n"
+        + "from emp";
+    // planAfter is intentionally identical to what PROJECT_TO_CALC produces;
+    // CALC_REDUCE_EXPRESSIONS does not transform the Calc because the output
+    // expressions are unchanged.
+    sql(sql)
+        .withProgram(program)
+        .check();
+  }
+
   @Test void testReduceConstantsCalc() {
     // This reduction does not work using
     // ReduceExpressionsRule.PROJECT_INSTANCE or FILTER_INSTANCE,
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 06bb03975a..f480acabe2 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -1920,6 +1920,24 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], 
MGR=[$3], HIREDATE=[$4], SAL=[$
   MultiJoin(joinFilter=[true], isFullOuterJoin=[false], joinTypes=[[INNER, 
LEFT]], outerJoinConditions=[[NULL, true]], projFields=[[ALL, ALL]])
     LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testCalcReduceExpressionsNoChangeInOutput">
+    <Resource name="sql">
+      <![CDATA[select floor(cast(sal as bigint) / 1)
+from emp]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(EXPR$0=[FLOOR(/(CAST($5):BIGINT NOT NULL, 1))])
+  LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalCalc(expr#0..8=[{inputs}], expr#9=[CAST($t5):BIGINT NOT NULL], 
expr#10=[1], expr#11=[/($t9, $t10)], expr#12=[FLOOR($t11)], EXPR$0=[$t12])
+  LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
     </Resource>
   </TestCase>

Reply via email to