You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by da...@apache.org on 2020/06/16 03:11:21 UTC

[calcite] branch master updated: [CALCITE-3786] Add Digest interface to enable efficient hashCode(equals) for RexNode and RelNode

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 69f2586  [CALCITE-3786] Add Digest interface to enable efficient hashCode(equals) for RexNode and RelNode
69f2586 is described below

commit 69f25863f5f4197c17927a39a82cbf1cffd12b80
Author: yuzhao.cyz <yu...@gmail.com>
AuthorDate: Tue Jun 9 21:16:22 2020 +0800

    [CALCITE-3786] Add Digest interface to enable efficient hashCode(equals) for RexNode and RelNode
    
    * Add class Digest used to identify the node;
    * There is a pre-computed hashcode to speedup #hashCode and #equals;
    * Change RexCall to use object#equals instead of pure string digest
    comparison;
    * We only support RexInputRef normalization which is the most common
    case;
    * Remove RexNode#toStringRaw because it makes the thing complicated,
    RexNode can always be normalized(default true).
---
 .../org/apache/calcite/adapter/jdbc/JdbcRules.java |   2 +-
 .../apache/calcite/plan/AbstractRelOptPlanner.java |  14 --
 .../main/java/org/apache/calcite/plan/Digest.java  | 256 +++++++++++++++++++++
 .../plan/MaterializedViewSubstitutionVisitor.java  |   2 +-
 .../java/org/apache/calcite/plan/RelOptNode.java   |   2 +-
 .../java/org/apache/calcite/plan/RelOptUtil.java   |  19 +-
 .../org/apache/calcite/plan/hep/HepPlanner.java    |  13 +-
 .../org/apache/calcite/plan/hep/HepRelVertex.java  |   5 +-
 .../org/apache/calcite/plan/volcano/RelSubset.java |  10 +-
 .../calcite/plan/volcano/VolcanoPlanner.java       |  39 ++--
 .../org/apache/calcite/rel/AbstractRelNode.java    |  81 ++-----
 .../main/java/org/apache/calcite/rel/RelNode.java  |   3 +-
 .../java/org/apache/calcite/rel/RelWriter.java     |  15 --
 .../java/org/apache/calcite/rel/core/Match.java    |  10 +
 .../java/org/apache/calcite/rel/core/Window.java   |  22 +-
 .../calcite/rel/externalize/RelJsonWriter.java     |   5 +-
 .../calcite/rel/externalize/RelWriterImpl.java     |  21 +-
 .../rel/metadata/RelMdExpressionLineage.java       |   2 +-
 .../calcite/rel/rel2sql/RelToSqlConverter.java     |   4 +-
 .../rel/rules/JoinProjectTransposeRule.java        |   4 +-
 .../rel/rules/ProjectWindowTransposeRule.java      |   4 +-
 .../materialize/MaterializedViewAggregateRule.java |  22 +-
 .../materialize/MaterializedViewJoinRule.java      |   4 +-
 .../rules/materialize/MaterializedViewRule.java    |   2 +-
 .../main/java/org/apache/calcite/rex/RexCall.java  | 148 +++++-------
 .../main/java/org/apache/calcite/rex/RexNode.java  |  63 +----
 .../main/java/org/apache/calcite/rex/RexOver.java  |  21 ++
 .../java/org/apache/calcite/rex/RexSubQuery.java   |  10 +
 .../main/java/org/apache/calcite/sql/SqlKind.java  |  19 ++
 .../calcite/sql/fun/SqlStdOperatorTable.java       |  20 ++
 .../calcite/materialize/LatticeSuggesterTest.java  |   4 +-
 .../org/apache/calcite/plan/RelOptUtilTest.java    |   8 +-
 .../calcite/plan/volcano/TraitPropagationTest.java |   4 +-
 .../calcite/plan/volcano/VolcanoPlannerTest.java   |  10 +-
 .../calcite/rel/rules/DateRangeRulesTest.java      |   4 +-
 .../calcite/rex/RexCallNormalizationTest.java      |  76 +++---
 .../org/apache/calcite/rex/RexProgramTest.java     |   8 +-
 .../org/apache/calcite/rex/RexProgramTestBase.java |  39 ++--
 .../rex/RexSqlStandardConvertletTableTest.java     |   6 +-
 .../org/apache/calcite/test/HepPlannerTest.java    |   6 +-
 .../org/apache/calcite/test/JdbcAdapterTest.java   |   4 +-
 .../java/org/apache/calcite/test/LatticeTest.java  |   6 +-
 .../test/MaterializedViewRelOptRulesTest.java      |   3 +-
 .../MaterializedViewSubstitutionVisitorTest.java   |  10 +-
 .../org/apache/calcite/test/RelBuilderTest.java    |   6 +-
 .../org/apache/calcite/test/RelMetadataTest.java   |  14 +-
 .../org/apache/calcite/test/RelOptRulesTest.java   |   2 +-
 .../apache/calcite/test/RexTransformerTest.java    |   4 +-
 .../apache/calcite/test/SqlHintsConverterTest.java |   6 +-
 .../test/enumerable/EnumerableCorrelateTest.java   |   8 +-
 .../org/apache/calcite/test/RelOptRulesTest.xml    |  74 +++---
 .../apache/calcite/test/SqlToRelConverterTest.xml  |  24 +-
 .../org/apache/calcite/test/TopDownOptTest.xml     |   4 +-
 core/src/test/resources/sql/blank.iq               |   2 +-
 core/src/test/resources/sql/sub-query.iq           |   4 +-
 .../apache/calcite/adapter/druid/DruidRules.java   |   2 +-
 .../calcite/piglet/PigToSqlAggregateRule.java      |   6 +-
 57 files changed, 664 insertions(+), 522 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcRules.java b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcRules.java
index 4b9b1b4..438781b 100644
--- a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcRules.java
+++ b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcRules.java
@@ -523,7 +523,7 @@ public class JdbcRules {
 
     private static boolean userDefinedFunctionInProject(Project project) {
       CheckingUserDefinedFunctionVisitor visitor = new CheckingUserDefinedFunctionVisitor();
-      for (RexNode node : project.getChildExps()) {
+      for (RexNode node : project.getProjects()) {
         node.accept(visitor);
         if (visitor.containsUserDefinedFunction()) {
           return true;
diff --git a/core/src/main/java/org/apache/calcite/plan/AbstractRelOptPlanner.java b/core/src/main/java/org/apache/calcite/plan/AbstractRelOptPlanner.java
index cf350b6..08731a8 100644
--- a/core/src/main/java/org/apache/calcite/plan/AbstractRelOptPlanner.java
+++ b/core/src/main/java/org/apache/calcite/plan/AbstractRelOptPlanner.java
@@ -20,7 +20,6 @@ import org.apache.calcite.plan.volcano.RelSubset;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.metadata.RelMetadataProvider;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
-import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexExecutor;
 import org.apache.calcite.util.CancelFlag;
 import org.apache.calcite.util.Pair;
@@ -443,19 +442,6 @@ public abstract class AbstractRelOptPlanner implements RelOptPlanner {
     });
   }
 
-  /** Computes the key for relational expression digest cache. */
-  protected static Pair<String, List<RelDataType>> key(RelNode rel) {
-    return key(rel.getDigest(), rel.getRowType());
-  }
-
-  /** Computes the key for relational expression digest cache. */
-  protected static Pair<String, List<RelDataType>> key(String digest, RelDataType relType) {
-    final List<RelDataType> v = relType.isStruct()
-        ? Pair.right(relType.getFieldList())
-        : Collections.singletonList(relType);
-    return Pair.of(digest, v);
-  }
-
   /** Listener for counting the attempts of each rule. Only enabled under DEBUG level.*/
   private class RuleAttemptsListener implements RelOptListener {
     private long beforeTimestamp;
diff --git a/core/src/main/java/org/apache/calcite/plan/Digest.java b/core/src/main/java/org/apache/calcite/plan/Digest.java
new file mode 100644
index 0000000..494cb14
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/plan/Digest.java
@@ -0,0 +1,256 @@
+/*
+ * 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.plan;
+
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.util.Pair;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+
+/**
+ * A short description of relational expression's type, inputs, and
+ * other properties. The digest uniquely identifies the node; another node
+ * is equivalent if and only if it has the same value.
+ *
+ * <p>Row type is part of the digest for the rare occasion that similar
+ * expressions have different types, e.g. variants of
+ * {@code Project(child=rel#1, a=null)} where a is a null INTEGER or a
+ * null VARCHAR(10). Row type is represented as fieldTypes only, so {@code RelNode}
+ * that differ with field names only are treated equal.
+ * For instance, {@code Project(input=rel#1,empid=$0)} and {@code Project(input=rel#1,deptno=$0)}
+ * are equal.
+ *
+ * <p>Computed by {@code org.apache.calcite.rel.AbstractRelNode#computeDigest},
+ * assigned by {@link org.apache.calcite.rel.AbstractRelNode#onRegister},
+ * returned by {@link org.apache.calcite.rel.AbstractRelNode#getDigest()}.
+ */
+public class Digest implements Comparable<Digest> {
+
+  //~ Instance fields --------------------------------------------------------
+
+  private final int hashCode;
+  private final List<Pair<String, Object>> items;
+  private final RelNode rel;
+
+  // Used for debugging, computed lazily.
+  private String digest = null;
+
+  //~ Constructors -----------------------------------------------------------
+
+  /**
+   * Creates a digest with given rel and properties.
+   *
+   * @param rel   The rel
+   * @param items The properties, e.g. the inputs, the type, the traits and so on
+   */
+  private Digest(RelNode rel, List<Pair<String, Object>> items) {
+    this.rel = rel;
+    this.items = normalizeContents(items);
+    this.hashCode = computeIdentity(rel, this.items);
+  }
+
+  /**
+   * Creates a digest with given rel, the digest is computed as simple,
+   * see {@link #simpleRelDigest(RelNode)}.
+   */
+  private Digest(RelNode rel) {
+    this(rel, simpleRelDigest(rel));
+  }
+
+  /** Creates a digest with given rel and string format digest. */
+  private Digest(RelNode rel, String digest) {
+    this.rel = rel;
+    this.items = Collections.emptyList();
+    this.digest = digest;
+    this.hashCode = this.digest.hashCode();
+  }
+
+  /** Returns the identity of this digest which is used to speedup hashCode and equals. */
+  private static int computeIdentity(RelNode rel, List<Pair<String, Object>> contents) {
+    return Objects.hash(collect(rel, contents, false));
+  }
+
+  /**
+   * Collects the items used for {@link #hashCode} and {@link #equals}.
+   *
+   * <p>Generally, the items used for hashCode and equals should be the same. The exception
+   * is the row type of the relational expression: the row type is needed because during
+   * planning, new equivalent rels may be produced with changed fields nullability
+   * (i.e. most of them comes from the rex simplify or constant reduction).
+   * This expects to be rare case, so the hashcode is computed without row type
+   * but when it conflicts, we compare with the row type involved(sans field names).
+   *
+   * @param rel      The rel to compute digest
+   * @param contents The rel properties should be considered in digest
+   * @param withType Whether to involve the row type
+   */
+  private static Object[] collect(
+      RelNode rel,
+      List<Pair<String, Object>> contents,
+      boolean withType) {
+    List<Object> hashCodeItems = new ArrayList<>();
+    // The type name.
+    hashCodeItems.add(rel.getRelTypeName());
+    // The traits.
+    hashCodeItems.addAll(rel.getTraitSet());
+    // The hints.
+    if (rel instanceof Hintable) {
+      hashCodeItems.addAll(((Hintable) rel).getHints());
+    }
+    if (withType) {
+      // The row type sans field names.
+      RelDataType relType = rel.getRowType();
+      if (relType.isStruct()) {
+        hashCodeItems.addAll(Pair.right(relType.getFieldList()));
+      } else {
+        // Make a decision here because
+        // some downstream projects have custom rel type which has no explicit fields.
+        hashCodeItems.add(relType);
+      }
+    }
+    // The rel node contents(e.g. the inputs or exprs).
+    hashCodeItems.addAll(contents);
+    return hashCodeItems.toArray();
+  }
+
+  /** Normalizes the rel node properties, currently, just to replace the
+   * {@link RelNode} with a simple string format digest. **/
+  private static List<Pair<String, Object>> normalizeContents(
+      List<Pair<String, Object>> items) {
+    List<Pair<String, Object>> normalized = new ArrayList<>();
+    for (Pair<String, Object> item : items) {
+      if (item.right instanceof RelNode) {
+        RelNode input = (RelNode) item.right;
+        normalized.add(Pair.of(item.left, simpleRelDigest(input)));
+      } else {
+        normalized.add(item);
+      }
+    }
+    return normalized;
+  }
+
+  /**
+   * Returns a simple string format digest.
+   *
+   * <p>Currently, returns composition of class name and id.
+   *
+   * @param rel The rel
+   */
+  private static String simpleRelDigest(RelNode rel) {
+    return rel.getRelTypeName() + '#' + rel.getId();
+  }
+
+  @Override public String toString() {
+    if (null != digest) {
+      return digest;
+    }
+    StringBuilder sb = new StringBuilder();
+    sb.append(rel.getRelTypeName());
+
+    for (RelTrait trait : rel.getTraitSet()) {
+      sb.append('.');
+      sb.append(trait.toString());
+    }
+
+    sb.append('(');
+    int j = 0;
+    for (Pair<String, Object> item : items) {
+      if (j++ > 0) {
+        sb.append(',');
+      }
+      sb.append(item.left);
+      sb.append('=');
+      sb.append(item.right);
+    }
+    sb.append(')');
+    digest = sb.toString();
+    return digest;
+  }
+
+  @Override public int compareTo(@Nonnull Digest other) {
+    return this.equals(other) ? 0 : this.rel.getId() - other.rel.getId();
+  }
+
+  @Override public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    Digest that = (Digest) o;
+    return hashCode == that.hashCode && deepEquals(that);
+  }
+
+  /**
+   * The method is used to resolve hash conflict, in current 6000+ tests, there are about 8
+   * tests with conflict, so we do not cache the hash code items in order to
+   * reduce mem consumption.
+   */
+  private boolean deepEquals(Digest other) {
+    Object[] thisItems = collect(this.rel, this.items, true);
+    Object[] thatItems = collect(other.rel, other.items, true);
+    if (thisItems.length != thatItems.length) {
+      return false;
+    }
+    for (int i = 0; i < thisItems.length; i++) {
+      if (!Objects.equals(thisItems[i], thatItems[i])) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override public int hashCode() {
+    return hashCode;
+  }
+
+  /**
+   * Creates a digest with given rel and properties.
+   */
+  public static Digest create(RelNode rel, List<Pair<String, Object>> contents) {
+    return new Digest(rel, contents);
+  }
+
+  /**
+   * Creates a digest with given rel.
+   */
+  public static Digest create(RelNode rel) {
+    return new Digest(rel);
+  }
+
+  /**
+   * Creates a digest with given rel and string format digest
+   */
+  public static Digest create(RelNode rel, String digest) {
+    return new Digest(rel, digest);
+  }
+
+  /**
+   * Instantiates a digest with solid string format digest, this digest should only
+   * be used as a initial.
+   */
+  public static Digest initial(RelNode rel) {
+    return new Digest(rel, simpleRelDigest(rel));
+  }
+}
diff --git a/core/src/main/java/org/apache/calcite/plan/MaterializedViewSubstitutionVisitor.java b/core/src/main/java/org/apache/calcite/plan/MaterializedViewSubstitutionVisitor.java
index 0ddee54..cc3692c 100644
--- a/core/src/main/java/org/apache/calcite/plan/MaterializedViewSubstitutionVisitor.java
+++ b/core/src/main/java/org/apache/calcite/plan/MaterializedViewSubstitutionVisitor.java
@@ -22,7 +22,7 @@ import org.apache.calcite.tools.RelBuilderFactory;
 /**
  * Extension to {@link SubstitutionVisitor}.
  */
-@Deprecated // Kept for backward compatibility and to be removed before 2.0
+@Deprecated // to be removed before 2.0
 public class MaterializedViewSubstitutionVisitor extends SubstitutionVisitor {
 
   public MaterializedViewSubstitutionVisitor(RelNode target_, RelNode query_) {
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptNode.java b/core/src/main/java/org/apache/calcite/plan/RelOptNode.java
index 2a0f077..8484f54 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptNode.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptNode.java
@@ -47,7 +47,7 @@ public interface RelOptNode {
    *
    * @return Digest of this {@code RelNode}
    */
-  String getDigest();
+  Digest getDigest();
 
   /**
    * Retrieves this RelNode's traits. Note that although the RelTraitSet
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
index 8ace22d..1083047 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
@@ -182,20 +182,14 @@ public abstract class RelOptUtil {
    * Whether this node contains a limit specification.
    */
   public static boolean isLimit(RelNode rel) {
-    if ((rel instanceof Sort) && ((Sort) rel).fetch != null) {
-      return true;
-    }
-    return false;
+    return (rel instanceof Sort) && ((Sort) rel).fetch != null;
   }
 
   /**
    * Whether this node contains a sort specification.
    */
   public static boolean isOrder(RelNode rel) {
-    if ((rel instanceof Sort) && !((Sort) rel).getCollation().getFieldCollations().isEmpty()) {
-      return true;
-    }
-    return false;
+    return (rel instanceof Sort) && !((Sort) rel).getCollation().getFieldCollations().isEmpty();
   }
 
   /**
@@ -227,8 +221,9 @@ public abstract class RelOptUtil {
    * or its children.
    */
   public static List<String> findAllTableQualifiedNames(RelNode rel) {
-    return Lists.transform(findAllTables(rel),
-        table -> table.getQualifiedName().toString());
+    return findAllTables(rel).stream()
+        .map(table -> table.getQualifiedName().toString())
+        .collect(Collectors.toList());
   }
 
   /**
@@ -3802,7 +3797,7 @@ public abstract class RelOptUtil {
       return true;
     }
     final RexImplicationChecker checker =
-        new RexImplicationChecker(rexBuilder, (RexExecutorImpl) executor,
+        new RexImplicationChecker(rexBuilder, executor,
             rowType);
     final RexNode first =
         RexUtil.composeConjunction(rexBuilder, predicates.pulledUpPredicates);
@@ -4185,7 +4180,6 @@ public abstract class RelOptUtil {
 
   /** Converts types to descriptive strings. */
   public static class TypeDumper {
-    private final String extraIndent = "  ";
     private String indent;
     private final PrintWriter pw;
 
@@ -4203,6 +4197,7 @@ public abstract class RelOptUtil {
         //   J VARCHAR(240))
         pw.println("RECORD (");
         String prevIndent = indent;
+        String extraIndent = "  ";
         this.indent = indent + extraIndent;
         acceptFields(fields);
         this.indent = prevIndent;
diff --git a/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java b/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java
index 44b4fb8..36bd186 100644
--- a/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java
+++ b/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java
@@ -21,6 +21,7 @@ import org.apache.calcite.linq4j.function.Functions;
 import org.apache.calcite.plan.AbstractRelOptPlanner;
 import org.apache.calcite.plan.CommonRelSubExprRule;
 import org.apache.calcite.plan.Context;
+import org.apache.calcite.plan.Digest;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptCostFactory;
 import org.apache.calcite.plan.RelOptCostImpl;
@@ -84,7 +85,7 @@ public class HepPlanner extends AbstractRelOptPlanner {
    * {@link RelDataType} is represented with its field types as {@code List<RelDataType>}.
    * This enables to treat as equal projects that differ in expression names only.
    */
-  private final Map<Pair<String, List<RelDataType>>, HepRelVertex> mapDigestToVertex =
+  private final Map<Digest, HepRelVertex> mapDigestToVertex =
       new HashMap<>();
 
   private int nTransformations;
@@ -810,8 +811,7 @@ public class HepPlanner extends AbstractRelOptPlanner {
     // try to find equivalent rel only if DAG is allowed
     if (!noDag) {
       // Now, check if an equivalent vertex already exists in graph.
-      Pair<String, List<RelDataType>> key = key(rel);
-      HepRelVertex equivVertex = mapDigestToVertex.get(key);
+      HepRelVertex equivVertex = mapDigestToVertex.get(rel.getDigest());
       if (equivVertex != null) {
         // Use existing vertex.
         return equivVertex;
@@ -900,7 +900,7 @@ public class HepPlanner extends AbstractRelOptPlanner {
       // reachable from here.
       notifyDiscard(vertex.getCurrentRel());
     }
-    Pair<String, List<RelDataType>> oldKey = key(vertex.getCurrentRel());
+    Digest oldKey = vertex.getCurrentRel().getDigest();
     if (mapDigestToVertex.get(oldKey) == vertex) {
       mapDigestToVertex.remove(oldKey);
     }
@@ -911,8 +911,7 @@ public class HepPlanner extends AbstractRelOptPlanner {
     // otherwise the digest will be removed wrongly in the mapDigestToVertex
     //  when collectGC
     // so it must update the digest that map to vertex
-    Pair<String, List<RelDataType>> newKey = key(rel);
-    mapDigestToVertex.put(newKey, vertex);
+    mapDigestToVertex.put(rel.getDigest(), vertex);
     if (rel != vertex.getCurrentRel()) {
       vertex.replaceRel(rel);
     }
@@ -978,7 +977,7 @@ public class HepPlanner extends AbstractRelOptPlanner {
     graphSizeLastGC = graph.vertexSet().size();
 
     // Clean up digest map too.
-    Iterator<Map.Entry<Pair<String, List<RelDataType>>, HepRelVertex>> digestIter =
+    Iterator<Map.Entry<Digest, HepRelVertex>> digestIter =
         mapDigestToVertex.entrySet().iterator();
     while (digestIter.hasNext()) {
       HepRelVertex vertex = digestIter.next().getValue();
diff --git a/core/src/main/java/org/apache/calcite/plan/hep/HepRelVertex.java b/core/src/main/java/org/apache/calcite/plan/hep/HepRelVertex.java
index 6ec8a6c..92394b1 100644
--- a/core/src/main/java/org/apache/calcite/plan/hep/HepRelVertex.java
+++ b/core/src/main/java/org/apache/calcite/plan/hep/HepRelVertex.java
@@ -16,6 +16,7 @@
  */
 package org.apache.calcite.plan.hep;
 
+import org.apache.calcite.plan.Digest;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelTraitSet;
@@ -75,8 +76,8 @@ public class HepRelVertex extends AbstractRelNode {
     return currentRel.getRowType();
   }
 
-  @Override protected String computeDigest() {
-    return "HepRelVertex(" + currentRel + ")";
+  @Override protected Digest computeDigest() {
+    return Digest.create(this, getRelTypeName() + '#' + getCurrentRel().getId());
   }
 
   /**
diff --git a/core/src/main/java/org/apache/calcite/plan/volcano/RelSubset.java b/core/src/main/java/org/apache/calcite/plan/volcano/RelSubset.java
index 8b0a78a..e821496 100644
--- a/core/src/main/java/org/apache/calcite/plan/volcano/RelSubset.java
+++ b/core/src/main/java/org/apache/calcite/plan/volcano/RelSubset.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.plan.volcano;
 
 import org.apache.calcite.linq4j.Linq4j;
+import org.apache.calcite.plan.Digest;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptListener;
@@ -224,13 +225,14 @@ public class RelSubset extends AbstractRelNode {
     pw.done(input);
   }
 
-  @Override protected String computeDigest() {
-    StringBuilder digest = new StringBuilder("Subset#");
+  @Override protected Digest computeDigest() {
+    StringBuilder digest = new StringBuilder(getRelTypeName());
+    digest.append('#');
     digest.append(set.id);
-    for (RelTrait trait : traitSet) {
+    for (RelTrait trait : getTraitSet()) {
       digest.append('.').append(trait);
     }
-    return digest.toString();
+    return Digest.create(this, digest.toString());
   }
 
   @Override protected RelDataType deriveRowType() {
diff --git a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
index 8374f1b..4842932 100644
--- a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
+++ b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoPlanner.java
@@ -22,6 +22,7 @@ import org.apache.calcite.plan.AbstractRelOptPlanner;
 import org.apache.calcite.plan.Context;
 import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.ConventionTraitDef;
+import org.apache.calcite.plan.Digest;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptCostFactory;
 import org.apache.calcite.plan.RelOptLattice;
@@ -48,7 +49,6 @@ import org.apache.calcite.rel.metadata.RelMdUtil;
 import org.apache.calcite.rel.metadata.RelMetadataProvider;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.rules.TransformationRule;
-import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.runtime.Hook;
 import org.apache.calcite.sql.SqlExplainLevel;
 import org.apache.calcite.util.Litmus;
@@ -102,17 +102,8 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
   /**
    * Canonical map from {@link String digest} to the unique
    * {@link RelNode relational expression} with that digest.
-   *
-   * <p>Row type is part of the key for the rare occasion that similar
-   * expressions have different types, e.g. variants of
-   * {@code Project(child=rel#1, a=null)} where a is a null INTEGER or a
-   * null VARCHAR(10).</p>
-   * <p>Row type is represented as fieldTypes only, so {@code RelNode} that differ
-   * with field names only are treated equal.
-   * For instance, {@code Project(input=rel#1,empid=$0)} and {@code Project(input=rel#1,deptno=$0)}
-   * are equal</p>
    */
-  private final Map<Pair<String, List<RelDataType>>, RelNode> mapDigestToRel =
+  private final Map<Digest, RelNode> mapDigestToRel =
       new HashMap<>();
 
   /**
@@ -898,15 +889,13 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
    * @param rel Relational expression
    */
   void rename(RelNode rel) {
-    final String oldDigest = rel.getDigest();
+    final Digest oldDigest = rel.getDigest();
     if (fixUpInputs(rel)) {
-      final Pair<String, List<RelDataType>> oldKey = key(oldDigest, rel.getRowType());
-      final RelNode removed = mapDigestToRel.remove(oldKey);
+      final RelNode removed = mapDigestToRel.remove(oldDigest);
       assert removed == rel;
-      final String newDigest = rel.recomputeDigest();
+      final Digest newDigest = rel.recomputeDigest();
       LOGGER.trace("Rename #{} from '{}' to '{}'", rel.getId(), oldDigest, newDigest);
-      final Pair<String, List<RelDataType>> key = key(rel);
-      final RelNode equivRel = mapDigestToRel.put(key, rel);
+      final RelNode equivRel = mapDigestToRel.put(newDigest, rel);
       if (equivRel != null) {
         assert equivRel != rel;
 
@@ -915,7 +904,7 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
         LOGGER.trace("After renaming rel#{} it is now equivalent to rel#{}",
             rel.getId(), equivRel.getId());
 
-        mapDigestToRel.put(key, equivRel);
+        mapDigestToRel.put(newDigest, equivRel);
         checkPruned(equivRel, rel);
 
         RelSubset equivRelSubset = getSubset(equivRel);
@@ -970,8 +959,7 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
     // Is there an equivalent relational expression? (This might have
     // just occurred because the relational expression's child was just
     // found to be equivalent to another set.)
-    final Pair<String, List<RelDataType>> key = key(rel);
-    RelNode equivRel = mapDigestToRel.get(key);
+    RelNode equivRel = mapDigestToRel.get(rel.getDigest());
     if (equivRel != null && equivRel != rel) {
       assert equivRel.getClass() == rel.getClass();
       assert equivRel.getTraitSet().equals(rel.getTraitSet());
@@ -1180,8 +1168,8 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
 
     // If it is equivalent to an existing expression, return the set that
     // the equivalent expression belongs to.
-    Pair<String, List<RelDataType>> key = key(rel);
-    RelNode equivExp = mapDigestToRel.get(key);
+    Digest digest = rel.getDigest();
+    RelNode equivExp = mapDigestToRel.get(digest);
     if (equivExp == null) {
       // do nothing
     } else if (equivExp == rel) {
@@ -1218,9 +1206,8 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
         // check whether we are now equivalent to an existing
         // expression.
         if (fixUpInputs(rel)) {
-          rel.recomputeDigest();
-          key = key(rel);
-          RelNode equivRel = mapDigestToRel.get(key);
+          digest = rel.recomputeDigest();
+          RelNode equivRel = mapDigestToRel.get(digest);
           if ((equivRel != rel) && (equivRel != null)) {
 
             // make sure this bad rel didn't get into the
@@ -1261,7 +1248,7 @@ public class VolcanoPlanner extends AbstractRelOptPlanner {
     final int subsetBeforeCount = set.subsets.size();
     RelSubset subset = addRelToSet(rel, set);
 
-    final RelNode xx = mapDigestToRel.put(key, rel);
+    final RelNode xx = mapDigestToRel.put(digest, rel);
     assert xx == null || xx == rel : rel.getDigest();
 
     LOGGER.trace("Register {} in {}", rel, subset);
diff --git a/core/src/main/java/org/apache/calcite/rel/AbstractRelNode.java b/core/src/main/java/org/apache/calcite/rel/AbstractRelNode.java
index 8240c60..c5ec873 100644
--- a/core/src/main/java/org/apache/calcite/rel/AbstractRelNode.java
+++ b/core/src/main/java/org/apache/calcite/rel/AbstractRelNode.java
@@ -18,13 +18,13 @@ package org.apache.calcite.rel;
 
 import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.ConventionTraitDef;
+import org.apache.calcite.plan.Digest;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelOptQuery;
 import org.apache.calcite.plan.RelOptTable;
 import org.apache.calcite.plan.RelOptUtil;
-import org.apache.calcite.plan.RelTrait;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.core.CorrelationId;
 import org.apache.calcite.rel.metadata.Metadata;
@@ -70,13 +70,9 @@ public abstract class AbstractRelNode implements RelNode {
   protected RelDataType rowType;
 
   /**
-   * A short description of this relational expression's type, inputs, and
-   * other properties. The string uniquely identifies the node; another node
-   * is equivalent if and only if it has the same value. Computed by
-   * {@link #computeDigest}, assigned by {@link #onRegister}, returned by
-   * {@link #getDigest()}.
+   * The digest that uniquely identifies the node.
    */
-  protected String digest;
+  protected Digest digest;
 
   private final RelOptCluster cluster;
 
@@ -101,7 +97,7 @@ public abstract class AbstractRelNode implements RelNode {
     this.cluster = cluster;
     this.traitSet = traitSet;
     this.id = NEXT_ID.getAndIncrement();
-    this.digest = getRelTypeName() + "#" + id;
+    this.digest = Digest.initial(this);
     LOGGER.trace("new {}", digest);
   }
 
@@ -148,13 +144,13 @@ public abstract class AbstractRelNode implements RelNode {
     return null;
   }
 
-  @SuppressWarnings("deprecation")
+  @Deprecated // to be removed before 1.25
   public boolean isDistinct() {
     final RelMetadataQuery mq = cluster.getMetadataQuery();
     return Boolean.TRUE.equals(mq.areRowsUnique(this));
   }
 
-  @SuppressWarnings("deprecation")
+  @Deprecated // to be removed before 1.25
   public boolean isKey(ImmutableBitSet columns) {
     final RelMetadataQuery mq = cluster.getMetadataQuery();
     return Boolean.TRUE.equals(mq.areColumnsUnique(this, columns));
@@ -169,7 +165,7 @@ public abstract class AbstractRelNode implements RelNode {
     return inputs.get(i);
   }
 
-  @SuppressWarnings("deprecation")
+  @Deprecated // to be removed before 1.25
   public final RelOptQuery getQuery() {
     return getCluster().getQuery();
   }
@@ -193,7 +189,7 @@ public abstract class AbstractRelNode implements RelNode {
     return litmus.succeed();
   }
 
-  @SuppressWarnings("deprecation")
+  @Deprecated // to be removed before 1.25
   public boolean isValid(boolean fail) {
     return isValid(Litmus.THROW, null);
   }
@@ -226,7 +222,7 @@ public abstract class AbstractRelNode implements RelNode {
     return Collections.emptyList();
   }
 
-  @SuppressWarnings("deprecation")
+  @Deprecated // to be removed before 1.25
   public final double getRows() {
     return estimateRowCount(cluster.getMetadataQuery());
   }
@@ -235,7 +231,7 @@ public abstract class AbstractRelNode implements RelNode {
     return 1.0;
   }
 
-  @SuppressWarnings("deprecation")
+  @Deprecated // to be removed before 1.25
   public final Set<String> getVariablesStopped() {
     return CorrelationId.names(getVariablesSet());
   }
@@ -281,7 +277,6 @@ public abstract class AbstractRelNode implements RelNode {
       RelMetadataQuery mq) {
     // by default, assume cost is proportional to number of rows
     double rowCount = mq.getRowCount(this);
-    double bytesPerRow = 1;
     return planner.getCostFactory().makeCost(rowCount, rowCount, 0);
   }
 
@@ -323,15 +318,11 @@ public abstract class AbstractRelNode implements RelNode {
     List<RelNode> inputs = new ArrayList<>(oldInputs.size());
     for (final RelNode input : oldInputs) {
       RelNode e = planner.ensureRegistered(input, null);
-      if (e != input) {
-        // TODO: change 'equal' to 'eq', which is stronger.
-        assert RelOptUtil.equal(
-            "rowtype of rel before registration",
-            input.getRowType(),
-            "rowtype of rel after registration",
-            e.getRowType(),
-            Litmus.THROW);
-      }
+      assert e == input || RelOptUtil.equal("rowtype of rel before registration",
+          input.getRowType(),
+          "rowtype of rel after registration",
+          e.getRowType(),
+          Litmus.THROW);
       inputs.add(e);
     }
     RelNode r = this;
@@ -343,7 +334,7 @@ public abstract class AbstractRelNode implements RelNode {
     return r;
   }
 
-  public String recomputeDigest() {
+  public Digest recomputeDigest() {
     digest = computeDigest();
     assert digest != null : "computeDigest() should be non-null";
     return digest;
@@ -355,20 +346,20 @@ public abstract class AbstractRelNode implements RelNode {
     throw new UnsupportedOperationException("replaceInput called on " + this);
   }
 
-  /* Description, consists of id plus digest */
+  /** Description, consists of id plus digest */
   public String toString() {
     StringBuilder sb = new StringBuilder();
-    sb = RelOptUtil.appendRelDescription(sb, this);
+    RelOptUtil.appendRelDescription(sb, this);
     return sb.toString();
   }
 
-  /* Description, consists of id plus digest */
+  /** Description, consists of id plus digest */
   @Deprecated // to be removed before 2.0
   public final String getDescription() {
     return this.toString();
   }
 
-  public final String getDigest() {
+  public final Digest getDigest() {
     return digest;
   }
 
@@ -381,7 +372,7 @@ public abstract class AbstractRelNode implements RelNode {
    *
    * @return Digest
    */
-  protected String computeDigest() {
+  protected Digest computeDigest() {
     RelDigestWriter rdw = new RelDigestWriter();
     explain(rdw);
     return rdw.digest;
@@ -421,7 +412,7 @@ public abstract class AbstractRelNode implements RelNode {
 
     private final List<Pair<String, Object>> values = new ArrayList<>();
 
-    String digest = null;
+    Digest digest = null;
 
     @Override public void explain(final RelNode rel, final List<Pair<String, Object>> valueList) {
       throw new IllegalStateException("Should not be called for computing digest");
@@ -437,33 +428,7 @@ public abstract class AbstractRelNode implements RelNode {
     }
 
     @Override public RelWriter done(RelNode node) {
-      StringBuilder sb = new StringBuilder();
-      sb.append(node.getRelTypeName());
-
-      for (RelTrait trait : node.getTraitSet()) {
-        sb.append('.');
-        sb.append(trait.toString());
-      }
-
-      sb.append('(');
-      int j = 0;
-      for (Pair<String, Object> value : values) {
-        if (j++ > 0) {
-          sb.append(',');
-        }
-        sb.append(value.left);
-        sb.append('=');
-        if (value.right instanceof RelNode) {
-          RelNode input = (RelNode) value.right;
-          sb.append(input.getRelTypeName());
-          sb.append('#');
-          sb.append(input.getId());
-        } else {
-          sb.append(value.right);
-        }
-      }
-      sb.append(')');
-      digest = sb.toString();
+      digest = Digest.create(node, values);
       return this;
     }
   }
diff --git a/core/src/main/java/org/apache/calcite/rel/RelNode.java b/core/src/main/java/org/apache/calcite/rel/RelNode.java
index 11c35ef..18ff76f 100644
--- a/core/src/main/java/org/apache/calcite/rel/RelNode.java
+++ b/core/src/main/java/org/apache/calcite/rel/RelNode.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.rel;
 
 import org.apache.calcite.plan.Convention;
+import org.apache.calcite.plan.Digest;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptNode;
 import org.apache.calcite.plan.RelOptPlanner;
@@ -308,7 +309,7 @@ public interface RelNode extends RelOptNode, Cloneable {
    *
    * @return Digest of this relational expression
    */
-  String recomputeDigest();
+  Digest recomputeDigest();
 
   /**
    * Replaces the <code>ordinalInParent</code><sup>th</sup> input. You must
diff --git a/core/src/main/java/org/apache/calcite/rel/RelWriter.java b/core/src/main/java/org/apache/calcite/rel/RelWriter.java
index 4f6d114..92f78b6 100644
--- a/core/src/main/java/org/apache/calcite/rel/RelWriter.java
+++ b/core/src/main/java/org/apache/calcite/rel/RelWriter.java
@@ -16,12 +16,9 @@
  */
 package org.apache.calcite.rel;
 
-import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.sql.SqlExplainLevel;
 import org.apache.calcite.util.Pair;
 
-import org.apiguardian.api.API;
-
 import java.util.List;
 
 /**
@@ -88,16 +85,4 @@ public interface RelWriter {
   default boolean nest() {
     return false;
   }
-
-  /**
-   * Activates {@link RexNode} normalization if {@link SqlExplainLevel#DIGEST_ATTRIBUTES} is used.
-   * Note: the returned value must be closed, and the API is designed to be used with a
-   * try-with-resources.
-   * @return a handle that should be closed to revert normalization state
-   */
-  @API(since = "1.22", status = API.Status.EXPERIMENTAL)
-  default RexNode.Closeable withRexNormalize() {
-    boolean needNormalize = getDetailLevel() == SqlExplainLevel.DIGEST_ATTRIBUTES;
-    return RexNode.withNormalize(needNormalize);
-  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Match.java b/core/src/main/java/org/apache/calcite/rel/core/Match.java
index 12fe3b9..4b1a5ca 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Match.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Match.java
@@ -332,5 +332,15 @@ public abstract class Match extends SingleRel {
     @Override public int compareTo(RexMRAggCall o) {
       return toString().compareTo(o.toString());
     }
+
+    @Override public boolean equals(Object obj) {
+      return obj == this
+          || obj instanceof RexMRAggCall
+          && toString().equals(obj.toString());
+    }
+
+    @Override public int hashCode() {
+      return toString().hashCode();
+    }
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Window.java b/core/src/main/java/org/apache/calcite/rel/core/Window.java
index 71176e5..620afe6 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Window.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Window.java
@@ -394,16 +394,24 @@ public abstract class Window extends SingleRel {
       this.ignoreNulls = ignoreNulls;
     }
 
-    /** {@inheritDoc}
-     *
-     * <p>Override {@link RexCall}, defining equality based on identity.
-     */
-    @Override public boolean equals(Object obj) {
-      return this == obj;
+    @Override public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      if (!super.equals(o)) {
+        return false;
+      }
+      RexWinAggCall that = (RexWinAggCall) o;
+      return ordinal == that.ordinal
+          && distinct == that.distinct
+          && ignoreNulls == that.ignoreNulls;
     }
 
     @Override public int hashCode() {
-      return Objects.hash(digest, ordinal, distinct);
+      return Objects.hash(super.hashCode(), ordinal, distinct, ignoreNulls);
     }
 
     @Override public RexCall clone(RelDataType type, List<RexNode> operands) {
diff --git a/core/src/main/java/org/apache/calcite/rel/externalize/RelJsonWriter.java b/core/src/main/java/org/apache/calcite/rel/externalize/RelJsonWriter.java
index acdd2cf..5b6a346 100644
--- a/core/src/main/java/org/apache/calcite/rel/externalize/RelJsonWriter.java
+++ b/core/src/main/java/org/apache/calcite/rel/externalize/RelJsonWriter.java
@@ -18,7 +18,6 @@ package org.apache.calcite.rel.externalize;
 
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelWriter;
-import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.sql.SqlExplainLevel;
 import org.apache.calcite.util.JsonBuilder;
 import org.apache.calcite.util.Pair;
@@ -141,8 +140,6 @@ public class RelJsonWriter implements RelWriter {
   public String asString() {
     final Map<String, Object> map = jsonBuilder.map();
     map.put("rels", relList);
-    try (RexNode.Closeable ignored = withRexNormalize()) {
-      return jsonBuilder.toJsonString(map);
-    }
+    return jsonBuilder.toJsonString(map);
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/externalize/RelWriterImpl.java b/core/src/main/java/org/apache/calcite/rel/externalize/RelWriterImpl.java
index 02eec8b..3fd1ebc 100644
--- a/core/src/main/java/org/apache/calcite/rel/externalize/RelWriterImpl.java
+++ b/core/src/main/java/org/apache/calcite/rel/externalize/RelWriterImpl.java
@@ -21,7 +21,6 @@ import org.apache.calcite.linq4j.Ord;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelWriter;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
-import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.sql.SqlExplainLevel;
 import org.apache.calcite.util.Pair;
 
@@ -125,9 +124,7 @@ public class RelWriterImpl implements RelWriter {
   }
 
   public final void explain(RelNode rel, List<Pair<String, Object>> valueList) {
-    try (RexNode.Closeable ignored = withRexNormalize()) {
-      explain_(rel, valueList);
-    }
+    explain_(rel, valueList);
   }
 
   public SqlExplainLevel getDetailLevel() {
@@ -144,9 +141,7 @@ public class RelWriterImpl implements RelWriter {
     final List<Pair<String, Object>> valuesCopy =
         ImmutableList.copyOf(values);
     values.clear();
-    try (RexNode.Closeable ignored = withRexNormalize()) {
-      explain_(node, valuesCopy);
-    }
+    explain_(node, valuesCopy);
     pw.flush();
     return this;
   }
@@ -169,15 +164,13 @@ public class RelWriterImpl implements RelWriter {
    */
   public String simple() {
     final StringBuilder buf = new StringBuilder("(");
-    try (RexNode.Closeable ignored = withRexNormalize()) {
-      for (Ord<Pair<String, Object>> ord : Ord.zip(values)) {
-        if (ord.i > 0) {
-          buf.append(", ");
-        }
-        buf.append(ord.e.left).append("=[").append(ord.e.right).append("]");
+    for (Ord<Pair<String, Object>> ord : Ord.zip(values)) {
+      if (ord.i > 0) {
+        buf.append(", ");
       }
-      buf.append(")");
+      buf.append(ord.e.left).append("=[").append(ord.e.right).append("]");
     }
+    buf.append(")");
     return buf.toString();
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdExpressionLineage.java b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdExpressionLineage.java
index 7f495b1..6aba8b3 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdExpressionLineage.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdExpressionLineage.java
@@ -361,7 +361,7 @@ public class RelMdExpressionLineage
     // Infer column origin expressions for given references
     final Map<RexInputRef, Set<RexNode>> mapping = new LinkedHashMap<>();
     for (int idx : inputFieldsUsed) {
-      final RexNode inputExpr = rel.getChildExps().get(idx);
+      final RexNode inputExpr = rel.getProjects().get(idx);
       final Set<RexNode> originalExprs = mq.getExpressionLineage(input, inputExpr);
       if (originalExprs == null) {
         // Bail out
diff --git a/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
index a4fcd42..083e5e1 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
@@ -326,13 +326,13 @@ public class RelToSqlConverter extends SqlImplementor
     e.getVariablesSet();
     Result x = visitChild(0, e.getInput());
     parseCorrelTable(e, x);
-    if (isStar(e.getChildExps(), e.getInput().getRowType(), e.getRowType())) {
+    if (isStar(e.getProjects(), e.getInput().getRowType(), e.getRowType())) {
       return x;
     }
     final Builder builder =
         x.builder(e, Clause.SELECT);
     final List<SqlNode> selectList = new ArrayList<>();
-    for (RexNode ref : e.getChildExps()) {
+    for (RexNode ref : e.getProjects()) {
       SqlNode sqlExpr = builder.context.toSql(null, ref);
       if (SqlUtil.isNullLiteral(sqlExpr, false)) {
         sqlExpr = castNullType(sqlExpr, e.getRowType().getFieldList().get(selectList.size()));
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/JoinProjectTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/JoinProjectTransposeRule.java
index 085b3e6..b3104dc 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/JoinProjectTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/JoinProjectTransposeRule.java
@@ -171,11 +171,11 @@ public class JoinProjectTransposeRule extends RelOptRule implements Transformati
     }
 
     // Skip projects containing over clause
-    if (leftProj != null && RexOver.containsOver(leftProj.getChildExps(), null)) {
+    if (leftProj != null && RexOver.containsOver(leftProj.getProjects(), null)) {
       leftProj = null;
       leftJoinChild = joinRel.getLeft();
     }
-    if (rightProj != null && RexOver.containsOver(rightProj.getChildExps(), null)) {
+    if (rightProj != null && RexOver.containsOver(rightProj.getProjects(), null)) {
       rightProj = null;
       rightJoinChild = joinRel.getRight();
     }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/ProjectWindowTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/ProjectWindowTransposeRule.java
index 74b9a96..de52288 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/ProjectWindowTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/ProjectWindowTransposeRule.java
@@ -174,7 +174,7 @@ public class ProjectWindowTransposeRule extends RelOptRule implements Transforma
 
     // Modify the top LogicalProject
     final List<RexNode> topProjExps =
-        indexAdjustment.visitList(project.getChildExps());
+        indexAdjustment.visitList(project.getProjects());
 
     final LogicalProject newTopProj = project.copy(
         newLogicalWindow.getTraitSet(),
@@ -205,7 +205,7 @@ public class ProjectWindowTransposeRule extends RelOptRule implements Transforma
     };
 
     // Reference in LogicalProject
-    referenceFinder.visitEach(project.getChildExps());
+    referenceFinder.visitEach(project.getProjects());
 
     // Reference in LogicalWindow
     for (Window.Group group : window.groups) {
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewAggregateRule.java b/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewAggregateRule.java
index f4c318d..9ccacf6 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewAggregateRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewAggregateRule.java
@@ -225,9 +225,9 @@ public abstract class MaterializedViewAggregateRule extends MaterializedViewRule
           0, 0, aggregateViewNode.getGroupCount(),
           newViewNode.getGroupCount(), aggregateViewNode.getGroupCount(),
           aggregateViewNode.getAggCallList().size());
-      for (int i = 0; i < topViewProject.getChildExps().size(); i++) {
+      for (int i = 0; i < topViewProject.getProjects().size(); i++) {
         nodes.add(
-            topViewProject.getChildExps().get(i).accept(
+            topViewProject.getProjects().get(i).accept(
                 new RexPermuteInputsShuttle(shiftMapping, newViewNode)));
         fieldNames.add(topViewProject.getRowType().getFieldNames().get(i));
       }
@@ -414,7 +414,7 @@ public abstract class MaterializedViewAggregateRule extends MaterializedViewRule
       // We have a Project on top, gather only what is needed
       final RelOptUtil.InputFinder inputFinder =
           new RelOptUtil.InputFinder(new LinkedHashSet<>());
-      inputFinder.visitEach(topProject.getChildExps());
+      inputFinder.visitEach(topProject.getProjects());
       references = inputFinder.build();
       for (int i = 0; i < queryAggregate.getGroupCount(); i++) {
         indexes.set(queryAggregate.getGroupSet().nth(i));
@@ -561,8 +561,8 @@ public abstract class MaterializedViewAggregateRule extends MaterializedViewRule
           final ImmutableBitSet refs = RelOptUtil.InputFinder.bits(targetNode);
           for (int childTargetIdx : refs) {
             added = false;
-            for (int k = 0; k < topViewProject.getChildExps().size() && !added; k++) {
-              RexNode n = topViewProject.getChildExps().get(k);
+            for (int k = 0; k < topViewProject.getProjects().size() && !added; k++) {
+              RexNode n = topViewProject.getProjects().get(k);
               if (!n.isA(SqlKind.INPUT_REF)) {
                 continue;
               }
@@ -589,8 +589,8 @@ public abstract class MaterializedViewAggregateRule extends MaterializedViewRule
           added = true;
         } else {
           // This expression should be referenced directly
-          for (int k = 0; k < topViewProject.getChildExps().size() && !added; k++) {
-            RexNode n = topViewProject.getChildExps().get(k);
+          for (int k = 0; k < topViewProject.getProjects().size() && !added; k++) {
+            RexNode n = topViewProject.getProjects().get(k);
             if (!n.isA(SqlKind.INPUT_REF)) {
               continue;
             }
@@ -623,8 +623,8 @@ public abstract class MaterializedViewAggregateRule extends MaterializedViewRule
         }
         AggregateCall queryAggCall = queryAggregate.getAggCallList().get(i);
         boolean added = false;
-        for (int k = 0; k < topViewProject.getChildExps().size() && !added; k++) {
-          RexNode n = topViewProject.getChildExps().get(k);
+        for (int k = 0; k < topViewProject.getProjects().size() && !added; k++) {
+          RexNode n = topViewProject.getProjects().get(k);
           if (!n.isA(SqlKind.INPUT_REF)) {
             continue;
           }
@@ -701,7 +701,7 @@ public abstract class MaterializedViewAggregateRule extends MaterializedViewRule
     final RelDataType topRowType;
     final List<RexNode> topExprs = new ArrayList<>();
     if (topProject != null && !unionRewriting) {
-      topExprs.addAll(topProject.getChildExps());
+      topExprs.addAll(topProject.getProjects());
       topRowType = topProject.getRowType();
     } else {
       // Add all
@@ -713,7 +713,7 @@ public abstract class MaterializedViewAggregateRule extends MaterializedViewRule
     // Available in view.
     final Multimap<RexNode, Integer> viewExprs = ArrayListMultimap.create();
     int numberViewExprs = 0;
-    for (RexNode viewExpr : topViewProject.getChildExps()) {
+    for (RexNode viewExpr : topViewProject.getProjects()) {
       viewExprs.put(viewExpr, numberViewExprs++);
     }
     for (RexNode additionalViewExpr : additionalViewExprs) {
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewJoinRule.java b/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewJoinRule.java
index 7b470fb..ef0fcd2 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewJoinRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewJoinRule.java
@@ -245,7 +245,7 @@ public abstract class MaterializedViewJoinRule extends MaterializedViewRule {
       EquivalenceClasses queryEC) {
     List<RexNode> exprs = topProject == null
         ? extractReferences(rexBuilder, node)
-        : topProject.getChildExps();
+        : topProject.getProjects();
     List<RexNode> exprsLineage = new ArrayList<>(exprs.size());
     for (RexNode expr : exprs) {
       Set<RexNode> s = mq.getExpressionLineage(node, expr);
@@ -262,7 +262,7 @@ public abstract class MaterializedViewJoinRule extends MaterializedViewRule {
     }
     List<RexNode> viewExprs = topViewProject == null
         ? extractReferences(rexBuilder, viewNode)
-        : topViewProject.getChildExps();
+        : topViewProject.getProjects();
     List<RexNode> rewrittenExprs = rewriteExpressions(rexBuilder, mq, input, viewNode, viewExprs,
         queryToViewTableMapping.inverse(), queryEC, true, exprsLineage);
     if (rewrittenExprs == null) {
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewRule.java b/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewRule.java
index fc3741c..00e9722 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewRule.java
@@ -428,7 +428,7 @@ public abstract class MaterializedViewRule extends RelOptRule {
               // in the view output (condition 2).
               List<RexNode> viewExprs = topViewProject == null
                   ? extractReferences(rexBuilder, view)
-                  : topViewProject.getChildExps();
+                  : topViewProject.getProjects();
               // For compensationColumnsEquiPred, we use the view equivalence classes,
               // since we want to enforce the rest
               if (!compensationColumnsEquiPred.isAlwaysTrue()) {
diff --git a/core/src/main/java/org/apache/calcite/rex/RexCall.java b/core/src/main/java/org/apache/calcite/rex/RexCall.java
index d5c2cea..6b8f2ac 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexCall.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexCall.java
@@ -22,18 +22,16 @@ import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlSyntax;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.util.Litmus;
+import org.apache.calcite.util.Pair;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Sets;
 
 import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 import javax.annotation.Nonnull;
 
 /**
@@ -53,12 +51,6 @@ import javax.annotation.Nonnull;
  * no one is going to be generating source code from this tree.)</p>
  */
 public class RexCall extends RexNode {
-  /**
-   * Sort shorter digests first, then order by string representation.
-   * The result is designed for consistent output and better readability.
-   */
-  private static final Comparator<String> OPERAND_READABILITY_COMPARATOR =
-      Comparator.comparing(String::length).thenComparing(Comparator.naturalOrder());
 
   //~ Instance fields --------------------------------------------------------
 
@@ -67,22 +59,6 @@ public class RexCall extends RexNode {
   public final RelDataType type;
   public final int nodeCount;
 
-  /**
-   * Simple binary operators are those operators which expects operands from the same Domain.
-   *
-   * <p>Example: simple comparisions ({@code =}, {@code <}).
-   *
-   * <p>Note: it does not contain {@code IN} because that is defined on D x D^n.
-   */
-  private static final Set<SqlKind> SIMPLE_BINARY_OPS;
-
-  static {
-    EnumSet<SqlKind> kinds = EnumSet.of(SqlKind.PLUS, SqlKind.MINUS, SqlKind.TIMES, SqlKind.DIVIDE);
-    kinds.addAll(SqlKind.COMPARISON);
-    kinds.remove(SqlKind.IN);
-    SIMPLE_BINARY_OPS = Sets.immutableEnumSet(kinds);
-  }
-
   //~ Constructors -----------------------------------------------------------
 
   protected RexCall(
@@ -90,8 +66,11 @@ public class RexCall extends RexNode {
       SqlOperator op,
       List<? extends RexNode> operands) {
     this.type = Objects.requireNonNull(type, "type");
-    this.op = Objects.requireNonNull(op, "operator");
-    this.operands = ImmutableList.copyOf(operands);
+    Objects.requireNonNull(op, "operator");
+    final Pair<SqlOperator, ImmutableList<RexNode>> normalized =
+        normalize(op, operands);
+    this.op = normalized.left;
+    this.operands = normalized.right;
     this.nodeCount = RexUtil.nodeCount(1, this.operands);
     assert op.getKind() != null : op;
     assert op.validRexOperands(operands.size(), Litmus.THROW) : this;
@@ -108,11 +87,10 @@ public class RexCall extends RexNode {
    *
    * @see RexLiteral#computeDigest(RexDigestIncludeType)
    * @param sb destination
-   * @return original StringBuilder for fluent API
    */
-  protected final StringBuilder appendOperands(StringBuilder sb) {
+  protected final void appendOperands(StringBuilder sb) {
     if (operands.isEmpty()) {
-      return sb;
+      return;
     }
     List<String> operandDigests = new ArrayList<>(operands.size());
     for (int i = 0; i < operands.size(); i++) {
@@ -123,15 +101,16 @@ public class RexCall extends RexNode {
       }
       // Type information might be omitted in certain cases to improve readability
       // For instance, AND/OR arguments should be BOOLEAN, so
-      // AND(true, null) is better than AND(true, null:BOOLEAN), and we keep the same info
-      // +($0, 2) is better than +($0, 2:BIGINT). Note: if $0 has BIGINT, then 2 is expected to be
+      // AND(true, null) is better than AND(true, null:BOOLEAN), and we keep the same info.
+
+      // +($0, 2) is better than +($0, 2:BIGINT). Note: if $0 is BIGINT, then 2 is expected to be
       // of BIGINT type as well.
       RexDigestIncludeType includeType = RexDigestIncludeType.OPTIONAL;
       if ((isA(SqlKind.AND) || isA(SqlKind.OR))
           && operand.getType().getSqlTypeName() == SqlTypeName.BOOLEAN) {
         includeType = RexDigestIncludeType.NO_TYPE;
       }
-      if (SIMPLE_BINARY_OPS.contains(getKind()) && operands.size() == 2) {
+      if (SqlKind.SIMPLE_BINARY_OPS.contains(getKind())) {
         RexNode otherArg = operands.get(1 - i);
         if ((!(otherArg instanceof RexLiteral)
             || ((RexLiteral) otherArg).digestIncludesType() == RexDigestIncludeType.NO_TYPE)
@@ -146,7 +125,6 @@ public class RexCall extends RexNode {
       totalLength += s.length();
     }
     sb.ensureCapacity(sb.length() + totalLength);
-    sortOperandsIfNeeded(sb, operands, operandDigests);
     for (int i = 0; i < operandDigests.size(); i++) {
       String op = operandDigests.get(i);
       if (i != 0) {
@@ -154,72 +132,58 @@ public class RexCall extends RexNode {
       }
       sb.append(op);
     }
-    return sb;
   }
 
-  private void sortOperandsIfNeeded(StringBuilder sb,
-      List<RexNode> operands, List<String> operandDigests) {
-    if (operands.isEmpty() || !needNormalize()) {
-      return;
+  private Pair<SqlOperator, ImmutableList<RexNode>> normalize(
+      SqlOperator operator,
+      List<? extends RexNode> operands) {
+    final ImmutableList<RexNode> oldOperands = ImmutableList.copyOf(operands);
+    final Pair<SqlOperator, ImmutableList<RexNode>> original = Pair.of(operator, oldOperands);
+    if (!needNormalize()) {
+      return original;
+    }
+    if (operands.size() != 2
+        || !operands.stream().allMatch(operand -> operand.getClass() == RexInputRef.class)) {
+      return original;
     }
-    final SqlKind kind = op.getKind();
+    final RexInputRef operand0 = (RexInputRef) operands.get(0);
+    final RexInputRef operand1 = (RexInputRef) operands.get(1);
+    final SqlKind kind = operator.getKind();
+    if (operand0.getIndex() < operand1.getIndex()) {
+      return original;
+    }
+    // If arguments are the same, then we normalize < vs >
+    // '<' == 60, '>' == 62, so we prefer <.
+    if (operand0.getIndex() == operand1.getIndex()) {
+      if (kind.reverse().compareTo(kind) < 0) {
+        return Pair.of(SqlStdOperatorTable.reverse(operator), oldOperands);
+      } else {
+        return original;
+      }
+    }
+
     if (SqlKind.SYMMETRICAL_SAME_ARG_TYPE.contains(kind)) {
       final RelDataType firstType = operands.get(0).getType();
       for (int i = 1; i < operands.size(); i++) {
         if (!equalSansNullability(firstType, operands.get(i).getType())) {
           // Arguments have different type, thus they must not be sorted
-          return;
+          return original;
         }
       }
       // fall through: order arguments below
     } else if (!SqlKind.SYMMETRICAL.contains(kind)
-        && (kind == kind.reverse()
-        || !op.getName().equals(kind.sql)
-        || sb.length() < kind.sql.length() + 1
-        || sb.charAt(sb.length() - 1) != '(')) {
+        && kind == kind.reverse()) {
       // The operations have to be either symmetrical or reversible
       // Nothing matched => we skip argument sorting
-      // Note: RexCall digest uses op.getName() that might be different from kind.sql
-      // for certain calls. So we skip normalizing the calls that have customized op.getName()
-      // We ensure the current string contains enough room for preceding kind.sql otherwise
-      // we won't have an option to replace the operator to reverse it in case the operands are
-      // reordered.
-      return;
+      return original;
     }
-    // $0=$1 is the same as $1=$0, so we make sure the digest is the same for them
-    String oldFirstArg = operandDigests.get(0);
-    operandDigests.sort(OPERAND_READABILITY_COMPARATOR);
+    // $0=$1 is the same as $1=$0, so we make sure the digest is the same for them.
 
     // When $1 > $0 is normalized, the operation needs to be flipped
-    // So we sort arguments first, then flip the sign
-    if (kind != kind.reverse()) {
-      assert operands.size() == 2
-          : "Compare operation must have 2 arguments: " + this
-          + ". Actual arguments are " + operandDigests;
-      int operatorEnd = sb.length() - 1 /* ( */;
-      int operatorStart = operatorEnd - op.getName().length();
-      assert op.getName().contentEquals(sb.subSequence(operatorStart, operatorEnd))
-          : "Operation name must precede opening brace like in <=(x, y). Actual content is "
-          + sb.subSequence(operatorStart, operatorEnd)
-          + " at position " + operatorStart + " in " + sb;
-
-      SqlKind newKind = kind.reverse();
-
-      // If arguments are the same, then we normalize < vs >
-      // '<' == 60, '>' == 62, so we prefer <
-      if (operandDigests.get(0).equals(operandDigests.get(1))) {
-        if (newKind.compareTo(kind) > 0) {
-          // If reverse kind is greater, then skip reversing
-          return;
-        }
-      } else if (oldFirstArg.equals(operandDigests.get(0))) {
-        // The sorting did not shuffle the operands, so we do not need to update operation name
-        // in the digest
-        return;
-      }
-      // Replace operator name in the digest
-      sb.replace(operatorStart, operatorEnd, newKind.sql);
-    }
+    // So we sort arguments first, then flip the sign.
+    final SqlOperator newOp = SqlStdOperatorTable.reverse(operator);
+    final ImmutableList<RexNode> newOperands = ImmutableList.of(operand1, operand0);
+    return Pair.of(newOp, newOperands);
   }
 
   /**
@@ -359,13 +323,19 @@ public class RexCall extends RexNode {
     return new RexCall(type, op, operands);
   }
 
-  @Override public boolean equals(Object obj) {
-    return obj == this
-        || obj instanceof RexCall
-        && toString().equals(obj.toString());
+  @Override public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    RexCall rexCall = (RexCall) o;
+    return op.equals(rexCall.op)
+        && operands.equals(rexCall.operands);
   }
 
   @Override public int hashCode() {
-    return toString().hashCode();
+    return Objects.hash(op, operands);
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/rex/RexNode.java b/core/src/main/java/org/apache/calcite/rex/RexNode.java
index 7a8f058..88332a2 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexNode.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexNode.java
@@ -23,7 +23,6 @@ import org.apache.calcite.sql.SqlKind;
 import org.apiguardian.api.API;
 
 import java.util.Collection;
-import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Row expression.
@@ -40,54 +39,6 @@ import java.util.concurrent.atomic.AtomicInteger;
  * <p>All sub-classes of RexNode are immutable.</p>
  */
 public abstract class RexNode {
-  /**
-   * Sometimes RexCall nodes are located deep (e.g. inside Lists),
-   * If the value is non-zero, then a non-normalized representation is printed.
-   * int is used to allow for re-entrancy.
-   */
-  private static final ThreadLocal<AtomicInteger> DESCRIBE_WITHOUT_NORMALIZE =
-      ThreadLocal.withInitial(AtomicInteger::new);
-
-  /** Removes a Hook after use. */
-  @API(since = "1.22", status = API.Status.EXPERIMENTAL)
-  public interface Closeable extends AutoCloseable {
-    // override, removing "throws"
-    @Override void close();
-  }
-
-  private static final Closeable DECREMENT_ON_CLOSE = () -> {
-    DESCRIBE_WITHOUT_NORMALIZE.get().decrementAndGet();
-  };
-
-  private static final Closeable EMPTY = () -> { };
-
-  /**
-   * The digest of {@code RexNode} is normalized by default, however, sometimes a non-normalized
-   * representation is required.
-   * This API enables to skip normalization.
-   * Note: the returned value must be closed, and the API is designed to be used with a
-   * try-with-resources.
-   * @param needNormalize true if normalization should be enabled or false if it should be skipped
-   * @return a handle that should be closed to revert normalization state
-   */
-  @API(since = "1.22", status = API.Status.EXPERIMENTAL)
-  public static Closeable withNormalize(boolean needNormalize) {
-    return needNormalize ? EMPTY : skipNormalize();
-  }
-
-  /**
-   * The digest of {@code RexNode} is normalized by default, however, sometimes a non-normalized
-   * representation is required.
-   * This API enables to skip normalization.
-   * Note: the returned value must be closed, and the API is designed to be used with a
-   * try-with-resources.
-   * @return a handle that should be closed to revert normalization state
-   */
-  @API(since = "1.22", status = API.Status.EXPERIMENTAL)
-  public static Closeable skipNormalize() {
-    DESCRIBE_WITHOUT_NORMALIZE.get().incrementAndGet();
-    return DECREMENT_ON_CLOSE;
-  }
 
   /**
    * The digest of {@code RexNode} is normalized by default, however, sometimes a non-normalized
@@ -97,8 +48,7 @@ public abstract class RexNode {
    */
   @API(since = "1.22", status = API.Status.EXPERIMENTAL)
   protected static boolean needNormalize() {
-    return DESCRIBE_WITHOUT_NORMALIZE.get().get() == 0
-        && CalciteSystemProperty.ENABLE_REX_DIGEST_NORMALIZE.value();
+    return CalciteSystemProperty.ENABLE_REX_DIGEST_NORMALIZE.value();
   }
 
   //~ Instance fields --------------------------------------------------------
@@ -147,17 +97,6 @@ public abstract class RexNode {
     return digest;
   }
 
-  /**
-   * Returns string representation of this node.
-   * @return the same as {@link #toString()}, but without normalizing the output
-   */
-  @API(since = "1.22", status = API.Status.EXPERIMENTAL)
-  public String toStringRaw() {
-    try (Closeable ignored = skipNormalize()) {
-      return toString();
-    }
-  }
-
   /** Returns the number of nodes in this expression.
    *
    * <p>Leaf nodes, such as {@link RexInputRef} or {@link RexLiteral}, have
diff --git a/core/src/main/java/org/apache/calcite/rex/RexOver.java b/core/src/main/java/org/apache/calcite/rex/RexOver.java
index cfdd6c2..e90fa3c 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexOver.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexOver.java
@@ -196,4 +196,25 @@ public class RexOver extends RexCall {
       throw OverFound.INSTANCE;
     }
   }
+
+  @Override public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    RexOver rexOver = (RexOver) o;
+    return distinct == rexOver.distinct
+        && ignoreNulls == rexOver.ignoreNulls
+        && window.equals(rexOver.window)
+        && op.allowsFraming() == rexOver.op.allowsFraming();
+  }
+
+  @Override public int hashCode() {
+    return Objects.hash(super.hashCode(), window, distinct, ignoreNulls, op.allowsFraming());
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rex/RexSubQuery.java b/core/src/main/java/org/apache/calcite/rex/RexSubQuery.java
index 5a081dc..d702387 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSubQuery.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSubQuery.java
@@ -136,4 +136,14 @@ public class RexSubQuery extends RexCall {
   public RexSubQuery clone(RelNode rel) {
     return new RexSubQuery(type, getOperator(), operands, rel);
   }
+
+  @Override public boolean equals(Object obj) {
+    return obj == this
+        || obj instanceof RexCall
+        && toString().equals(obj.toString());
+  }
+
+  @Override public int hashCode() {
+    return toString().hashCode();
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlKind.java b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
index b3e42a8..f98a0c2 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlKind.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
@@ -16,6 +16,8 @@
  */
 package org.apache.calcite.sql;
 
+import com.google.common.collect.Sets;
+
 import org.apiguardian.api.API;
 
 import java.util.Collection;
@@ -1373,6 +1375,23 @@ public enum SqlKind {
       EnumSet.of(
           PLUS, TIMES);
 
+  /**
+   * Simple binary operators are those operators which expects operands from the same Domain.
+   *
+   * <p>Example: simple comparisions ({@code =}, {@code <}).
+   *
+   * <p>Note: it does not contain {@code IN} because that is defined on D x D^n.
+   */
+  @API(since = "1.24", status = API.Status.EXPERIMENTAL)
+  public static final Set<SqlKind> SIMPLE_BINARY_OPS;
+
+  static {
+    EnumSet<SqlKind> kinds = EnumSet.of(SqlKind.PLUS, SqlKind.MINUS, SqlKind.TIMES, SqlKind.DIVIDE);
+    kinds.addAll(SqlKind.COMPARISON);
+    kinds.remove(SqlKind.IN);
+    SIMPLE_BINARY_OPS = Sets.immutableEnumSet(kinds);
+  }
+
   /** Lower-case name. */
   public final String lowerName = name().toLowerCase(Locale.ROOT);
   public final String sql;
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
index e026b95..3a6027d 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
@@ -2595,4 +2595,24 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
     }
   }
 
+  /** Returns the binary operator that corresponds to this operator but in the opposite
+   * direction. Or returns this, if its kind is not reversible.
+   *
+   * <p>For example, {@code reverse(GREATER_THAN)} returns {@link #LESS_THAN}.
+   */
+  public static SqlOperator reverse(SqlOperator operator) {
+    switch (operator.getKind()) {
+    case GREATER_THAN:
+      return LESS_THAN;
+    case GREATER_THAN_OR_EQUAL:
+      return LESS_THAN_OR_EQUAL;
+    case LESS_THAN:
+      return GREATER_THAN;
+    case LESS_THAN_OR_EQUAL:
+      return GREATER_THAN_OR_EQUAL;
+    default:
+      return operator;
+    }
+  }
+
 }
diff --git a/core/src/test/java/org/apache/calcite/materialize/LatticeSuggesterTest.java b/core/src/test/java/org/apache/calcite/materialize/LatticeSuggesterTest.java
index bc3cc8e..f56d49c 100644
--- a/core/src/test/java/org/apache/calcite/materialize/LatticeSuggesterTest.java
+++ b/core/src/test/java/org/apache/calcite/materialize/LatticeSuggesterTest.java
@@ -670,12 +670,12 @@ class LatticeSuggesterTest {
     t.addQuery(q0);
     assertThat(t.s.latticeMap.size(), is(1));
     assertThat(t.s.latticeMap.keySet().iterator().next(),
-        is("sales_fact_1997 (customer:+(2, $2)):[MIN(customer.fname)]"));
+        is("sales_fact_1997 (customer:+($2, 2)):[MIN(customer.fname)]"));
     assertThat(t.s.space.g.toString(),
         is("graph(vertices: [[foodmart, customer],"
             + " [foodmart, sales_fact_1997]], "
             + "edges: [Step([foodmart, sales_fact_1997],"
-            + " [foodmart, customer], +(2, $2):+(1, $0))])"));
+            + " [foodmart, customer], +($2, 2):+($0, 1))])"));
   }
 
   /** Tests that we can run the suggester against non-JDBC schemas.
diff --git a/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java b/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java
index d567ce1..5af0d27 100644
--- a/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java
@@ -389,7 +389,7 @@ class RelOptUtilTest {
             .toString()));
     assertThat(newJoin.getLeft(), is(instanceOf(Project.class)));
     Project leftInput = (Project) newJoin.getLeft();
-    assertThat(leftInput.getChildExps().get(empRow.getFieldCount()).toString(),
+    assertThat(leftInput.getProjects().get(empRow.getFieldCount()).toString(),
         is(relBuilder.call(SqlStdOperatorTable.PLUS, leftKeyInputRef, relBuilder.literal(1))
             .toString()));
   }
@@ -434,7 +434,7 @@ class RelOptUtilTest {
             .toString()));
     assertThat(newJoin.getLeft(), is(instanceOf(Project.class)));
     Project leftInput = (Project) newJoin.getLeft();
-    assertThat(leftInput.getChildExps().get(empRow.getFieldCount()).toString(),
+    assertThat(leftInput.getProjects().get(empRow.getFieldCount()).toString(),
         is(relBuilder.call(SqlStdOperatorTable.PLUS, leftKeyInputRef, relBuilder.literal(1))
             .toString()));
 
@@ -486,7 +486,7 @@ class RelOptUtilTest {
                 .toString()));
     assertThat(newJoin.getLeft(), is(instanceOf(Project.class)));
     Project leftInput = (Project) newJoin.getLeft();
-    assertThat(leftInput.getChildExps().get(empRow.getFieldCount()).toString(),
+    assertThat(leftInput.getProjects().get(empRow.getFieldCount()).toString(),
         is(relBuilder.call(SqlStdOperatorTable.PLUS, leftKeyInputRef, relBuilder.literal(1))
             .toString()));
   }
@@ -538,7 +538,7 @@ class RelOptUtilTest {
               .toString()));
     assertThat(newJoin.getLeft(), is(instanceOf(Project.class)));
     Project leftInput = (Project) newJoin.getLeft();
-    assertThat(leftInput.getChildExps().get(empRow.getFieldCount()).toString(),
+    assertThat(leftInput.getProjects().get(empRow.getFieldCount()).toString(),
         is(relBuilder.call(SqlStdOperatorTable.PLUS, leftKeyInputRef, relBuilder.literal(1))
             .toString()));
   }
diff --git a/core/src/test/java/org/apache/calcite/plan/volcano/TraitPropagationTest.java b/core/src/test/java/org/apache/calcite/plan/volcano/TraitPropagationTest.java
index b671282..809174b 100644
--- a/core/src/test/java/org/apache/calcite/plan/volcano/TraitPropagationTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/volcano/TraitPropagationTest.java
@@ -242,12 +242,12 @@ class TraitPropagationTest {
             RelTraitSet outcome = child.getTraitSet().replace(PHYSICAL);
             call.transformTo(
                 new PhysProj(rel.getCluster(), outcome, convert(child, outcome),
-                    rel.getChildExps(), rel.getRowType()));
+                    rel.getProjects(), rel.getRowType()));
           }
         }
       } else {
         call.transformTo(
-            PhysProj.create(input, rel.getChildExps(), rel.getRowType()));
+            PhysProj.create(input, rel.getProjects(), rel.getRowType()));
       }
 
     }
diff --git a/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java b/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
index d93a223..d98b2bf 100644
--- a/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
@@ -240,9 +240,9 @@ class VolcanoPlannerTest {
     assertThat(sort(buf),
         equalTo(
             sort(
-                "NoneSingleRel:Subset#0.NONE.[]",
-                "PhysSingleRel:Subset#0.PHYS.[0]",
-                "PhysSingleRel:Subset#0.PHYS.[]")));
+                "NoneSingleRel:RelSubset#0.NONE.[]",
+                "PhysSingleRel:RelSubset#0.PHYS.[0]",
+                "PhysSingleRel:RelSubset#0.PHYS.[]")));
   }
 
   private static <E extends Comparable> List<E> sort(List<E> list) {
@@ -281,8 +281,8 @@ class VolcanoPlannerTest {
     assertTrue(result instanceof PhysSingleRel);
     assertThat(sort(buf),
         equalTo(
-            sort("PhysSingleRel:Subset#0.PHYS.[]",
-            "PhysSingleRel:Subset#0.PHYS_3.[]")));
+            sort("PhysSingleRel:RelSubset#0.PHYS.[]",
+            "PhysSingleRel:RelSubset#0.PHYS_3.[]")));
   }
 
   /**
diff --git a/core/src/test/java/org/apache/calcite/rel/rules/DateRangeRulesTest.java b/core/src/test/java/org/apache/calcite/rel/rules/DateRangeRulesTest.java
index e729288..bf4ca05 100644
--- a/core/src/test/java/org/apache/calcite/rel/rules/DateRangeRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/rel/rules/DateRangeRulesTest.java
@@ -700,9 +700,9 @@ class DateRangeRulesTest {
   private void checkDateRange(Fixture f, RexNode e, String timeZone,
       Matcher<String> matcher, Matcher<String> simplifyMatcher) {
     e = DateRangeRules.replaceTimeUnits(f.rexBuilder, e, timeZone);
-    assertThat(e.toStringRaw(), matcher);
+    assertThat(e.toString(), matcher);
     final RexNode e2 = f.simplify.simplify(e);
-    assertThat(e2.toStringRaw(), simplifyMatcher);
+    assertThat(e2.toString(), simplifyMatcher);
   }
 
   /** Common expressions across tests. */
diff --git a/core/src/test/java/org/apache/calcite/rex/RexCallNormalizationTest.java b/core/src/test/java/org/apache/calcite/rex/RexCallNormalizationTest.java
index e1bcbe4..38d08c1 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexCallNormalizationTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexCallNormalizationTest.java
@@ -20,54 +20,40 @@ import org.junit.jupiter.api.Test;
 
 class RexCallNormalizationTest extends RexProgramTestBase {
   @Test void digestIsNormalized() {
-    final RexNode node = and(or(vBool(1), vBool()), vBool());
-    checkDigest(node, "AND(?0.bool0, OR(?0.bool0, ?0.bool1))");
-    checkRaw(node, "AND(OR(?0.bool1, ?0.bool0), ?0.bool0)");
+    final RexNode node = and(or(input(tBool(), 1), input(tBool(), 0)), input(tBool(), 0));
+    checkDigest(node, "AND(OR($0, $1), $0)");
 
-    checkDigest(eq(vVarchar(), literal("0123456789012345")),
-        "=(?0.varchar0, '0123456789012345')");
-    checkDigest(eq(vVarchar(), literal("01")), "=('01', ?0.varchar0)");
-  }
-
-  @Test void skipNormalizationWorks() {
-    final RexNode node = and(or(vBool(1), vBool()), vBool());
-    try (RexNode.Closeable ignored = RexNode.skipNormalize()) {
-      checkDigest(node, "AND(OR(?0.bool1, ?0.bool0), ?0.bool0)");
-      checkRaw(node, "AND(OR(?0.bool1, ?0.bool0), ?0.bool0)");
-    }
-  }
-
-  @Test void skipNormalizeWorks() {
-    checkDigest(and(or(vBool(1), vBool()), vBool()),
-        "AND(?0.bool0, OR(?0.bool0, ?0.bool1))");
+    checkDigest(eq(input(tVarchar(), 0), literal("0123456789012345")),
+        "=($0, '0123456789012345')");
+    checkDigest(eq(input(tVarchar(), 0), literal("01")), "=($0, '01')");
   }
 
   @Test void reversibleSameArgOpsNormalizedToLess() {
-    checkDigest(lt(vBool(), vBool()), "<(?0.bool0, ?0.bool0)");
-    checkDigest(gt(vBool(), vBool()), "<(?0.bool0, ?0.bool0)");
-    checkDigest(le(vBool(), vBool()), "<=(?0.bool0, ?0.bool0)");
-    checkDigest(ge(vBool(), vBool()), "<=(?0.bool0, ?0.bool0)");
+    checkDigest(lt(input(tBool(), 0), input(tBool(), 0)), "<($0, $0)");
+    checkDigest(gt(input(tBool(), 0), input(tBool(), 0)), "<($0, $0)");
+    checkDigest(le(input(tBool(), 0), input(tBool(), 0)), "<=($0, $0)");
+    checkDigest(ge(input(tBool(), 0), input(tBool(), 0)), "<=($0, $0)");
   }
 
   @Test void reversibleDifferentArgTypesShouldNotBeShuffled() {
-    checkDigest(plus(vSmallInt(), vInt()), "+(?0.smallint0, ?0.int0)");
-    checkDigest(plus(vInt(), vSmallInt()), "+(?0.int0, ?0.smallint0)");
-    checkDigest(mul(vSmallInt(), vInt()), "*(?0.smallint0, ?0.int0)");
-    checkDigest(mul(vInt(), vSmallInt()), "*(?0.int0, ?0.smallint0)");
+    checkDigest(plus(input(tSmallInt(), 0), input(tInt(), 1)), "+($0, $1)");
+    checkDigest(plus(input(tInt(), 0), input(tSmallInt(), 1)), "+($0, $1)");
+    checkDigest(mul(input(tSmallInt(), 0), input(tInt(), 1)), "*($0, $1)");
+    checkDigest(mul(input(tInt(), 0), input(tSmallInt(), 1)), "*($0, $1)");
   }
 
   @Test void reversibleDifferentNullabilityArgsAreNormalized() {
-    checkDigest(plus(vIntNotNull(), vInt()), "+(?0.int0, ?0.notNullInt0)");
-    checkDigest(plus(vInt(), vIntNotNull()), "+(?0.int0, ?0.notNullInt0)");
-    checkDigest(mul(vIntNotNull(), vInt()), "*(?0.int0, ?0.notNullInt0)");
-    checkDigest(mul(vInt(), vIntNotNull()), "*(?0.int0, ?0.notNullInt0)");
+    checkDigest(plus(input(tInt(false), 1), input(tInt(), 0)), "+($0, $1)");
+    checkDigest(plus(input(tInt(), 1), input(tInt(false), 0)), "+($0, $1)");
+    checkDigest(mul(input(tInt(false), 1), input(tInt(), 0)), "*($0, $1)");
+    checkDigest(mul(input(tInt(), 1), input(tInt(false), 0)), "*($0, $1)");
   }
 
   @Test void symmetricalDifferentArgOps() {
     for (int i = 0; i < 2; i++) {
       int j = 1 - i;
-      checkDigest(eq(vBool(i), vBool(j)), "=(?0.bool0, ?0.bool1)");
-      checkDigest(ne(vBool(i), vBool(j)), "<>(?0.bool0, ?0.bool1)");
+      checkDigest(eq(input(tBool(), i), input(tBool(), j)), "=($0, $1)");
+      checkDigest(ne(input(tBool(), i), input(tBool(), j)), "<>($0, $1)");
     }
   }
 
@@ -75,25 +61,25 @@ class RexCallNormalizationTest extends RexProgramTestBase {
     for (int i = 0; i < 2; i++) {
       int j = 1 - i;
       checkDigest(
-          lt(vBool(i), vBool(j)),
+          lt(input(tBool(), i), input(tBool(), j)),
           i < j
-              ? "<(?0.bool0, ?0.bool1)"
-              : ">(?0.bool0, ?0.bool1)");
+              ? "<($0, $1)"
+              : ">($0, $1)");
       checkDigest(
-          le(vBool(i), vBool(j)),
+          le(input(tBool(), i), input(tBool(), j)),
           i < j
-              ? "<=(?0.bool0, ?0.bool1)"
-              : ">=(?0.bool0, ?0.bool1)");
+              ? "<=($0, $1)"
+              : ">=($0, $1)");
       checkDigest(
-          gt(vBool(i), vBool(j)),
+          gt(input(tBool(), i), input(tBool(), j)),
           i < j
-              ? ">(?0.bool0, ?0.bool1)"
-              : "<(?0.bool0, ?0.bool1)");
+              ? ">($0, $1)"
+              : "<($0, $1)");
       checkDigest(
-          ge(vBool(i), vBool(j)),
+          ge(input(tBool(), i), input(tBool(), j)),
           i < j
-              ? ">=(?0.bool0, ?0.bool1)"
-              : "<=(?0.bool0, ?0.bool1)");
+              ? ">=($0, $1)"
+              : "<=($0, $1)");
     }
   }
 }
diff --git a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
index 53676e4..a5d14c8 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
@@ -741,7 +741,7 @@ class RexProgramTest extends RexProgramTestBase {
     final int nodeCount = cnf.nodeCount();
     assertThat((n + 1) * (int) Math.pow(2, n) + 1, equalTo(nodeCount));
     if (n == 3) {
-      assertThat(cnf.toStringRaw(),
+      assertThat(cnf.toString(),
           equalTo("AND(OR(?0.x0, ?0.x1, ?0.x2), OR(?0.x0, ?0.x1, ?0.y2),"
               + " OR(?0.x0, ?0.y1, ?0.x2), OR(?0.x0, ?0.y1, ?0.y2),"
               + " OR(?0.y0, ?0.x1, ?0.x2), OR(?0.y0, ?0.x1, ?0.y2),"
@@ -1104,7 +1104,7 @@ class RexProgramTest extends RexProgramTestBase {
     // as previous, using simplifyFilterPredicates
     assertThat(simplify
             .simplifyFilterPredicates(args)
-            .toStringRaw(),
+            .toString(),
         equalTo("AND(=(?0.a, 1), =(?0.b, 1))"));
 
     // "a = 1 and a = 10" is always false
@@ -1410,7 +1410,7 @@ class RexProgramTest extends RexProgramTestBase {
     // TODO: "b is not unknown" would be the best simplification.
     final RexNode simplified =
         this.simplify.simplifyUnknownAs(neOrEq, RexUnknownAs.UNKNOWN);
-    assertThat(simplified.toStringRaw(),
+    assertThat(simplified.toString(),
         equalTo("OR(<>(?0.b, 1), =(?0.b, 1))"));
 
     // "a is null or a is not null" ==> "true"
@@ -2171,7 +2171,7 @@ class RexProgramTest extends RexProgramTestBase {
 
   private void assertTypeAndToString(
       RexNode rexNode, String representation, String type) {
-    assertEquals(representation, rexNode.toStringRaw());
+    assertEquals(representation, rexNode.toString());
     assertEquals(type, rexNode.getType().toString()
         + (rexNode.getType().isNullable() ? "" : " NOT NULL"), "type of " + rexNode);
   }
diff --git a/core/src/test/java/org/apache/calcite/rex/RexProgramTestBase.java b/core/src/test/java/org/apache/calcite/rex/RexProgramTestBase.java
index 140fb11..c3a634c 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramTestBase.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTestBase.java
@@ -30,32 +30,27 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 class RexProgramTestBase extends RexProgramBuilderBase {
 
   protected void checkDigest(RexNode node, String expected) {
-    assertEquals(expected, node.toString(), () -> "Digest of " + node.toStringRaw());
-  }
-
-  protected void checkRaw(RexNode node, String expected) {
-    assertEquals(expected, node.toStringRaw(),
-        () -> "Raw representation of node with digest " + node);
+    assertEquals(expected, node.toString(), () -> "Digest of " + node.toString());
   }
 
   protected void checkCnf(RexNode node, String expected) {
     assertThat("RexUtil.toCnf(rexBuilder, " + node + ")",
-        RexUtil.toCnf(rexBuilder, node).toStringRaw(), equalTo(expected));
+        RexUtil.toCnf(rexBuilder, node).toString(), equalTo(expected));
   }
 
   protected void checkThresholdCnf(RexNode node, int threshold, String expected) {
     assertThat("RexUtil.toCnf(rexBuilder, threshold=" + threshold + " , " + node + ")",
-        RexUtil.toCnf(rexBuilder, threshold, node).toStringRaw(),
+        RexUtil.toCnf(rexBuilder, threshold, node).toString(),
         equalTo(expected));
   }
 
   protected void checkPullFactorsUnchanged(RexNode node) {
-    checkPullFactors(node, node.toStringRaw());
+    checkPullFactors(node, node.toString());
   }
 
   protected void checkPullFactors(RexNode node, String expected) {
     assertThat("RexUtil.pullFactors(rexBuilder, " + node + ")",
-        RexUtil.pullFactors(rexBuilder, node).toStringRaw(),
+        RexUtil.pullFactors(rexBuilder, node).toString(),
         equalTo(expected));
   }
 
@@ -69,7 +64,7 @@ class RexProgramTestBase extends RexProgramBuilderBase {
     String actual;
     if (node.isA(SqlKind.CAST) || node.isA(SqlKind.NEW_SPECIFICATION)) {
       // toString contains type (see RexCall.toString)
-      actual = node.toStringRaw();
+      actual = node.toString();
     } else {
       actual = node + ":" + node.getType() + (node.getType().isNullable() ? "" : " NOT NULL");
     }
@@ -78,17 +73,17 @@ class RexProgramTestBase extends RexProgramBuilderBase {
 
   /** Simplifies an expression and checks that the result is as expected. */
   protected void checkSimplify(RexNode node, String expected) {
-    final String nodeString = node.toStringRaw();
+    final String nodeString = node.toString();
     checkSimplify3_(node, expected, expected, expected);
     if (expected.equals(nodeString)) {
-      throw new AssertionError("expected == node.toStringRaw(); "
+      throw new AssertionError("expected == node.toString(); "
           + "use checkSimplifyUnchanged");
     }
   }
 
   /** Simplifies an expression and checks that the result is unchanged. */
   protected void checkSimplifyUnchanged(RexNode node) {
-    final String expected = node.toStringRaw();
+    final String expected = node.toString();
     checkSimplify3_(node, expected, expected, expected);
   }
 
@@ -126,16 +121,16 @@ class RexProgramTestBase extends RexProgramBuilderBase {
     final RexNode simplified =
         simplify.simplifyUnknownAs(node, RexUnknownAs.UNKNOWN);
     assertThat("simplify(unknown as unknown): " + node,
-        simplified.toStringRaw(), equalTo(expected));
+        simplified.toString(), equalTo(expected));
     if (node.getType().getSqlTypeName() == SqlTypeName.BOOLEAN) {
       final RexNode simplified2 =
           simplify.simplifyUnknownAs(node, RexUnknownAs.FALSE);
       assertThat("simplify(unknown as false): " + node,
-          simplified2.toStringRaw(), equalTo(expectedFalse));
+          simplified2.toString(), equalTo(expectedFalse));
       final RexNode simplified3 =
           simplify.simplifyUnknownAs(node, RexUnknownAs.TRUE);
       assertThat("simplify(unknown as true): " + node,
-          simplified3.toStringRaw(), equalTo(expectedTrue));
+          simplified3.toString(), equalTo(expectedTrue));
     } else {
       assertThat("node type is not BOOLEAN, so <<expectedFalse>> should match <<expected>>",
           expectedFalse, is(expected));
@@ -147,7 +142,7 @@ class RexProgramTestBase extends RexProgramBuilderBase {
   protected void checkSimplifyFilter(RexNode node, String expected) {
     final RexNode simplified =
         this.simplify.simplifyUnknownAs(node, RexUnknownAs.FALSE);
-    assertThat(simplified.toStringRaw(), equalTo(expected));
+    assertThat(simplified.toString(), equalTo(expected));
   }
 
   protected void checkSimplifyFilter(RexNode node, RelOptPredicateList predicates,
@@ -155,7 +150,7 @@ class RexProgramTestBase extends RexProgramBuilderBase {
     final RexNode simplified =
         simplify.withPredicates(predicates)
             .simplifyUnknownAs(node, RexUnknownAs.FALSE);
-    assertThat(simplified.toStringRaw(), equalTo(expected));
+    assertThat(simplified.toString(), equalTo(expected));
   }
 
   /** Checks that {@link RexNode#isAlwaysTrue()},
@@ -163,11 +158,11 @@ class RexProgramTestBase extends RexProgramBuilderBase {
    * an expression reduces to true or false. */
   protected void checkIs(RexNode e, boolean expected) {
     assertThat(
-        "isAlwaysTrue() of expression: " + e.toStringRaw(), e.isAlwaysTrue(), is(expected));
+        "isAlwaysTrue() of expression: " + e.toString(), e.isAlwaysTrue(), is(expected));
     assertThat(
-        "isAlwaysFalse() of expression: " + e.toStringRaw(), e.isAlwaysFalse(), is(!expected));
+        "isAlwaysFalse() of expression: " + e.toString(), e.isAlwaysFalse(), is(!expected));
     assertThat(
-        "Simplification is not using isAlwaysX informations", simplify(e).toStringRaw(),
+        "Simplification is not using isAlwaysX informations", simplify(e).toString(),
         is(expected ? "true" : "false"));
   }
 
diff --git a/core/src/test/java/org/apache/calcite/rex/RexSqlStandardConvertletTableTest.java b/core/src/test/java/org/apache/calcite/rex/RexSqlStandardConvertletTableTest.java
index 8ad8357..f5e235b 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexSqlStandardConvertletTableTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexSqlStandardConvertletTableTest.java
@@ -44,7 +44,7 @@ class RexSqlStandardConvertletTableTest extends SqlToRelTestBase {
   @Test void testCoalesce() {
     final Project project = (Project) convertSqlToRel(
             "SELECT COALESCE(NULL, 'a')", false);
-    final RexNode rex = project.getChildExps().get(0);
+    final RexNode rex = project.getProjects().get(0);
     final RexToSqlNodeConverter rexToSqlNodeConverter = rexToSqlNodeConverter();
     final SqlNode convertedSql = rexToSqlNodeConverter.convertNode(rex);
     assertEquals(
@@ -56,7 +56,7 @@ class RexSqlStandardConvertletTableTest extends SqlToRelTestBase {
     final Project project =
             (Project) convertSqlToRel(
                     "SELECT CASE NULL WHEN NULL THEN NULL ELSE 'a' END", false);
-    final RexNode rex = project.getChildExps().get(0);
+    final RexNode rex = project.getProjects().get(0);
     final RexToSqlNodeConverter rexToSqlNodeConverter = rexToSqlNodeConverter();
     final SqlNode convertedSql = rexToSqlNodeConverter.convertNode(rex);
     assertEquals(
@@ -67,7 +67,7 @@ class RexSqlStandardConvertletTableTest extends SqlToRelTestBase {
   @Test void testCaseNoValue() {
     final Project project = (Project) convertSqlToRel(
             "SELECT CASE WHEN NULL IS NULL THEN NULL ELSE 'a' END", false);
-    final RexNode rex = project.getChildExps().get(0);
+    final RexNode rex = project.getProjects().get(0);
     final RexToSqlNodeConverter rexToSqlNodeConverter = rexToSqlNodeConverter();
     final SqlNode convertedSql = rexToSqlNodeConverter.convertNode(rex);
     assertEquals(
diff --git a/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java b/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java
index 5b5fcc2..a4269c5 100644
--- a/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java
+++ b/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java
@@ -150,8 +150,10 @@ class HepPlannerTest extends RelOptTestBase {
     // Bad digest includes full tree like rel#66:LogicalProject(input=rel#64:LogicalUnion(...))
     // So the assertion is to ensure digest includes LogicalUnion exactly once
 
-    assertIncludesExactlyOnce("best.getDescription()", best.toString(), "LogicalUnion");
-    assertIncludesExactlyOnce("best.getDigest()", best.getDigest(), "LogicalUnion");
+    assertIncludesExactlyOnce("best.getDescription()",
+        best.toString(), "LogicalUnion");
+    assertIncludesExactlyOnce("best.getDigest()",
+        best.getDigest().toString(), "LogicalUnion");
   }
 
   private void assertIncludesExactlyOnce(String message, String digest, String substring) {
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
index 93d9225..d92a451 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
@@ -249,7 +249,7 @@ class JdbcAdapterTest {
             + "on s.losal <= e.sal and s.hisal >= e.sal")
         .explainContains("PLAN=JdbcToEnumerableConverter\n"
             + "  JdbcProject(EMPNO=[$0], ENAME=[$1], GRADE=[$3])\n"
-            + "    JdbcJoin(condition=[AND(<=($4, $2), >=($5, $2))], joinType=[inner])\n"
+            + "    JdbcJoin(condition=[AND(>=($2, $4), <=($2, $5))], joinType=[inner])\n"
             + "      JdbcProject(EMPNO=[$0], ENAME=[$1], SAL=[$5])\n"
             + "        JdbcTableScan(table=[[SCOTT, EMP]])\n"
             + "      JdbcTableScan(table=[[SCOTT, SALGRADE]])")
@@ -292,7 +292,7 @@ class JdbcAdapterTest {
             + "e.mgr = m.empno and (e.sal > m.sal or m.hiredate > e.hiredate)")
         .explainContains("PLAN=JdbcToEnumerableConverter\n"
             + "  JdbcProject(EMPNO=[$0], ENAME=[$1], EMPNO0=[$0], ENAME0=[$1])\n"
-            + "    JdbcJoin(condition=[AND(=($2, $5), OR(>($4, $7), >($6, $3)))], joinType=[inner])\n"
+            + "    JdbcJoin(condition=[AND(=($2, $5), OR(>($4, $7), <($3, $6)))], joinType=[inner])\n"
             + "      JdbcProject(EMPNO=[$0], ENAME=[$1], MGR=[$3], HIREDATE=[$4], SAL=[$5])\n"
             + "        JdbcTableScan(table=[[SCOTT, EMP]])\n"
             + "      JdbcProject(EMPNO=[$0], HIREDATE=[$4], SAL=[$5])\n"
diff --git a/core/src/test/java/org/apache/calcite/test/LatticeTest.java b/core/src/test/java/org/apache/calcite/test/LatticeTest.java
index 1a68667..a6591d3 100644
--- a/core/src/test/java/org/apache/calcite/test/LatticeTest.java
+++ b/core/src/test/java/org/apache/calcite/test/LatticeTest.java
@@ -239,9 +239,9 @@ class LatticeTest {
           final Map.Entry<String, CalciteSchema.LatticeEntry> entry =
               adhoc.unwrap(CalciteSchema.class).getLatticeMap().firstEntry();
           final Lattice lattice = entry.getValue().getLattice();
-          assertThat(lattice.firstColumn("S"), is(10));
-          assertThat(lattice.firstColumn("P"), is(18));
-          assertThat(lattice.firstColumn("T"), is(0));
+          assertThat(lattice.firstColumn("S"), is(0));
+          assertThat(lattice.firstColumn("P"), is(8));
+          assertThat(lattice.firstColumn("T"), is(23));
           assertThat(lattice.firstColumn("PC"), is(-1));
           assertThat(lattice.defaultMeasures.size(), is(1));
           assertThat(lattice.rootNode.descendants.size(), is(3));
diff --git a/core/src/test/java/org/apache/calcite/test/MaterializedViewRelOptRulesTest.java b/core/src/test/java/org/apache/calcite/test/MaterializedViewRelOptRulesTest.java
index 8e45d23..58019b4 100644
--- a/core/src/test/java/org/apache/calcite/test/MaterializedViewRelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/MaterializedViewRelOptRulesTest.java
@@ -821,8 +821,7 @@ public class MaterializedViewRelOptRulesTest extends AbstractMaterializedViewTes
         .withChecker(
             resultContains(""
             + "EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):JavaType(int) NOT NULL], "
-            + "expr#2=[1], expr#3=[CAST($t1):INTEGER NOT NULL], expr#4=[=($t2, $t3)], "
-            + "EXPR$0=[$t1], $condition=[$t4])\n"
+            + "expr#2=[1], expr#3=[=($t2, $t0)], EXPR$0=[$t1], $condition=[$t3])\n"
             + "  EnumerableTableScan(table=[[hr, MV0]])"))
         .ok();
   }
diff --git a/core/src/test/java/org/apache/calcite/test/MaterializedViewSubstitutionVisitorTest.java b/core/src/test/java/org/apache/calcite/test/MaterializedViewSubstitutionVisitorTest.java
index e599382..2dd25cd 100644
--- a/core/src/test/java/org/apache/calcite/test/MaterializedViewSubstitutionVisitorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/MaterializedViewSubstitutionVisitorTest.java
@@ -1146,7 +1146,7 @@ public class MaterializedViewSubstitutionVisitorTest extends AbstractMaterialize
   private void checkSatisfiable(RexNode e, String s) {
     assertTrue(SubstitutionVisitor.mayBeSatisfiable(e));
     final RexNode simple = simplify.simplifyUnknownAsFalse(e);
-    assertEquals(s, simple.toStringRaw());
+    assertEquals(s, simple.toString());
   }
 
   @Test void testSplitFilter() {
@@ -1219,7 +1219,7 @@ public class MaterializedViewSubstitutionVisitorTest extends AbstractMaterialize
     newFilter = SubstitutionVisitor.splitFilter(simplify,
         x_eq_1,
         rexBuilder.makeCall(SqlStdOperatorTable.OR, x_eq_1, z_eq_3));
-    assertThat(newFilter.toStringRaw(), equalTo("=($0, 1)"));
+    assertThat(newFilter.toString(), equalTo("=($0, 1)"));
 
     // 2b.
     //   condition: x = 1 or y = 2
@@ -1229,7 +1229,7 @@ public class MaterializedViewSubstitutionVisitorTest extends AbstractMaterialize
     newFilter = SubstitutionVisitor.splitFilter(simplify,
         rexBuilder.makeCall(SqlStdOperatorTable.OR, x_eq_1, y_eq_2),
         rexBuilder.makeCall(SqlStdOperatorTable.OR, x_eq_1, y_eq_2, z_eq_3));
-    assertThat(newFilter.toStringRaw(), equalTo("OR(=($0, 1), =($1, 2))"));
+    assertThat(newFilter.toString(), equalTo("OR(=($0, 1), =($1, 2))"));
 
     // 2c.
     //   condition: x = 1
@@ -1239,7 +1239,7 @@ public class MaterializedViewSubstitutionVisitorTest extends AbstractMaterialize
     newFilter = SubstitutionVisitor.splitFilter(simplify,
         x_eq_1,
         rexBuilder.makeCall(SqlStdOperatorTable.OR, x_eq_1, y_eq_2, z_eq_3));
-    assertThat(newFilter.toStringRaw(),
+    assertThat(newFilter.toString(),
         equalTo("=($0, 1)"));
 
     // 2d.
@@ -1287,7 +1287,7 @@ public class MaterializedViewSubstitutionVisitorTest extends AbstractMaterialize
     newFilter = SubstitutionVisitor.splitFilter(simplify,
         rexBuilder.makeCall(SqlStdOperatorTable.AND, x_eq_1, y_eq_2),
         y_eq_2);
-    assertThat(newFilter.toStringRaw(), equalTo("=($0, 1)"));
+    assertThat(newFilter.toString(), equalTo("=($0, 1)"));
 
     // Example 5.
     //   condition: x = 1
diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
index ac45e49..d00228f 100644
--- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
@@ -1978,7 +1978,7 @@ public class RelBuilderTest {
         + "  LogicalTableScan(table=[[scott, DEPT]])\n";
     final String expectedWithoutSimplify = ""
         + "LogicalJoin(condition=[OR(null:NULL, "
-        + "AND(=($7, 1), =($7, 2), =($8, $7)))], joinType=[inner])\n"
+        + "AND(=($7, 1), =($7, 2), =($7, $8)))], joinType=[inner])\n"
         + "  LogicalTableScan(table=[[scott, EMP]])\n"
         + "  LogicalTableScan(table=[[scott, DEPT]])\n";
     assertThat(f.apply(createBuilder()), hasTree(expected));
@@ -2151,7 +2151,7 @@ public class RelBuilderTest {
                     builder.field("e", "MGR")))
             .build();
     final String expected = ""
-        + "LogicalFilter(condition=[AND(=($7, $16), =($8, $3))])\n"
+        + "LogicalFilter(condition=[AND(=($7, $16), =($3, $8))])\n"
         + "  LogicalJoin(condition=[true], joinType=[inner])\n"
         + "    LogicalTableScan(table=[[scott, EMP]])\n"
         + "    LogicalJoin(condition=[true], joinType=[inner])\n"
@@ -3544,7 +3544,7 @@ public class RelBuilderTest {
             builder.field("EMPNO"),
             builder.literal(1),
             builder.literal(5));
-    assertThat(call.toStringRaw(), is("BETWEEN ASYMMETRIC($0, 1, 5)"));
+    assertThat(call.toString(), is("BETWEEN ASYMMETRIC($0, 1, 5)"));
   }
 
   /** Test case for
diff --git a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
index 962e484..74abc71 100644
--- a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
@@ -2037,7 +2037,7 @@ public class RelMetadataTest extends SqlToRelTestBase {
         + "select empno, comm, deptno from emp where empno=1 and comm=4");
     final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
     assertThat(mq.getPulledUpPredicates(rel).pulledUpPredicates,
-        sortsAs("[=($0, 1), OR(AND(=($2, 3), =($1, 2)), =($1, 4))]"));
+        sortsAs("[=($0, 1), OR(AND(=($1, 2), =($2, 3)), =($1, 4))]"));
 
   }
 
@@ -3066,14 +3066,12 @@ public class RelMetadataTest extends SqlToRelTestBase {
    */
   public static <T> Matcher<Iterable<? extends T>> sortsAs(final String value) {
     return Matchers.compose(equalTo(value), item -> {
-      try (RexNode.Closeable ignored = RexNode.skipNormalize()) {
-        final List<String> strings = new ArrayList<>();
-        for (T t : item) {
-          strings.add(t.toString());
-        }
-        Collections.sort(strings);
-        return strings.toString();
+      final List<String> strings = new ArrayList<>();
+      for (T t : item) {
+        strings.add(t.toString());
       }
+      Collections.sort(strings);
+      return strings.toString();
     });
   }
 
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 ec2d30d..a5abb11 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -6848,7 +6848,7 @@ class RelOptRulesTest extends RelOptTestBase {
       final LogicalProject logicalProject = call.rel(0);
       final RelNode input = logicalProject.getInput();
       final MyProject myProject = new MyProject(input.getCluster(), input.getTraitSet(), input,
-          logicalProject.getChildExps(), logicalProject.getRowType());
+          logicalProject.getProjects(), logicalProject.getRowType());
       call.transformTo(myProject);
     }
   }
diff --git a/core/src/test/java/org/apache/calcite/test/RexTransformerTest.java b/core/src/test/java/org/apache/calcite/test/RexTransformerTest.java
index b7b6104..978d1b3 100644
--- a/core/src/test/java/org/apache/calcite/test/RexTransformerTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RexTransformerTest.java
@@ -114,7 +114,7 @@ class RexTransformerTest {
 
     RexTransformer transformer = new RexTransformer(root, rexBuilder);
     RexNode result = transformer.transformNullSemantics();
-    String actual = result.toStringRaw();
+    String actual = result.toString();
     if (!actual.equals(expected)) {
       String msg =
           "\nExpected=<" + expected + ">\n  Actual=<" + actual + ">";
@@ -374,7 +374,7 @@ class RexTransformerTest {
         null,
         null);
 
-    assertThat(remaining.toStringRaw(), is("<>($0, $9)"));
+    assertThat(remaining.toString(), is("<>($0, $9)"));
     assertThat(leftJoinKeys.isEmpty(), is(true));
     assertThat(rightJoinKeys.isEmpty(), is(true));
   }
diff --git a/core/src/test/java/org/apache/calcite/test/SqlHintsConverterTest.java b/core/src/test/java/org/apache/calcite/test/SqlHintsConverterTest.java
index 4e7a3f6..bf9079a 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlHintsConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlHintsConverterTest.java
@@ -76,8 +76,6 @@ import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
 import org.apache.log4j.spi.LoggingEvent;
 
-import com.google.common.collect.ImmutableList;
-
 import org.junit.jupiter.api.Test;
 
 import java.util.ArrayList;
@@ -510,11 +508,11 @@ class SqlHintsConverterTest extends SqlToRelTestBase {
 
     public void onMatch(RelOptRuleCall call) {
       LogicalJoin join = call.rel(0);
-      assertThat(1, is(join.getHints().size()));
+      assertThat(join.getHints().size(), is(1));
       call.transformTo(
           LogicalJoin.create(join.getLeft(),
               join.getRight(),
-              ImmutableList.of(),
+              join.getHints(),
               join.getCondition(),
               join.getVariablesSet(),
               join.getJoinType()));
diff --git a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableCorrelateTest.java b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableCorrelateTest.java
index 11aa4aa..d0a65e9 100644
--- a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableCorrelateTest.java
+++ b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableCorrelateTest.java
@@ -96,11 +96,11 @@ class EnumerableCorrelateTest {
           planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
         })
         .explainContains(""
-            + "EnumerableCalc(expr#0..3=[{inputs}], empid=[$t1], name=[$t3])\n"
+            + "PLAN=EnumerableCalc(expr#0..3=[{inputs}], empid=[$t1], name=[$t3])\n"
             + "  EnumerableCorrelate(correlation=[$cor1], joinType=[inner], requiredColumns=[{0}])\n"
             + "    EnumerableAggregate(group=[{0}])\n"
             + "      EnumerableTableScan(table=[[s, depts]])\n"
-            + "    EnumerableCalc(expr#0..4=[{inputs}], expr#5=[$cor1], expr#6=[$t5.deptno], expr#7=[=($t1, $t6)], proj#0..2=[{exprs}], $condition=[$t7])\n"
+            + "    EnumerableCalc(expr#0..4=[{inputs}], expr#5=[$cor1], expr#6=[$t5.deptno], expr#7=[=($t6, $t1)], proj#0..2=[{exprs}], $condition=[$t7])\n"
             + "      EnumerableTableScan(table=[[s, emps]])")
         .returnsUnordered(
             "empid=100; name=Bill",
@@ -126,11 +126,11 @@ class EnumerableCorrelateTest {
           planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
         })
         .explainContains(""
-            + "EnumerableCalc(expr#0..3=[{inputs}], empid=[$t1], name=[$t3])\n"
+            + "PLAN=EnumerableCalc(expr#0..3=[{inputs}], empid=[$t1], name=[$t3])\n"
             + "  EnumerableCorrelate(correlation=[$cor1], joinType=[inner], requiredColumns=[{0}])\n"
             + "    EnumerableAggregate(group=[{0}])\n"
             + "      EnumerableTableScan(table=[[s, depts]])\n"
-            + "    EnumerableCalc(expr#0..4=[{inputs}], expr#5=[$cor1], expr#6=[$t5.deptno], expr#7=[=($t1, $t6)], expr#8=[100], expr#9=[>($t0, $t8)], expr#10=[AND($t7, $t9)], proj#0..2=[{exprs}], $condition=[$t10])\n"
+            + "    EnumerableCalc(expr#0..4=[{inputs}], expr#5=[$cor1], expr#6=[$t5.deptno], expr#7=[=($t6, $t1)], expr#8=[100], expr#9=[>($t0, $t8)], expr#10=[AND($t7, $t9)], proj#0..2=[{exprs}], $condition=[$t10])\n"
             + "      EnumerableTableScan(table=[[s, emps]])")
         .returnsUnordered(
             "empid=110; name=Theodore",
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 57fd8c4..e8c29b6 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -638,7 +638,7 @@ LogicalProject(MGR=[$3])
         </Resource>
         <Resource name="planMid">
             <![CDATA[
-LogicalProject(DEPTNO=[$0], EXPR$1=[OR(AND(IS NOT NULL($5), <>($2, 0)), AND(<($3, $2), null, <>($2, 0), IS NULL($5)))])
+LogicalProject(DEPTNO=[$0], EXPR$1=[OR(AND(IS NOT NULL($5), <>($2, 0)), AND(>($2, $3), null, <>($2, 0), IS NULL($5)))])
   LogicalJoin(condition=[=($1, $4)], joinType=[left])
     LogicalJoin(condition=[true], joinType=[inner])
       LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
@@ -652,7 +652,7 @@ LogicalProject(DEPTNO=[$0], EXPR$1=[OR(AND(IS NOT NULL($5), <>($2, 0)), AND(<($3
         </Resource>
         <Resource name="planAfter">
             <![CDATA[
-LogicalProject(DEPTNO=[$0], EXPR$1=[OR(AND(IS NOT NULL($5), <>($2, 0)), AND(<($3, $2), null, <>($2, 0), IS NULL($5)))])
+LogicalProject(DEPTNO=[$0], EXPR$1=[OR(AND(IS NOT NULL($5), <>($2, 0)), AND(>($2, $3), null, <>($2, 0), IS NULL($5)))])
   LogicalJoin(condition=[=($1, $4)], joinType=[left])
     LogicalJoin(condition=[true], joinType=[inner])
       LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
@@ -1837,7 +1837,7 @@ group by case when e.sal < 11 then 11 else -1 * e.sal end]]>
 LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
   LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
     LogicalProject(EXPR$1=[CASE(<($9, 11), 11, *(-1, $9))])
-      LogicalJoin(condition=[=($5, $0)], joinType=[left])
+      LogicalJoin(condition=[=($0, $5)], joinType=[left])
         LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
@@ -1847,7 +1847,7 @@ LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
 LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
   LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
     LogicalProject(EXPR$1=[CASE($2, 11, $3)])
-      LogicalJoin(condition=[=($1, $0)], joinType=[left])
+      LogicalJoin(condition=[=($0, $1)], joinType=[left])
         LogicalProject(ENAME=[$0])
           LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
         LogicalProject(ENAME=[$1], <=[<($5, 11)], *=[*(-1, $5)])
@@ -1866,7 +1866,7 @@ group by case when e.sal < 11 then -1 * e.sal else e.sal end]]>
 LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
   LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
     LogicalProject(EXPR$1=[CASE(<($9, 11), *(-1, $9), $9)])
-      LogicalJoin(condition=[=($5, $0)], joinType=[left])
+      LogicalJoin(condition=[=($0, $5)], joinType=[left])
         LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
@@ -1876,7 +1876,7 @@ LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
 LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
   LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
     LogicalProject(EXPR$1=[$2])
-      LogicalJoin(condition=[=($1, $0)], joinType=[left])
+      LogicalJoin(condition=[=($0, $1)], joinType=[left])
         LogicalProject(ENAME=[$0])
           LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
         LogicalProject(ENAME=[$1], CASE=[CASE(<($5, 11), *(-1, $5), $5)])
@@ -1983,7 +1983,7 @@ group by case when e.sal < 11 then 11 else -1 * e.sal end]]>
 LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
   LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
     LogicalProject(EXPR$1=[CASE(<($9, 11), 11, *(-1, $9))])
-      LogicalJoin(condition=[=($5, $0)], joinType=[right])
+      LogicalJoin(condition=[=($0, $5)], joinType=[right])
         LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
@@ -1993,7 +1993,7 @@ LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
 LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
   LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
     LogicalProject(EXPR$1=[$2])
-      LogicalJoin(condition=[=($1, $0)], joinType=[right])
+      LogicalJoin(condition=[=($0, $1)], joinType=[right])
         LogicalProject(ENAME=[$0])
           LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
         LogicalProject(ENAME=[$1], CASE=[CASE(<($5, 11), 11, *(-1, $5))])
@@ -2012,7 +2012,7 @@ group by case when e.sal < 11 then -1 * e.sal else e.sal end]]>
 LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
   LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
     LogicalProject(EXPR$1=[CASE(<($9, 11), *(-1, $9), $9)])
-      LogicalJoin(condition=[=($5, $0)], joinType=[right])
+      LogicalJoin(condition=[=($0, $5)], joinType=[right])
         LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
@@ -2022,7 +2022,7 @@ LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
 LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
   LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
     LogicalProject(EXPR$1=[$2])
-      LogicalJoin(condition=[=($1, $0)], joinType=[right])
+      LogicalJoin(condition=[=($0, $1)], joinType=[right])
         LogicalProject(ENAME=[$0])
           LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
         LogicalProject(ENAME=[$1], CASE=[CASE(<($5, 11), *(-1, $5), $5)])
@@ -3582,7 +3582,7 @@ join sales.emp e on e.deptno = d.deptno and d.deptno not in (4, 6)]]>
         <Resource name="planBefore">
             <![CDATA[
 LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], EMPNO0=[$9], ENAME0=[$10], JOB0=[$11], MGR0=[$12], HIREDATE0=[$13], SAL0=[$14], COMM0=[$15], DEPTNO0=[$16], SLACKER0=[$17])
-  LogicalJoin(condition=[AND(=($16, $7), NOT(OR(=($7, 4), =($7, 6))))], joinType=[inner])
+  LogicalJoin(condition=[AND(=($7, $16), NOT(OR(=($7, 4), =($7, 6))))], joinType=[inner])
     LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
@@ -3590,7 +3590,7 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$
         <Resource name="planAfter">
             <![CDATA[
 LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], EMPNO0=[$9], ENAME0=[$10], JOB0=[$11], MGR0=[$12], HIREDATE0=[$13], SAL0=[$14], COMM0=[$15], DEPTNO0=[$16], SLACKER0=[$17])
-  LogicalJoin(condition=[=($16, $7)], joinType=[inner])
+  LogicalJoin(condition=[=($7, $16)], joinType=[inner])
     LogicalFilter(condition=[AND(<>($7, 4), <>($7, 6))])
       LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalFilter(condition=[AND(<>($7, 4), <>($7, 6))])
@@ -3750,10 +3750,10 @@ LogicalProject(SAL=[$5])
     LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], SLACKER=[$8])
       LogicalFilter(condition=[AND(=($7, 20), >($5, 1000))])
         LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
-    LogicalFilter(condition=[=($1, $0)])
+    LogicalFilter(condition=[=($0, $1)])
       LogicalAggregate(group=[{0, 1}])
         LogicalProject(SAL=[$5], SAL0=[$8])
-          LogicalJoin(condition=[OR(=($8, $5), =($8, 4))], joinType=[inner])
+          LogicalJoin(condition=[OR(=($5, $8), =($8, 4))], joinType=[inner])
             LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], SLACKER=[$8])
               LogicalFilter(condition=[AND(=($7, 20), >($5, 1000))])
                 LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
@@ -6414,7 +6414,7 @@ group by e.job]]>
             <![CDATA[
 LogicalAggregate(group=[{2}])
   LogicalJoin(condition=[AND(=($2, $11), =($9, $12))], joinType=[full])
-    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, $0)])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($0, $7)])
       LogicalFilter(condition=[=($1, 'A')])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalProject(DEPTNO=[$0], NAME=[$1], $f2=[+($0, 5)])
@@ -6426,7 +6426,7 @@ LogicalAggregate(group=[{2}])
 LogicalAggregate(group=[{0}])
   LogicalJoin(condition=[AND(=($0, $2), =($1, $3))], joinType=[full])
     LogicalAggregate(group=[{2, 9}])
-      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, $0)])
+      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($0, $7)])
         LogicalFilter(condition=[=($1, 'A')])
           LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalAggregate(group=[{1, 2}])
@@ -6596,7 +6596,7 @@ group by e.job]]>
             <![CDATA[
 LogicalAggregate(group=[{2}])
   LogicalJoin(condition=[AND(=($2, $11), =($9, $12))], joinType=[left])
-    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, $0)])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($0, $7)])
       LogicalFilter(condition=[=($1, 'A')])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalProject(DEPTNO=[$0], NAME=[$1], $f2=[+($0, 5)])
@@ -6608,7 +6608,7 @@ LogicalAggregate(group=[{2}])
 LogicalAggregate(group=[{0}])
   LogicalJoin(condition=[AND(=($0, $2), =($1, $3))], joinType=[left])
     LogicalAggregate(group=[{2, 9}])
-      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, $0)])
+      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($0, $7)])
         LogicalFilter(condition=[=($1, 'A')])
           LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalAggregate(group=[{1, 2}])
@@ -6629,7 +6629,7 @@ group by d.name]]>
             <![CDATA[
 LogicalAggregate(group=[{11}])
   LogicalJoin(condition=[AND(=($2, $11), =($9, $12))], joinType=[left])
-    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, $0)])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($0, $7)])
       LogicalFilter(condition=[=($1, 'A')])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalProject(DEPTNO=[$0], NAME=[$1], $f2=[+($0, 5)])
@@ -6641,7 +6641,7 @@ LogicalAggregate(group=[{11}])
 LogicalAggregate(group=[{2}])
   LogicalJoin(condition=[AND(=($0, $2), =($1, $3))], joinType=[left])
     LogicalAggregate(group=[{2, 9}])
-      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, $0)])
+      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($0, $7)])
         LogicalFilter(condition=[=($1, 'A')])
           LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalAggregate(group=[{1, 2}])
@@ -6662,7 +6662,7 @@ group by e.job, d.name]]>
             <![CDATA[
 LogicalAggregate(group=[{2, 11}])
   LogicalJoin(condition=[AND(=($2, $11), =($9, $12))], joinType=[left])
-    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, $0)])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($0, $7)])
       LogicalFilter(condition=[=($1, 'A')])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalProject(DEPTNO=[$0], NAME=[$1], $f2=[+($0, 5)])
@@ -6674,7 +6674,7 @@ LogicalAggregate(group=[{2, 11}])
 LogicalAggregate(group=[{0, 2}])
   LogicalJoin(condition=[AND(=($0, $2), =($1, $3))], joinType=[left])
     LogicalAggregate(group=[{2, 9}])
-      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, $0)])
+      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($0, $7)])
         LogicalFilter(condition=[=($1, 'A')])
           LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalAggregate(group=[{1, 2}])
@@ -6743,7 +6743,7 @@ join sales.dept as d on e.empno = d.deptno and e.deptno + e.empno = d.deptno + 5
             <![CDATA[
 LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$10], NAME=[$11])
   LogicalJoin(condition=[AND(=($0, $10), =($9, $12))], joinType=[inner])
-    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, $0)])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($0, $7)])
       LogicalFilter(condition=[=($0, 10)])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalProject(DEPTNO=[$0], NAME=[$1], $f2=[+($0, 5)])
@@ -6754,7 +6754,7 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$
             <![CDATA[
 LogicalProject(EMPNO=[10], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[10], NAME=[$11])
   LogicalJoin(condition=[=($9, 15)], joinType=[inner])
-    LogicalProject(EMPNO=[10], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, 10)])
+    LogicalProject(EMPNO=[10], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+(10, $7)])
       LogicalFilter(condition=[=($0, 10)])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalProject(DEPTNO=[10], NAME=[$1], $f2=[15])
@@ -7868,13 +7868,13 @@ from emp]]>
             <![CDATA[
 LogicalProject($0=[$3], $1=[$4])
   LogicalWindow(window#0=[window(partition {1} order by [0] aggs [SUM($1), SUM($2)])])
-    LogicalProject(SAL=[$5], DEPTNO=[$7], $2=[+($7, $5)])
+    LogicalProject(SAL=[$5], DEPTNO=[$7], $2=[+($5, $7)])
       LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
         </Resource>
         <Resource name="planBefore">
             <![CDATA[
-LogicalProject(SUM1=[SUM($7) OVER (PARTITION BY $7 ORDER BY $5)], SUM2=[SUM(+($7, $5)) OVER (PARTITION BY $7 ORDER BY $5)])
+LogicalProject(SUM1=[SUM($7) OVER (PARTITION BY $7 ORDER BY $5)], SUM2=[SUM(+($5, $7)) OVER (PARTITION BY $7 ORDER BY $5)])
   LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
         </Resource>
@@ -7920,7 +7920,7 @@ group by e.job,d.name]]>
             <![CDATA[
 LogicalAggregate(group=[{2, 11}])
   LogicalJoin(condition=[AND(=($2, $11), =($9, $12))], joinType=[inner])
-    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, $0)])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($0, $7)])
       LogicalFilter(condition=[=($1, 'A')])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalProject(DEPTNO=[$0], NAME=[$1], $f2=[+($0, 5)])
@@ -7932,7 +7932,7 @@ LogicalAggregate(group=[{2, 11}])
 LogicalAggregate(group=[{0, 2}])
   LogicalJoin(condition=[AND(=($0, $2), =($1, $3))], joinType=[inner])
     LogicalAggregate(group=[{2, 9}])
-      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, $0)])
+      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($0, $7)])
         LogicalFilter(condition=[=($1, 'A')])
           LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalAggregate(group=[{1, 2}])
@@ -8466,7 +8466,7 @@ LogicalProject(EXPR$0=[1])
             <![CDATA[
 LogicalProject(EXPR$0=[1])
   LogicalProject(DEPTNO=[$9], NAME=[$10], EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO0=[$7], SLACKER=[$8])
-    LogicalJoin(condition=[=($9, $7)], joinType=[right])
+    LogicalJoin(condition=[=($7, $9)], joinType=[right])
       LogicalTableScan(table=[[CATALOG, SALES, EMP]])
       LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
 ]]>
@@ -9742,7 +9742,7 @@ RIGHT JOIN sales.emp e ON e.deptno = d.deptno]]>
         <Resource name="planBefore">
             <![CDATA[
 LogicalProject(DEPTNO=[$9])
-  LogicalJoin(condition=[=($9, $0)], joinType=[right])
+  LogicalJoin(condition=[=($0, $9)], joinType=[right])
     LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
     LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
@@ -9786,7 +9786,7 @@ RIGHT JOIN sales.emp e ON e.deptno = d.deptno]]>
         <Resource name="planBefore">
             <![CDATA[
 LogicalProject(DEPTNO=[$9], NAME=[$1])
-  LogicalJoin(condition=[=($9, $0)], joinType=[right])
+  LogicalJoin(condition=[=($0, $9)], joinType=[right])
     LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
     LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
@@ -11289,7 +11289,7 @@ LogicalProject(DEPTNO=[$1])
         </Resource>
         <Resource name="planAfter">
             <![CDATA[
-LogicalProject(EMPNO=[$0], D=[CASE(=($9, 0), false, IS NULL(CASE(true, CAST($7):INTEGER, null:INTEGER)), null:BOOLEAN, IS NOT NULL($12), true, <($10, $9), null:BOOLEAN, false)])
+LogicalProject(EMPNO=[$0], D=[CASE(=($9, 0), false, IS NULL(CASE(true, CAST($7):INTEGER, null:INTEGER)), null:BOOLEAN, IS NOT NULL($12), true, >($9, $10), null:BOOLEAN, false)])
   LogicalJoin(condition=[=(CASE(true, CAST($7):INTEGER, null:INTEGER), $11)], joinType=[left])
     LogicalJoin(condition=[true], joinType=[inner])
       LogicalTableScan(table=[[CATALOG, SALES, EMP]])
@@ -11337,7 +11337,7 @@ LogicalProject(EXPR$0=[CASE(true, CAST($7):INTEGER, null:INTEGER)])
             <![CDATA[
 LogicalProject(EMPNO=[$0])
   LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
-    LogicalFilter(condition=[<($0, CASE(OR(AND(IS NOT NULL($12), <>($9, 0)), AND(<($10, $9), null, <>($9, 0), IS NULL($12))), 10, AND(OR(IS NULL($12), =($9, 0)), OR(>=($10, $9), =($9, 0), IS NOT NULL($12))), 20, 30))])
+    LogicalFilter(condition=[<($0, CASE(OR(AND(IS NOT NULL($12), <>($9, 0)), AND(>($9, $10), null, <>($9, 0), IS NULL($12))), 10, AND(OR(IS NULL($12), =($9, 0)), OR(<=($9, $10), =($9, 0), IS NOT NULL($12))), 20, 30))])
       LogicalJoin(condition=[=($7, $11)], joinType=[left])
         LogicalJoin(condition=[true], joinType=[inner])
           LogicalTableScan(table=[[CATALOG, SALES, EMP]])
@@ -11764,7 +11764,7 @@ LogicalProject(DEPTNO=[$0])
         <Resource name="planAfter">
             <![CDATA[
 LogicalProject(SAL=[$5])
-  LogicalFilter(condition=[OR(=($10, 0), IS NOT TRUE(OR(IS NOT NULL($13), <($11, $10))))])
+  LogicalFilter(condition=[OR(=($10, 0), IS NOT TRUE(OR(IS NOT NULL($13), >($10, $11))))])
     LogicalJoin(condition=[AND(=($0, $12), =($2, $14))], joinType=[left])
       LogicalJoin(condition=[=($2, $9)], joinType=[left])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
@@ -11779,7 +11779,7 @@ LogicalProject(SAL=[$5])
             <![CDATA[
 LogicalProject(SAL=[$5])
   LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
-    LogicalFilter(condition=[OR(=($9, 0), IS NOT TRUE(OR(IS NOT NULL($12), <($10, $9))))])
+    LogicalFilter(condition=[OR(=($9, 0), IS NOT TRUE(OR(IS NOT NULL($12), >($9, $10))))])
       LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{2}])
         LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{2}])
           LogicalTableScan(table=[[CATALOG, SALES, EMP]])
@@ -11856,7 +11856,7 @@ LogicalProject(EMPNO=[$1])
             <![CDATA[
 LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
   LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
-    LogicalFilter(condition=[OR(=($9, 0), IS NOT TRUE(OR(IS NOT NULL($12), <($10, $9))))])
+    LogicalFilter(condition=[OR(=($9, 0), IS NOT TRUE(OR(IS NOT NULL($12), >($9, $10))))])
       LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{1}])
         LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{1}])
           LogicalTableScan(table=[[CATALOG, SALES, EMP]])
@@ -11875,7 +11875,7 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$
         <Resource name="planAfter">
             <![CDATA[
 LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
-  LogicalFilter(condition=[OR(=($10, 0), IS NOT TRUE(OR(IS NOT NULL($13), <($11, $10))))])
+  LogicalFilter(condition=[OR(=($10, 0), IS NOT TRUE(OR(IS NOT NULL($13), >($10, $11))))])
     LogicalJoin(condition=[AND(=($0, $12), =($1, $14))], joinType=[left])
       LogicalJoin(condition=[=($1, $9)], joinType=[left])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
diff --git a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index bff936c..2473830 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -104,7 +104,7 @@ LogicalAggregate(group=[{0, 1}])
             <![CDATA[
 LogicalProject(D=[$0], EXPR$1=[+($0, $1)])
   LogicalAggregate(group=[{0, 1}])
-    LogicalProject(D=[+($7, $0)], MGR=[$3])
+    LogicalProject(D=[+($0, $7)], MGR=[$3])
       LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
         </Resource>
@@ -1565,7 +1565,7 @@ order by sal + empno desc, sal * empno, sal + empno desc]]>
             <![CDATA[
 LogicalProject(EMPNO=[$0])
   LogicalSort(sort0=[$1], sort1=[$2], dir0=[DESC], dir1=[ASC])
-    LogicalProject(EMPNO=[$0], EXPR$1=[+($5, $0)], EXPR$2=[*($5, $0)])
+    LogicalProject(EMPNO=[$0], EXPR$1=[+($0, $5)], EXPR$2=[*($0, $5)])
       LogicalJoin(condition=[true], joinType=[inner])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
         LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
@@ -1879,8 +1879,8 @@ order by c + a]]>
             <![CDATA[
 LogicalProject(A=[$0], B=[$1], C=[$2], DEPTNO=[$3], NAME=[$4])
   LogicalSort(sort0=[$5], dir0=[ASC])
-    LogicalProject(A=[$0], B=[$1], C=[$2], DEPTNO=[$3], NAME=[$4], EXPR$5=[+($2, $0)])
-      LogicalJoin(condition=[=($3, $2)], joinType=[inner])
+    LogicalProject(A=[$0], B=[$1], C=[$2], DEPTNO=[$3], NAME=[$4], EXPR$5=[+($0, $2)])
+      LogicalJoin(condition=[=($2, $3)], joinType=[inner])
         LogicalProject(A=[$2], B=[$1], C=[$0])
           LogicalValues(tuples=[[{ 1, 2, 3 }]])
         LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
@@ -2317,7 +2317,7 @@ JOIN dept on dept.deptno = emp.deptno + 0]]>
         <Resource name="plan">
             <![CDATA[
 LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$10], NAME=[$11])
-  LogicalJoin(condition=[=($10, $9)], joinType=[inner])
+  LogicalJoin(condition=[=($9, $10)], joinType=[inner])
     LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f9=[+($7, 0)])
       LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
@@ -2407,7 +2407,7 @@ LogicalProject(D2=[$0], D3=[$1])
                   LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
                 LogicalAggregate(group=[{0, 1}], agg#0=[MIN($2)])
                   LogicalProject(D4=[$0], D6=[$2], $f0=[true])
-                    LogicalFilter(condition=[=($1, $0)])
+                    LogicalFilter(condition=[=($0, $1)])
                       LogicalProject(D4=[+($0, 4)], D5=[+($0, 5)], D6=[+($0, 6)])
                         LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
 ]]>
@@ -2590,7 +2590,7 @@ from dept]]>
         </Resource>
         <Resource name="plan">
             <![CDATA[
-LogicalProject(NAME=[$1], EXPR$1=[OR(AND(IS NOT NULL($6), <>($2, 0)), AND(<($3, $2), null, <>($2, 0), IS NULL($6)))])
+LogicalProject(NAME=[$1], EXPR$1=[OR(AND(IS NOT NULL($6), <>($2, 0)), AND(>($2, $3), null, <>($2, 0), IS NULL($6)))])
   LogicalJoin(condition=[=($4, $5)], joinType=[left])
     LogicalProject(DEPTNO=[$0], NAME=[$1], $f0=[$2], $f1=[$3], DEPTNO0=[$0])
       LogicalJoin(condition=[true], joinType=[inner])
@@ -2612,7 +2612,7 @@ from emp]]>
         </Resource>
         <Resource name="plan">
             <![CDATA[
-LogicalProject(EMPNO=[$0], EXPR$1=[OR(=($9, 0), AND(<($10, $9), null, <>($9, 0), IS NULL($13)), AND(<>($9, 0), IS NULL($13), >=($10, $9)))])
+LogicalProject(EMPNO=[$0], EXPR$1=[OR(=($9, 0), AND(>($9, $10), null, <>($9, 0), IS NULL($13)), AND(<>($9, 0), IS NULL($13), <=($9, $10)))])
   LogicalJoin(condition=[=($11, $12)], joinType=[left])
     LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f0=[$9], $f1=[$10], DEPTNO0=[$7])
       LogicalJoin(condition=[true], joinType=[inner])
@@ -2756,7 +2756,7 @@ from emp]]>
         </Resource>
         <Resource name="plan">
             <![CDATA[
-LogicalProject(EMPNO=[$0], EXPR$1=[OR(=($9, 0), AND(<($10, $9), null, <>($9, 0), IS NULL($13)), AND(<>($9, 0), IS NULL($13), >=($10, $9)))])
+LogicalProject(EMPNO=[$0], EXPR$1=[OR(=($9, 0), AND(>($9, $10), null, <>($9, 0), IS NULL($13)), AND(<>($9, 0), IS NULL($13), <=($9, $10)))])
   LogicalJoin(condition=[=($11, $12)], joinType=[left])
     LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f0=[$9], $f1=[$10], DEPTNO0=[$7])
       LogicalJoin(condition=[true], joinType=[inner])
@@ -5711,7 +5711,7 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$
   LogicalJoin(condition=[=($7, $11)], joinType=[inner])
     LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalProject(DEPTNO=[$0], NAME=[$1], DEPTNO0=[$2])
-      LogicalJoin(condition=[<($2, $0)], joinType=[inner])
+      LogicalJoin(condition=[>($0, $2)], joinType=[inner])
         LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
         LogicalAggregate(group=[{0}])
           LogicalProject(DEPTNO=[$7])
@@ -5759,7 +5759,7 @@ LogicalProject(D2=[$0], D3=[$1])
                   LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
                 LogicalAggregate(group=[{0, 1}], agg#0=[MIN($2)])
                   LogicalProject(D4=[$0], D6=[$2], $f0=[true])
-                    LogicalFilter(condition=[=($1, $0)])
+                    LogicalFilter(condition=[=($0, $1)])
                       LogicalProject(D4=[+($0, 4)], D5=[+($0, 5)], D6=[+($0, 6)])
                         LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
 ]]>
@@ -5883,7 +5883,7 @@ LogicalProject(EMPNO=[$0])
           LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
       LogicalAggregate(group=[{0}], agg#0=[MIN($1)])
         LogicalProject(EMPNO0=[$9], $f0=[true])
-          LogicalJoin(condition=[<($9, $0)], joinType=[inner])
+          LogicalJoin(condition=[>($0, $9)], joinType=[inner])
             LogicalTableScan(table=[[CATALOG, SALES, EMP]])
             LogicalAggregate(group=[{0}])
               LogicalProject(EMPNO=[$0])
diff --git a/core/src/test/resources/org/apache/calcite/test/TopDownOptTest.xml b/core/src/test/resources/org/apache/calcite/test/TopDownOptTest.xml
index 3442857..f2507fa 100644
--- a/core/src/test/resources/org/apache/calcite/test/TopDownOptTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/TopDownOptTest.xml
@@ -579,7 +579,7 @@ on r.job=s.job and r.ename=s.ename]]>
     <Resource name="planBefore">
       <![CDATA[
 LogicalProject(ENAME=[$0], JOB=[$1], SAL=[$2], COMM=[$3], ENAME0=[$4], JOB0=[$5], EXPR$2=[$6])
-  LogicalJoin(condition=[AND(=($5, $1), =($4, $0))], joinType=[inner])
+  LogicalJoin(condition=[AND(=($1, $5), =($0, $4))], joinType=[inner])
     LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
     LogicalAggregate(group=[{0, 1}], EXPR$2=[MAX($2)])
       LogicalProject(ENAME=[$1], JOB=[$2], SAL=[$5])
@@ -606,7 +606,7 @@ on r.job=s.job and r.ename=s.ename]]>
     <Resource name="planBefore">
       <![CDATA[
 LogicalProject(ENAME=[$0], JOB=[$1], SAL=[$2], COMM=[$3], ENAME0=[$4], JOB0=[$5], MGR=[$6], EXPR$3=[$7])
-  LogicalJoin(condition=[AND(=($5, $1), =($4, $0))], joinType=[inner])
+  LogicalJoin(condition=[AND(=($1, $5), =($0, $4))], joinType=[inner])
     LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
     LogicalAggregate(group=[{0, 1, 2}], EXPR$3=[MAX($3)])
       LogicalProject(ENAME=[$1], JOB=[$2], MGR=[$3], SAL=[$5])
diff --git a/core/src/test/resources/sql/blank.iq b/core/src/test/resources/sql/blank.iq
index 2d69aaf..32d3bcb 100644
--- a/core/src/test/resources/sql/blank.iq
+++ b/core/src/test/resources/sql/blank.iq
@@ -89,7 +89,7 @@ insert into table2 values (NULL, 1), (2, 1);
 # Checked on Oracle
 !set lateDecorrelate true
 select i, j from table1 where table1.j NOT IN (select i from table2 where table1.i=table2.j);
-EnumerableCalc(expr#0..7=[{inputs}], expr#8=[0], expr#9=[=($t3, $t8)], expr#10=[IS NULL($t1)], expr#11=[IS NOT NULL($t7)], expr#12=[<($t4, $t3)], expr#13=[OR($t10, $t11, $t12)], expr#14=[IS NOT TRUE($t13)], expr#15=[OR($t9, $t14)], proj#0..1=[{exprs}], $condition=[$t15])
+EnumerableCalc(expr#0..7=[{inputs}], expr#8=[0], expr#9=[=($t3, $t8)], expr#10=[IS NULL($t1)], expr#11=[IS NOT NULL($t7)], expr#12=[>($t3, $t4)], expr#13=[OR($t10, $t11, $t12)], expr#14=[IS NOT TRUE($t13)], expr#15=[OR($t9, $t14)], proj#0..1=[{exprs}], $condition=[$t15])
   EnumerableHashJoin(condition=[AND(=($0, $6), =($1, $5))], joinType=[left])
     EnumerableHashJoin(condition=[=($0, $2)], joinType=[left])
       EnumerableTableScan(table=[[BLANK, TABLE1]])
diff --git a/core/src/test/resources/sql/sub-query.iq b/core/src/test/resources/sql/sub-query.iq
index 9a9df2f..254738d 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -32,7 +32,7 @@ where t1.x not in (select t2.x from t2);
 (0 rows)
 
 !ok
-EnumerableCalc(expr#0..4=[{inputs}], expr#5=[0], expr#6=[=($t1, $t5)], expr#7=[IS NULL($t4)], expr#8=[>=($t2, $t1)], expr#9=[IS NOT NULL($t0)], expr#10=[AND($t7, $t8, $t9)], expr#11=[OR($t6, $t10)], X=[$t0], $condition=[$t11])
+EnumerableCalc(expr#0..4=[{inputs}], expr#5=[0], expr#6=[=($t1, $t5)], expr#7=[IS NULL($t4)], expr#8=[<=($t1, $t2)], expr#9=[IS NOT NULL($t0)], expr#10=[AND($t7, $t8, $t9)], expr#11=[OR($t6, $t10)], X=[$t0], $condition=[$t11])
   EnumerableHashJoin(condition=[=($0, $3)], joinType=[left])
     EnumerableNestedLoopJoin(condition=[true], joinType=[inner])
       EnumerableUnion(all=[true])
@@ -2048,7 +2048,7 @@ where sal + 100 not in (
 
 !ok
 EnumerableAggregate(group=[{}], C=[COUNT()])
-  EnumerableCalc(expr#0..9=[{inputs}], expr#10=[0], expr#11=[=($t4, $t10)], expr#12=[IS NULL($t2)], expr#13=[IS NOT NULL($t7)], expr#14=[<($t5, $t4)], expr#15=[OR($t12, $t13, $t14)], expr#16=[IS NOT TRUE($t15)], expr#17=[OR($t11, $t16)], proj#0..9=[{exprs}], $condition=[$t17])
+  EnumerableCalc(expr#0..9=[{inputs}], expr#10=[0], expr#11=[=($t4, $t10)], expr#12=[IS NULL($t2)], expr#13=[IS NOT NULL($t7)], expr#14=[>($t4, $t5)], expr#15=[OR($t12, $t13, $t14)], expr#16=[IS NOT TRUE($t15)], expr#17=[OR($t11, $t16)], proj#0..9=[{exprs}], $condition=[$t17])
     EnumerableHashJoin(condition=[AND(=($1, $8), =($2, $9))], joinType=[left])
       EnumerableHashJoin(condition=[=($1, $3)], joinType=[left])
         EnumerableCalc(expr#0..7=[{inputs}], proj#0..1=[{exprs}], SAL=[$t5])
diff --git a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java
index c3b5d1d..9c4f4be 100644
--- a/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java
+++ b/druid/src/main/java/org/apache/calcite/adapter/druid/DruidRules.java
@@ -384,7 +384,7 @@ public class DruidRules {
         return;
       }
       boolean hasRexCalls = false;
-      for (RexNode rexNode : project.getChildExps()) {
+      for (RexNode rexNode : project.getProjects()) {
         if (rexNode instanceof RexCall) {
           hasRexCalls = true;
           break;
diff --git a/piglet/src/main/java/org/apache/calcite/piglet/PigToSqlAggregateRule.java b/piglet/src/main/java/org/apache/calcite/piglet/PigToSqlAggregateRule.java
index bd94628..fb66415 100644
--- a/piglet/src/main/java/org/apache/calcite/piglet/PigToSqlAggregateRule.java
+++ b/piglet/src/main/java/org/apache/calcite/piglet/PigToSqlAggregateRule.java
@@ -200,7 +200,7 @@ public class PigToSqlAggregateRule extends RelOptRule {
     relBuilder.push(oldBottomProject.getInput());
     // First project all group keys, just copy from old one
     for (int i = 0; i < oldAgg.getGroupCount(); i++) {
-      newBottomProjects.add(oldBottomProject.getChildExps().get(i));
+      newBottomProjects.add(oldBottomProject.getProjects().get(i));
     }
     // If grouping aggregate is needed, project the whole ROW
     if (needGoupingCol) {
@@ -235,8 +235,8 @@ public class PigToSqlAggregateRule extends RelOptRule {
         } else {
           // Add it to the projection list if we never project it before
           // First get the ROW operator call
-          final RexCall rowCall = (RexCall) oldBottomProject.getChildExps()
-                                                .get(oldAgg.getGroupCount());
+          final RexCall rowCall = (RexCall) oldBottomProject.getProjects()
+              .get(oldAgg.getGroupCount());
           // Get the corresponding column index in parent rel through the call operand list
           final RexInputRef columRef = (RexInputRef) rowCall.getOperands().get(col);
           final int newIndex = newBottomProjects.size();