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>