You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2017/04/25 05:25:20 UTC

calcite git commit: [CALCITE-1753] Push expressions into null-generating side of a join if they are "strong" (null-preserving)

Repository: calcite
Updated Branches:
  refs/heads/master c850e227d -> 2ce41591b


[CALCITE-1753] Push expressions into null-generating side of a join if they are "strong" (null-preserving)

PushProjector now preserves expressions if the expression is strong
when pushing into the nullable-side of outer join. If an expression
f(x, y) is strong, we know that if it evaluates to null after the join
then x or y must be null, so it will also evaluate to null below the
join. Most SQL built-in functions are strong.

Close apache/calcite#425


Project: http://git-wip-us.apache.org/repos/asf/calcite/repo
Commit: http://git-wip-us.apache.org/repos/asf/calcite/commit/2ce41591
Tree: http://git-wip-us.apache.org/repos/asf/calcite/tree/2ce41591
Diff: http://git-wip-us.apache.org/repos/asf/calcite/diff/2ce41591

Branch: refs/heads/master
Commit: 2ce41591be8986359ac7382359f6934be1fc402b
Parents: c850e22
Author: Minji Kim <mi...@dremio.com>
Authored: Tue Apr 11 22:09:59 2017 -0700
Committer: Julian Hyde <jh...@apache.org>
Committed: Mon Apr 24 20:20:19 2017 -0700

----------------------------------------------------------------------
 .../java/org/apache/calcite/plan/Strong.java    |  63 ++--
 .../rel/rules/ProjectFilterTransposeRule.java   |   3 +-
 .../rel/rules/ProjectJoinTransposeRule.java     |   7 +-
 .../rel/rules/ProjectSetOpTransposeRule.java    |   5 +-
 .../apache/calcite/rel/rules/PushProjector.java |  75 +++-
 .../apache/calcite/test/RelOptRulesTest.java    |  88 +++++
 .../org/apache/calcite/test/RelOptTestBase.java |   5 +-
 .../org/apache/calcite/test/RexProgramTest.java |  81 ++++-
 .../org/apache/calcite/test/RelOptRulesTest.xml | 352 ++++++++++++++++++-
 9 files changed, 626 insertions(+), 53 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/2ce41591/core/src/main/java/org/apache/calcite/plan/Strong.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/plan/Strong.java b/core/src/main/java/org/apache/calcite/plan/Strong.java
index 7fd6055..99a1b4e 100644
--- a/core/src/main/java/org/apache/calcite/plan/Strong.java
+++ b/core/src/main/java/org/apache/calcite/plan/Strong.java
@@ -20,12 +20,15 @@ import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.util.ImmutableBitSet;
 
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 
+import java.util.ArrayList;
 import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
@@ -103,40 +106,40 @@ public class Strong {
    * expressions, and you may override methods to test hypotheses such as
    * "if {@code x} is null, is {@code x + y} null? */
   public boolean isNull(RexNode node) {
+    final Policy policy = MAP.get(node.getKind());
+    switch (policy) {
+    case NOT_NULL:
+      return false;
+    case ANY:
+      return anyNull(((RexCall) node).getOperands());
+    default:
+      break;
+    }
+
     switch (node.getKind()) {
     case LITERAL:
       return ((RexLiteral) node).getValue() == null;
+    // We can only guarantee AND to return NULL if both inputs are NULL  (similar for OR)
+    // AND(NULL, FALSE) = FALSE
     case AND:
-    case NOT:
-    case EQUALS:
-    case NOT_EQUALS:
-    case LESS_THAN:
-    case LESS_THAN_OR_EQUAL:
-    case GREATER_THAN:
-    case GREATER_THAN_OR_EQUAL:
-    case PLUS_PREFIX:
-    case MINUS_PREFIX:
-    case PLUS:
-    case TIMESTAMP_ADD:
-    case MINUS:
-    case TIMESTAMP_DIFF:
-    case TIMES:
-    case DIVIDE:
-    case CAST:
-    case REINTERPRET:
-    case TRIM:
-    case LTRIM:
-    case RTRIM:
-    case CEIL:
-    case FLOOR:
-    case EXTRACT:
-    case GREATEST:
-    case LEAST:
-      return anyNull(((RexCall) node).getOperands());
     case OR:
+    case COALESCE:
       return allNull(((RexCall) node).getOperands());
+    case NULLIF:
+      // NULLIF(null, X) where X can be NULL, returns NULL
+      // NULLIF(X, Y) where X is not NULL, then this may return NULL if X = Y, otherwise X.
+      return allNull(ImmutableList.of(((RexCall) node).getOperands().get(0)));
     case INPUT_REF:
       return isNull((RexInputRef) node);
+    case CASE:
+      final RexCall caseCall = (RexCall) node;
+      final List<RexNode> caseValues = new ArrayList<>();
+      for (int i = 0; i < caseCall.getOperands().size(); i++) {
+        if (!RexUtil.isCasePredicate(caseCall, i)) {
+          caseValues.add(caseCall.getOperands().get(i));
+        }
+      }
+      return allNull(caseValues);
     default:
       return false;
     }
@@ -183,9 +186,11 @@ public class Strong {
     // COALESCE(NULL, 2) yields 2
     map.put(SqlKind.COALESCE, Policy.AS_IS);
     map.put(SqlKind.NVL, Policy.AS_IS);
-    // FALSE OR NULL yields FALSE
+    // FALSE AND NULL yields FALSE
+    // TRUE AND NULL yields NULL
     map.put(SqlKind.AND, Policy.AS_IS);
     // TRUE OR NULL yields TRUE
+    // FALSE OR NULL yields NULL
     map.put(SqlKind.OR, Policy.AS_IS);
 
     // Expression types with custom handlers.
@@ -215,7 +220,9 @@ public class Strong {
     map.put(SqlKind.PLUS_PREFIX, Policy.ANY);
     map.put(SqlKind.MINUS_PREFIX, Policy.ANY);
     map.put(SqlKind.PLUS, Policy.ANY);
+    map.put(SqlKind.PLUS_PREFIX, Policy.ANY);
     map.put(SqlKind.MINUS, Policy.ANY);
+    map.put(SqlKind.MINUS_PREFIX, Policy.ANY);
     map.put(SqlKind.TIMES, Policy.ANY);
     map.put(SqlKind.DIVIDE, Policy.ANY);
     map.put(SqlKind.CAST, Policy.ANY);
@@ -228,6 +235,8 @@ public class Strong {
     map.put(SqlKind.EXTRACT, Policy.ANY);
     map.put(SqlKind.GREATEST, Policy.ANY);
     map.put(SqlKind.LEAST, Policy.ANY);
+    map.put(SqlKind.TIMESTAMP_ADD, Policy.ANY);
+    map.put(SqlKind.TIMESTAMP_DIFF, Policy.ANY);
 
     // Assume that any other expressions cannot be simplified.
     for (SqlKind k

http://git-wip-us.apache.org/repos/asf/calcite/blob/2ce41591/core/src/main/java/org/apache/calcite/rel/rules/ProjectFilterTransposeRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/ProjectFilterTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/ProjectFilterTransposeRule.java
index 0c7cb5c..f468f9c 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/ProjectFilterTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/ProjectFilterTransposeRule.java
@@ -100,8 +100,7 @@ public class ProjectFilterTransposeRule extends RelOptRule {
 
     PushProjector pushProjector =
         new PushProjector(
-            origProj, origFilter, rel, preserveExprCondition,
-            relBuilderFactory.create(origProj.getCluster(), null));
+            origProj, origFilter, rel, preserveExprCondition, call.builder());
     RelNode topProject = pushProjector.convertProject(null);
 
     if (topProject != null) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/2ce41591/core/src/main/java/org/apache/calcite/rel/rules/ProjectJoinTransposeRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/ProjectJoinTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/ProjectJoinTransposeRule.java
index b732635..2983703 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/ProjectJoinTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/ProjectJoinTransposeRule.java
@@ -39,7 +39,7 @@ import java.util.List;
 public class ProjectJoinTransposeRule extends RelOptRule {
   public static final ProjectJoinTransposeRule INSTANCE =
       new ProjectJoinTransposeRule(
-          PushProjector.ExprCondition.FALSE,
+          PushProjector.ExprCondition.TRUE,
           RelFactories.LOGICAL_BUILDER);
 
   //~ Instance fields --------------------------------------------------------
@@ -87,7 +87,7 @@ public class ProjectJoinTransposeRule extends RelOptRule {
             join.getCondition(),
             join,
             preserveExprCondition,
-            relBuilderFactory.create(origProj.getCluster(), null));
+            call.builder());
     if (pushProject.locateAllRefs()) {
       return;
     }
@@ -109,8 +109,7 @@ public class ProjectJoinTransposeRule extends RelOptRule {
     RexNode newJoinFilter = null;
     int[] adjustments = pushProject.getAdjustments();
     if (join.getCondition() != null) {
-      List<RelDataTypeField> projJoinFieldList =
-          new ArrayList<RelDataTypeField>();
+      List<RelDataTypeField> projJoinFieldList = new ArrayList<>();
       projJoinFieldList.addAll(
           join.getSystemFieldList());
       projJoinFieldList.addAll(

http://git-wip-us.apache.org/repos/asf/calcite/blob/2ce41591/core/src/main/java/org/apache/calcite/rel/rules/ProjectSetOpTransposeRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/ProjectSetOpTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/ProjectSetOpTransposeRule.java
index 5df05f6..48fb027 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/ProjectSetOpTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/ProjectSetOpTransposeRule.java
@@ -84,11 +84,10 @@ public class ProjectSetOpTransposeRule extends RelOptRule {
     // locate all fields referenced in the projection
     PushProjector pushProject =
         new PushProjector(
-            origProj, null, setOp, preserveExprCondition,
-            relBuilderFactory.create(origProj.getCluster(), null));
+            origProj, null, setOp, preserveExprCondition, call.builder());
     pushProject.locateAllRefs();
 
-    List<RelNode> newSetOpInputs = new ArrayList<RelNode>();
+    List<RelNode> newSetOpInputs = new ArrayList<>();
     int[] adjustments = pushProject.getAdjustments();
 
     // push the projects completely below the setop; this

http://git-wip-us.apache.org/repos/asf/calcite/blob/2ce41591/core/src/main/java/org/apache/calcite/rel/rules/PushProjector.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/PushProjector.java b/core/src/main/java/org/apache/calcite/rel/rules/PushProjector.java
index 75b6a73..8b5f83e 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/PushProjector.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/PushProjector.java
@@ -18,6 +18,7 @@ package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.linq4j.Ord;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.Strong;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Join;
 import org.apache.calcite.rel.core.Project;
@@ -30,6 +31,7 @@ import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.rex.RexVisitorImpl;
+import org.apache.calcite.runtime.PredicateImpl;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.util.BitSets;
@@ -37,7 +39,9 @@ import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.Pair;
 
 import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
@@ -103,6 +107,12 @@ public class PushProjector {
   final ImmutableBitSet rightBitmap;
 
   /**
+   * Bitmap containing the fields that should be strong, i.e. when preserving expressions
+   * we can only preserve them if the expressions if it is null when these fields are null.
+   */
+  final ImmutableBitSet strongBitmap;
+
+  /**
    * Number of fields in the RelNode that the projection is being pushed past,
    * if the RelNode is not a join. If the RelNode is a join, then this is the
    * number of fields in the left hand side of the join.
@@ -224,12 +234,29 @@ public class PushProjector {
           ImmutableBitSet.range(nSysFields, nFields + nSysFields);
       rightBitmap =
           ImmutableBitSet.range(nFields + nSysFields, nChildFields);
+
+      switch (joinRel.getJoinType()) {
+      case INNER:
+        strongBitmap = ImmutableBitSet.of();
+        break;
+      case RIGHT:  // All the left-input's columns must be strong
+        strongBitmap = ImmutableBitSet.range(nSysFields, nFields + nSysFields);
+        break;
+      case LEFT: // All the right-input's columns must be strong
+        strongBitmap = ImmutableBitSet.range(nFields + nSysFields, nChildFields);
+        break;
+      case FULL:
+      default:
+        strongBitmap = ImmutableBitSet.range(nSysFields, nChildFields);
+      }
+
     } else {
       nFields = nChildFields;
       nFieldsRight = 0;
       childBitmap = ImmutableBitSet.range(nChildFields);
       rightBitmap = null;
       nSysFields = 0;
+      strongBitmap = ImmutableBitSet.of();
     }
     assert nChildFields == nSysFields + nFields + nFieldsRight;
 
@@ -341,6 +368,7 @@ public class PushProjector {
             projRefs,
             childBitmap,
             rightBitmap,
+            strongBitmap,
             preserveExprCondition,
             childPreserveExprs,
             rightPreserveExprs),
@@ -587,14 +615,17 @@ public class PushProjector {
     private final BitSet rexRefs;
     private final ImmutableBitSet leftFields;
     private final ImmutableBitSet rightFields;
+    private final ImmutableBitSet strongFields;
     private final ExprCondition preserveExprCondition;
     private final List<RexNode> preserveLeft;
     private final List<RexNode> preserveRight;
+    private final Strong strong;
 
     public InputSpecialOpFinder(
         BitSet rexRefs,
         ImmutableBitSet leftFields,
         ImmutableBitSet rightFields,
+        final ImmutableBitSet strongFields,
         ExprCondition preserveExprCondition,
         List<RexNode> preserveLeft,
         List<RexNode> preserveRight) {
@@ -605,6 +636,9 @@ public class PushProjector {
       this.preserveExprCondition = preserveExprCondition;
       this.preserveLeft = preserveLeft;
       this.preserveRight = preserveRight;
+
+      this.strongFields = strongFields;
+      this.strong = Strong.of(strongFields);
     }
 
     public Void visitCall(RexCall call) {
@@ -615,6 +649,16 @@ public class PushProjector {
       return null;
     }
 
+    private boolean isStrong(final ImmutableBitSet exprArgs, final RexNode call) {
+      // If the expressions do not use any of the inputs that require output to be null,
+      // no need to check.  Otherwise, check that the expression is null.
+      // For example, in an "left outer join", we don't require that expressions
+      // pushed down into the left input to be strong.  On the other hand,
+      // expressions pushed into the right input must be.  In that case,
+      // strongFields == right input fields.
+      return !strongFields.intersects(exprArgs) || strong.isNull(call);
+    }
+
     private boolean preserve(RexNode call) {
       if (preserveExprCondition.test(call)) {
         // if the arguments of the expression only reference the
@@ -622,10 +666,10 @@ public class PushProjector {
         // it only references expressions on the right
         final ImmutableBitSet exprArgs = RelOptUtil.InputFinder.bits(call);
         if (exprArgs.cardinality() > 0) {
-          if (leftFields.contains(exprArgs)) {
+          if (leftFields.contains(exprArgs) && isStrong(exprArgs, call)) {
             addExpr(preserveLeft, call);
             return true;
-          } else if (rightFields.contains(exprArgs)) {
+          } else if (rightFields.contains(exprArgs) && isStrong(exprArgs, call)) {
             assert preserveRight != null;
             addExpr(preserveRight, call);
             return true;
@@ -759,7 +803,7 @@ public class PushProjector {
    *
    * @see org.apache.calcite.rel.rules.PushProjector.OperatorExprCondition
    */
-  public interface ExprCondition {
+  public interface ExprCondition extends Predicate<RexNode> {
     /**
      * Evaluates a condition for a given expression.
      *
@@ -772,18 +816,33 @@ public class PushProjector {
      * Constant condition that replies {@code false} for all expressions.
      */
     ExprCondition FALSE =
-        new ExprCondition() {
-          public boolean test(RexNode expr) {
+        new ExprConditionImpl() {
+          @Override public boolean test(RexNode expr) {
             return false;
           }
         };
+
+    /**
+     * Constant condition that replies {@code true} for all expressions.
+     */
+    ExprCondition TRUE =
+        new ExprConditionImpl() {
+          @Override public boolean test(RexNode expr) {
+            return true;
+          }
+        };
+  }
+
+  /** Implementation of {@link ExprCondition}. */
+  abstract static class ExprConditionImpl extends PredicateImpl<RexNode>
+      implements ExprCondition {
   }
 
   /**
    * An expression condition that evaluates to true if the expression is
    * a call to one of a set of operators.
    */
-  public static class OperatorExprCondition implements ExprCondition {
+  class OperatorExprCondition extends ExprConditionImpl {
     private final Set<SqlOperator> operatorSet;
 
     /**
@@ -791,8 +850,8 @@ public class PushProjector {
      *
      * @param operatorSet Set of operators
      */
-    public OperatorExprCondition(Set<SqlOperator> operatorSet) {
-      this.operatorSet = operatorSet;
+    public OperatorExprCondition(Iterable<? extends SqlOperator> operatorSet) {
+      this.operatorSet = ImmutableSet.copyOf(operatorSet);
     }
 
     public boolean test(RexNode expr) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/2ce41591/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
----------------------------------------------------------------------
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 8f553bb..f70640f 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -910,6 +910,94 @@ public class RelOptRulesTest extends RelOptTestBase {
             + "on e.ename = b.ename and e.deptno = 10");
   }
 
+  private static final String NOT_STRONG_EXPR =
+      "case when e.sal < 11 then 11 else -1 * e.sal end ";
+
+  private static final String STRONG_EXPR =
+      "case when e.sal < 11 then -1 * e.sal else e.sal end ";
+
+  /** Test case for
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-1753">[CALCITE-1753]
+   * PushProjector should only preserve expressions if the expression is strong
+   * when pushing into the nullable-side of outer join</a>. */
+  @Test public void testPushProjectPastInnerJoin() {
+    final String sql = "select count(*), " + NOT_STRONG_EXPR + "\n"
+        + "from emp e inner join bonus b on e.ename = b.ename\n"
+        + "group by " + NOT_STRONG_EXPR;
+    sql(sql).withRule(ProjectJoinTransposeRule.INSTANCE).check();
+  }
+
+  @Test public void testPushProjectPastInnerJoinStrong() {
+    final String sql = "select count(*), " + STRONG_EXPR + "\n"
+        + "from emp e inner join bonus b on e.ename = b.ename\n"
+        + "group by " + STRONG_EXPR;
+    sql(sql).withRule(ProjectJoinTransposeRule.INSTANCE).check();
+  }
+
+  @Test public void testPushProjectPastLeftJoin() {
+    final String sql = "select count(*), " + NOT_STRONG_EXPR + "\n"
+        + "from emp e left outer join bonus b on e.ename = b.ename\n"
+        + "group by case when e.sal < 11 then 11 else -1 * e.sal end";
+    sql(sql).withRule(ProjectJoinTransposeRule.INSTANCE).check();
+  }
+
+  @Test public void testPushProjectPastLeftJoinSwap() {
+    final String sql = "select count(*), " + NOT_STRONG_EXPR + "\n"
+        + "from bonus b left outer join emp e on e.ename = b.ename\n"
+        + "group by " + NOT_STRONG_EXPR;
+    sql(sql).withRule(ProjectJoinTransposeRule.INSTANCE).check();
+  }
+
+  @Test public void testPushProjectPastLeftJoinSwapStrong() {
+    final String sql = "select count(*), " + STRONG_EXPR + "\n"
+        + "from bonus b left outer join emp e on e.ename = b.ename\n"
+        + "group by " + STRONG_EXPR;
+    sql(sql).withRule(ProjectJoinTransposeRule.INSTANCE).check();
+  }
+
+  @Test public void testPushProjectPastRightJoin() {
+    final String sql = "select count(*), " + NOT_STRONG_EXPR + "\n"
+        + "from emp e right outer join bonus b on e.ename = b.ename\n"
+        + "group by " + NOT_STRONG_EXPR;
+    sql(sql).withRule(ProjectJoinTransposeRule.INSTANCE).check();
+  }
+
+  @Test public void testPushProjectPastRightJoinStrong() {
+    final String sql = "select count(*),\n"
+        + " case when e.sal < 11 then -1 * e.sal else e.sal end\n"
+        + "from emp e right outer join bonus b on e.ename = b.ename\n"
+        + "group by case when e.sal < 11 then -1 * e.sal else e.sal end";
+    sql(sql).withRule(ProjectJoinTransposeRule.INSTANCE).check();
+  }
+
+  @Test public void testPushProjectPastRightJoinSwap() {
+    final String sql = "select count(*), " + NOT_STRONG_EXPR + "\n"
+        + "from bonus b right outer join emp e on e.ename = b.ename\n"
+        + "group by " + NOT_STRONG_EXPR;
+    sql(sql).withRule(ProjectJoinTransposeRule.INSTANCE).check();
+  }
+
+  @Test public void testPushProjectPastRightJoinSwapStrong() {
+    final String sql = "select count(*), " + STRONG_EXPR + "\n"
+        + "from bonus b right outer join emp e on e.ename = b.ename\n"
+        + "group by " + STRONG_EXPR;
+    sql(sql).withRule(ProjectJoinTransposeRule.INSTANCE).check();
+  }
+
+  @Test public void testPushProjectPastFullJoin() {
+    final String sql = "select count(*), " + NOT_STRONG_EXPR + "\n"
+        + "from emp e full outer join bonus b on e.ename = b.ename\n"
+        + "group by " + NOT_STRONG_EXPR;
+    sql(sql).withRule(ProjectJoinTransposeRule.INSTANCE).check();
+  }
+
+  @Test public void testPushProjectPastFullJoinStrong() {
+    final String sql = "select count(*), " + STRONG_EXPR + "\n"
+        + "from emp e full outer join bonus b on e.ename = b.ename\n"
+        + "group by " + STRONG_EXPR;
+    sql(sql).withRule(ProjectJoinTransposeRule.INSTANCE).check();
+  }
+
   @Test public void testPushProjectPastSetOp() {
     checkPlanning(ProjectSetOpTransposeRule.INSTANCE,
         "select sal from "

http://git-wip-us.apache.org/repos/asf/calcite/blob/2ce41591/core/src/test/java/org/apache/calcite/test/RelOptTestBase.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptTestBase.java b/core/src/test/java/org/apache/calcite/test/RelOptTestBase.java
index 28f0b9c..848918e 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptTestBase.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptTestBase.java
@@ -220,6 +220,10 @@ abstract class RelOptTestBase extends SqlToRelTestBase {
           transforms);
     }
 
+    public Sql withRule(RelOptRule rule) {
+      return with(HepProgram.builder().addRuleInstance(rule).build());
+    }
+
     /** Adds a transform that will be applied to {@link #tester}
      * just before running the query. */
     private Sql withTransform(Function<Tester, Tester> transform) {
@@ -296,7 +300,6 @@ abstract class RelOptTestBase extends SqlToRelTestBase {
         checkPlanning(t, preProgram, hepPlanner, sql, unchanged);
       }
     }
-
   }
 
 }

http://git-wip-us.apache.org/repos/asf/calcite/blob/2ce41591/core/src/test/java/org/apache/calcite/test/RexProgramTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/RexProgramTest.java b/core/src/test/java/org/apache/calcite/test/RexProgramTest.java
index 671b685..ebc17f6 100644
--- a/core/src/test/java/org/apache/calcite/test/RexProgramTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RexProgramTest.java
@@ -170,6 +170,10 @@ public class RexProgramTest {
     return rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, node);
   }
 
+  private RexNode nullIf(RexNode node1, RexNode node2) {
+    return rexBuilder.makeCall(SqlStdOperatorTable.NULLIF, node1, node2);
+  }
+
   private RexNode not(RexNode node) {
     return rexBuilder.makeCall(SqlStdOperatorTable.NOT, node);
   }
@@ -537,16 +541,19 @@ public class RexProgramTest {
     final RexNode andTrueUnknown = and(trueLiteral, unknownLiteral);
     final RexNode andFalseTrue = and(falseLiteral, trueLiteral);
 
-    assertThat(Strong.isNull(andUnknownTrue, c), is(true));
-    assertThat(Strong.isNull(andTrueUnknown, c), is(true));
+    assertThat(Strong.isNull(andUnknownTrue, c), is(false));
+    assertThat(Strong.isNull(andTrueUnknown, c), is(false));
     assertThat(Strong.isNull(andFalseTrue, c), is(false));
 
     // If i0 is null, "i0 and i1 is null" is null
-    assertThat(Strong.isNull(and(i0, isNull(i1)), c0), is(true));
-    // If i1 is null, "i0 and i1 is null" is not necessarily null
+    assertThat(Strong.isNull(and(i0, isNull(i1)), c0), is(false));
+    // If i1 is null, "i0 and i1" is false
     assertThat(Strong.isNull(and(i0, isNull(i1)), c1), is(false));
-    // If i0 and i1 are both null, "i0 and i1 is null" is null
-    assertThat(Strong.isNull(and(i0, isNull(i1)), c01), is(true));
+    // If i0 and i1 are both null, "i0 and i1" is null
+    assertThat(Strong.isNull(and(i0, i1), c01), is(true));
+    assertThat(Strong.isNull(and(i0, i1), c1), is(false));
+    // If i0 and i1 are both null, "i0 and isNull(i1) is false"
+    assertThat(Strong.isNull(and(i0, isNull(i1)), c01), is(false));
     // If i0 and i1 are both null, "i0 or i1" is null
     assertThat(Strong.isNull(or(i0, i1), c01), is(true));
     // If i0 is null, "i0 or i1" is not necessarily null
@@ -563,6 +570,68 @@ public class RexProgramTest {
     RexNode notI0NotNull = not(isNotNull(i0));
     assertThat(Strong.isNull(notI0NotNull, c0), is(false));
     assertThat(Strong.isNotTrue(notI0NotNull, c0), is(false));
+
+    // NULLIF(null, null): null
+    // NULLIF(null, X): null
+    // NULLIF(X, X/Y): null or X
+    // NULLIF(X, null): X
+    assertThat(Strong.isNull(nullIf(nullLiteral, nullLiteral), c), is(true));
+    assertThat(Strong.isNull(nullIf(nullLiteral, trueLiteral), c), is(true));
+    assertThat(Strong.isNull(nullIf(trueLiteral, trueLiteral), c), is(false));
+    assertThat(Strong.isNull(nullIf(trueLiteral, falseLiteral), c), is(false));
+    assertThat(Strong.isNull(nullIf(trueLiteral, nullLiteral), c), is(false));
+
+    // ISNULL(null) is true, ISNULL(not null value) is false
+    assertThat(Strong.isNull(isNull(nullLiteral), c01), is(false));
+    assertThat(Strong.isNull(isNull(trueLiteral), c01), is(false));
+
+    // CASE ( <predicate1> <value1> <predicate2> <value2> <predicate3> <value3> ...)
+    // only definitely null if all values are null.
+    assertThat(
+        Strong.isNull(
+            case_(eq(i0, i1), nullLiteral, ge(i0, i1), nullLiteral, nullLiteral), c01),
+        is(true));
+    assertThat(
+        Strong.isNull(
+            case_(eq(i0, i1), i0, ge(i0, i1), nullLiteral, nullLiteral), c01),
+        is(true));
+    assertThat(
+        Strong.isNull(
+            case_(eq(i0, i1), i0, ge(i0, i1), nullLiteral, nullLiteral), c1),
+        is(false));
+    assertThat(
+        Strong.isNull(
+            case_(eq(i0, i1), nullLiteral, ge(i0, i1), i0, nullLiteral), c01),
+        is(true));
+    assertThat(
+        Strong.isNull(
+            case_(eq(i0, i1), nullLiteral, ge(i0, i1), i0, nullLiteral), c1),
+        is(false));
+    assertThat(
+        Strong.isNull(
+            case_(eq(i0, i1), nullLiteral, ge(i0, i1), nullLiteral, i0), c01),
+        is(true));
+    assertThat(
+        Strong.isNull(
+            case_(eq(i0, i1), nullLiteral, ge(i0, i1), nullLiteral, i0), c1),
+        is(false));
+    assertThat(
+        Strong.isNull(
+            case_(isNotNull(i0), i0, i1), c),
+        is(false));
+    assertThat(
+        Strong.isNull(
+            case_(isNotNull(i0), i0, i1), c0),
+        is(false));
+    assertThat(
+        Strong.isNull(
+            case_(isNotNull(i0), i0, i1), c1),
+        is(false));
+    assertThat(
+        Strong.isNull(
+            case_(isNotNull(i0), i0, i1), c01),
+        is(true));
+
   }
 
   /** Unit test for {@link org.apache.calcite.rex.RexUtil#toCnf}. */

http://git-wip-us.apache.org/repos/asf/calcite/blob/2ce41591/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
----------------------------------------------------------------------
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 cc6b417..2314612 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -854,14 +854,362 @@ LogicalProject(EXPR$0=[+($5, $12)])
         <Resource name="planAfter">
             <![CDATA[
 LogicalProject(EXPR$0=[+($1, $4)])
-  LogicalJoin(condition=[AND(=($0, $3), =($2, 10))], joinType=[inner])
-    LogicalProject(ENAME=[$1], SAL=[$5], DEPTNO=[$7])
+  LogicalJoin(condition=[AND(=($0, $3), $2)], joinType=[inner])
+    LogicalProject(ENAME=[$1], SAL=[$5], ==[=($7, 10)])
       LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalProject(ENAME=[$0], COMM=[$3])
       LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
 ]]>
         </Resource>
     </TestCase>
+    <TestCase name="testPushProjectPastInnerJoinStrong">
+        <Resource name="sql">
+            <![CDATA[select count(*), case when e.sal < 11 then -1 * e.sal else e.sal end
+            from emp e inner join bonus b on e.ename = b.ename group by
+            case when e.sal < 11 then -1 * e.sal else e.sal end]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[CASE(<($5, 11), *(-1, $5), $5)])
+      LogicalJoin(condition=[=($1, $9)], joinType=[inner])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[$1])
+      LogicalJoin(condition=[=($0, $2)], joinType=[inner])
+        LogicalProject(ENAME=[$1], CASE=[CASE(<($5, 11), *(-1, $5), $5)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalProject(ENAME=[$0])
+          LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testPushProjectPastInnerJoin">
+        <Resource name="sql">
+            <![CDATA[select count(*), case when e.sal < 11 then 11 else -1 * e.sal end
+            from emp e inner join bonus b on e.ename = b.ename group by
+            case when e.sal < 11 then 11 else -1 * e.sal end]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[CASE(<($5, 11), 11, *(-1, $5))])
+      LogicalJoin(condition=[=($1, $9)], joinType=[inner])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[$1])
+      LogicalJoin(condition=[=($0, $2)], joinType=[inner])
+        LogicalProject(ENAME=[$1], CASE=[CASE(<($5, 11), 11, *(-1, $5))])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalProject(ENAME=[$0])
+          LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testPushProjectPastLeftJoin">
+        <Resource name="sql">
+            <![CDATA[select count(*), case when e.sal < 11 then 11 else -1 * e.sal end
+            from emp e left outer join bonus b on e.ename = b.ename group by
+            case when e.sal < 11 then 11 else -1 * e.sal end]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[CASE(<($5, 11), 11, *(-1, $5))])
+      LogicalJoin(condition=[=($1, $9)], joinType=[left])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[$1])
+      LogicalJoin(condition=[=($0, $2)], joinType=[left])
+        LogicalProject(ENAME=[$1], CASE=[CASE(<($5, 11), 11, *(-1, $5))])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalProject(ENAME=[$0])
+          LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testPushProjectPastLeftJoinSwap">
+        <Resource name="sql">
+            <![CDATA[select count(*), case when e.sal < 11 then 11 else -1 * e.sal end
+            from bonus b left outer join emp e on e.ename = b.ename group by
+            case when e.sal < 11 then 11 else -1 * e.sal end]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+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])
+        LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+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])
+        LogicalProject(ENAME=[$0])
+          LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+        LogicalProject(ENAME=[$1], <=[<($5, 11)], *=[*(-1, $5)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testPushProjectPastLeftJoinSwapStrong">
+        <Resource name="sql">
+            <![CDATA[select count(*), case when e.sal < 11 then -1 * e.sal else e.sal end
+            from bonus b left outer join emp e on e.ename = b.ename group by
+            case when e.sal < 11 then -1 * e.sal else e.sal end]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+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])
+        LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[$2])
+      LogicalJoin(condition=[=($1, $0)], joinType=[left])
+        LogicalProject(ENAME=[$0])
+          LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+        LogicalProject(ENAME=[$1], CASE=[CASE(<($5, 11), *(-1, $5), $5)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testPushProjectPastLeftJoinStrong">
+        <Resource name="sql">
+            <![CDATA[select count(*), case when e.sal < 11 then -1 * e.sal else e.sal end
+            from bonus b left outer join emp e on e.ename = b.ename group by
+            case when e.sal < 11 then -1 * e.sal else e.sal end]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+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])
+        LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[$2])
+      LogicalJoin(condition=[=($1, $0)], joinType=[left])
+        LogicalProject(ENAME=[$0])
+          LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+        LogicalProject(ENAME=[$1], CASE=[CASE(<($5, 11), *(-1, $5), $5)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testPushProjectPastRightJoin">
+        <Resource name="sql">
+            <![CDATA[select count(*), case when e.sal < 11 then 11 else -1 * e.sal end
+            from emp e right outer join bonus b on e.ename = b.ename group by
+            case when e.sal < 11 then 11 else -1 * e.sal end]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[CASE(<($5, 11), 11, *(-1, $5))])
+      LogicalJoin(condition=[=($1, $9)], joinType=[right])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[CASE($1, 11, $2)])
+      LogicalJoin(condition=[=($0, $3)], joinType=[right])
+        LogicalProject(ENAME=[$1], <=[<($5, 11)], *=[*(-1, $5)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalProject(ENAME=[$0])
+          LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testPushProjectPastRightJoinStrong">
+        <Resource name="sql">
+            <![CDATA[select count(*), case when e.sal < 11 then -1 * e.sal else e.sal end
+            from emp e right outer join bonus b on e.ename = b.ename group by
+            case when e.sal < 11 then -1 * e.sal else e.sal end]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[CASE(<($5, 11), *(-1, $5), $5)])
+      LogicalJoin(condition=[=($1, $9)], joinType=[right])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[$1])
+      LogicalJoin(condition=[=($0, $2)], joinType=[right])
+        LogicalProject(ENAME=[$1], CASE=[CASE(<($5, 11), *(-1, $5), $5)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalProject(ENAME=[$0])
+          LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testPushProjectPastRightJoinSwap">
+        <Resource name="sql">
+            <![CDATA[select count(*), case when e.sal < 11 then 11 else -1 * e.sal end
+            from bonus b right outer join emp e on e.ename = b.ename group by
+            case when e.sal < 11 then 11 else -1 * e.sal end]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+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])
+        LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[$2])
+      LogicalJoin(condition=[=($1, $0)], joinType=[right])
+        LogicalProject(ENAME=[$0])
+          LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+        LogicalProject(ENAME=[$1], CASE=[CASE(<($5, 11), 11, *(-1, $5))])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testPushProjectPastRightJoinSwapStrong">
+        <Resource name="sql">
+            <![CDATA[select count(*), case when e.sal < 11 then -1 * e.sal else e.sal end
+            from bonus b right outer join emp e on e.ename = b.ename group by
+            case when e.sal < 11 then -1 * e.sal else e.sal end]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+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])
+        LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[$2])
+      LogicalJoin(condition=[=($1, $0)], joinType=[right])
+        LogicalProject(ENAME=[$0])
+          LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+        LogicalProject(ENAME=[$1], CASE=[CASE(<($5, 11), *(-1, $5), $5)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testPushProjectPastFullJoin">
+        <Resource name="sql">
+            <![CDATA[select count(*), case when e.sal < 11 then 11 else -1 * e.sal end
+            from emp e full outer join bonus b on e.ename = b.ename group by
+            case when e.sal < 11 then 11 else -1 * e.sal end]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[CASE(<($5, 11), 11, *(-1, $5))])
+      LogicalJoin(condition=[=($1, $9)], joinType=[full])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[CASE($1, 11, $2)])
+      LogicalJoin(condition=[=($0, $3)], joinType=[full])
+        LogicalProject(ENAME=[$1], <=[<($5, 11)], *=[*(-1, $5)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalProject(ENAME=[$0])
+          LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testPushProjectPastFullJoinStrong">
+        <Resource name="sql">
+            <![CDATA[select count(*), case when e.sal < 11 then -1 * e.sal else e.sal end
+            from emp e full outer join bonus b on e.ename = b.ename group by
+            case when e.sal < 11 then -1 * e.sal else e.sal end]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[CASE(<($5, 11), *(-1, $5), $5)])
+      LogicalJoin(condition=[=($1, $9)], joinType=[full])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(EXPR$0=[$1], EXPR$1=[$0])
+  LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+    LogicalProject(EXPR$1=[$1])
+      LogicalJoin(condition=[=($0, $2)], joinType=[full])
+        LogicalProject(ENAME=[$1], CASE=[CASE(<($5, 11), *(-1, $5), $5)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalProject(ENAME=[$0])
+          LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+        </Resource>
+    </TestCase>
     <TestCase name="testPushProjectPastSetOp">
         <Resource name="sql">
             <![CDATA[select sal from (select * from emp e1 union all select * from emp e2)]]>