You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by ja...@apache.org on 2018/12/16 02:10:25 UTC

calcite git commit: [CALCITE-2736] Update the ReduceExpressionsRule to better expose options

Repository: calcite
Updated Branches:
  refs/heads/master f362785e1 -> 0d0fd9e9c


[CALCITE-2736] Update the ReduceExpressionsRule to better expose options

* Update the reduction rule to use an options object to control behavior.
* Update existing methods so they use options object
* Deprecate methods that accept booleans instead of options object
* Expose ability to avoid special handling for dynamic calls


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

Branch: refs/heads/master
Commit: 0d0fd9e9cbf9ac0adfee6b9c7306c9d49d055bcd
Parents: f362785
Author: Jacques Nadeau <ja...@apache.org>
Authored: Wed Dec 12 10:25:58 2018 +0530
Committer: Jacques Nadeau <ja...@apache.org>
Committed: Sat Dec 15 17:37:47 2018 -0800

----------------------------------------------------------------------
 .../rel/rules/ReduceExpressionsRule.java        | 293 +++++++++++++++----
 .../apache/calcite/test/RelOptRulesTest.java    |  34 +++
 .../org/apache/calcite/test/RelOptRulesTest.xml |  22 ++
 3 files changed, 293 insertions(+), 56 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/0d0fd9e9/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java b/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java
index 48ed3cd..5f3425c 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java
@@ -92,6 +92,30 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
   //~ Static fields/initializers ---------------------------------------------
 
   /**
+   * Default reduction options used for project and calc.
+   *
+   * <ul>
+   * <li>Match nullability of expressions: true
+   * <li>Treat unknown values as false: false
+   * <li>Treat dynamic calls as non constant: true
+   * </ul>
+   */
+  public static final ReductionOptions DEFAULT_OPTIONS =
+      new ReductionOptions(true, false, true);
+
+  /**
+   * Default reduction options used for filter and join.
+   *
+   * <ul>
+   * <li>Match nullability of expressions: true
+   * <li>Treat unknown values as false: true
+   * <li>Treat dynamic calls as non constant: true
+   * </ul>
+   */
+  public static final ReductionOptions DEFAULT_FILTER_OPTIONS =
+      new ReductionOptions(true, true, true);
+
+  /**
    * Regular expression that matches the description of all instances of this
    * rule and {@link ValuesReduceRule} also. Use
    * it to prevent the planner from invoking these rules.
@@ -104,7 +128,8 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
    * {@link org.apache.calcite.rel.logical.LogicalFilter}.
    */
   public static final ReduceExpressionsRule FILTER_INSTANCE =
-      new FilterReduceExpressionsRule(LogicalFilter.class, true,
+      new FilterReduceExpressionsRule(LogicalFilter.class,
+          DEFAULT_FILTER_OPTIONS,
           RelFactories.LOGICAL_BUILDER);
 
   /**
@@ -112,7 +137,8 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
    * {@link org.apache.calcite.rel.logical.LogicalProject}.
    */
   public static final ReduceExpressionsRule PROJECT_INSTANCE =
-      new ProjectReduceExpressionsRule(LogicalProject.class, true,
+      new ProjectReduceExpressionsRule(LogicalProject.class,
+          DEFAULT_OPTIONS,
           RelFactories.LOGICAL_BUILDER);
 
   /**
@@ -120,7 +146,8 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
    * {@link org.apache.calcite.rel.core.Join}.
    */
   public static final ReduceExpressionsRule JOIN_INSTANCE =
-      new JoinReduceExpressionsRule(Join.class, true,
+      new JoinReduceExpressionsRule(Join.class,
+          DEFAULT_FILTER_OPTIONS,
           RelFactories.LOGICAL_BUILDER);
 
   /**
@@ -131,7 +158,7 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
       new CalcReduceExpressionsRule(LogicalCalc.class, true,
           RelFactories.LOGICAL_BUILDER);
 
-  protected final boolean matchNullability;
+  protected final ReductionOptions options;
 
   /**
    * Rule that reduces constants inside a {@link org.apache.calcite.rel.core.Filter}.
@@ -145,9 +172,16 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
       this(filterClass, true, relBuilderFactory);
     }
 
+    @Deprecated // to be removed before 2.0
     public FilterReduceExpressionsRule(Class<? extends Filter> filterClass,
         boolean matchNullability, RelBuilderFactory relBuilderFactory) {
-      super(filterClass, matchNullability, relBuilderFactory,
+      this(filterClass, DEFAULT_FILTER_OPTIONS.matchNullability(matchNullability),
+          relBuilderFactory);
+    }
+
+    public FilterReduceExpressionsRule(Class<? extends Filter> filterClass,
+        ReductionOptions options, RelBuilderFactory relBuilderFactory) {
+      super(filterClass, options, relBuilderFactory,
           "ReduceExpressionsRule(Filter)");
     }
 
@@ -160,8 +194,7 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
       final RelMetadataQuery mq = call.getMetadataQuery();
       final RelOptPredicateList predicates =
           mq.getPulledUpPredicates(filter.getInput());
-      if (reduceExpressions(filter, expList, predicates, true,
-          matchNullability)) {
+      if (reduceExpressions(filter, expList, predicates, options)) {
         assert expList.size() == 1;
         newConditionExp = expList.get(0);
         reduced = true;
@@ -272,9 +305,19 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
       this(projectClass, true, relBuilderFactory);
     }
 
+    @Deprecated // to be removed before 2.0
     public ProjectReduceExpressionsRule(Class<? extends Project> projectClass,
         boolean matchNullability, RelBuilderFactory relBuilderFactory) {
-      super(projectClass, matchNullability, relBuilderFactory,
+      this(projectClass,
+          DEFAULT_OPTIONS.matchNullability(matchNullability).unknownAsFalse(false),
+          relBuilderFactory);
+    }
+
+    public ProjectReduceExpressionsRule(
+        Class<? extends Project> projectClass,
+        ReductionOptions options,
+        RelBuilderFactory relBuilderFactory) {
+      super(projectClass, options, relBuilderFactory,
           "ReduceExpressionsRule(Project)");
     }
 
@@ -285,8 +328,7 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
           mq.getPulledUpPredicates(project.getInput());
       final List<RexNode> expList =
           Lists.newArrayList(project.getProjects());
-      if (reduceExpressions(project, expList, predicates, false,
-          matchNullability)) {
+      if (reduceExpressions(project, expList, predicates, options)) {
         call.transformTo(
             call.builder()
                 .push(project.getInput())
@@ -306,12 +348,20 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
     @Deprecated // to be removed before 2.0
     public JoinReduceExpressionsRule(Class<? extends Join> joinClass,
         RelBuilderFactory relBuilderFactory) {
-      this(joinClass, true, relBuilderFactory);
+      this(joinClass, DEFAULT_FILTER_OPTIONS, relBuilderFactory);
     }
 
+    @Deprecated // to be removed before 2.0
     public JoinReduceExpressionsRule(Class<? extends Join> joinClass,
         boolean matchNullability, RelBuilderFactory relBuilderFactory) {
-      super(joinClass, matchNullability, relBuilderFactory,
+      this(joinClass,
+          DEFAULT_FILTER_OPTIONS.matchNullability(matchNullability),
+          relBuilderFactory);
+    }
+
+    public JoinReduceExpressionsRule(Class<? extends Join> joinClass,
+        ReductionOptions options, RelBuilderFactory relBuilderFactory) {
+      super(joinClass, options, relBuilderFactory,
           "ReduceExpressionsRule(Join)");
     }
 
@@ -328,8 +378,7 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
       final RelOptPredicateList predicates =
           leftPredicates.union(rexBuilder,
               rightPredicates.shift(rexBuilder, fieldCount));
-      if (!reduceExpressions(join, expList, predicates, true,
-          matchNullability)) {
+      if (!reduceExpressions(join, expList, predicates, options)) {
         return;
       }
       if (join instanceof EquiJoin) {
@@ -362,12 +411,22 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
     @Deprecated // to be removed before 2.0
     public CalcReduceExpressionsRule(Class<? extends Calc> calcClass,
         RelBuilderFactory relBuilderFactory) {
-      this(calcClass, true, relBuilderFactory);
+      this(calcClass, DEFAULT_OPTIONS, relBuilderFactory);
     }
 
+    @Deprecated // to be removed before 2.0
     public CalcReduceExpressionsRule(Class<? extends Calc> calcClass,
         boolean matchNullability, RelBuilderFactory relBuilderFactory) {
-      super(calcClass, matchNullability, relBuilderFactory,
+      this(calcClass,
+          DEFAULT_OPTIONS.matchNullability(matchNullability),
+          relBuilderFactory);
+    }
+
+    public CalcReduceExpressionsRule(Class<? extends Calc> calcClass,
+        ReductionOptions options, RelBuilderFactory relBuilderFactory) {
+      super(calcClass,
+          options,
+          relBuilderFactory,
           "ReduceExpressionsRule(Calc)");
     }
 
@@ -388,8 +447,7 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
         expandedExprList.add(expr.accept(shuttle));
       }
       final RelOptPredicateList predicates = RelOptPredicateList.EMPTY;
-      if (reduceExpressions(calc, expandedExprList, predicates, false,
-          matchNullability)) {
+      if (reduceExpressions(calc, expandedExprList, predicates, options)) {
         final RexProgramBuilder builder =
             new RexProgramBuilder(
                 calc.getInput().getRowType(),
@@ -462,11 +520,25 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
    * @param matchNullability Whether to add a CAST when a nullable expression
    *                         reduces to a NOT NULL literal
    */
+  @Deprecated // to be removed before 2.0
   protected ReduceExpressionsRule(Class<? extends RelNode> clazz,
       boolean matchNullability, RelBuilderFactory relBuilderFactory,
       String description) {
+    this(clazz,
+        DEFAULT_OPTIONS.matchNullability(matchNullability)
+          .treatDynamicCallsAsNonConstant(true)
+          .unknownAsFalse(false),
+        relBuilderFactory,
+        description);
+  }
+
+  protected ReduceExpressionsRule(
+      Class<? extends RelNode> clazz,
+      ReductionOptions options,
+      RelBuilderFactory relBuilderFactory,
+      String description) {
     super(operand(clazz, any()), relBuilderFactory, description);
-    this.matchNullability = matchNullability;
+    this.options = options;
   }
 
   @Deprecated // to be removed before 2.0
@@ -485,50 +557,43 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
    * @param predicates Constraints known to hold on input expressions
    * @return whether reduction found something to change, and succeeded
    */
+  @Deprecated // to be removed before 2.0
   protected static boolean reduceExpressions(RelNode rel, List<RexNode> expList,
       RelOptPredicateList predicates) {
-    return reduceExpressions(rel, expList, predicates, false, true);
+    return reduceExpressions(rel, expList, predicates,
+        DEFAULT_OPTIONS.matchNullability(true).unknownAsFalse(false));
   }
 
   @Deprecated // to be removed before 2.0
   protected static boolean reduceExpressions(RelNode rel, List<RexNode> expList,
       RelOptPredicateList predicates, boolean unknownAsFalse) {
-    return reduceExpressions(rel, expList, predicates, unknownAsFalse, true);
+    return reduceExpressions(rel, expList, predicates,
+        DEFAULT_OPTIONS.matchNullability(true).unknownAsFalse(unknownAsFalse));
+  }
+
+  @Deprecated
+  protected static boolean reduceExpressions(RelNode rel, List<RexNode> expList,
+      RelOptPredicateList predicates, boolean unknownAsFalse,
+      boolean matchNullability) {
+    return reduceExpressions(rel, expList, predicates,
+        DEFAULT_OPTIONS.matchNullability(matchNullability)
+          .treatDynamicCallsAsNonConstant(true)
+          .matchNullability(matchNullability)
+        );
   }
 
   /**
    * Reduces a list of expressions.
    *
-   * <p>The {@code matchNullability} flag comes into play when reducing a
-   * expression whose type is nullable. Suppose we are reducing an expression
-   * {@code CASE WHEN 'a' = 'a' THEN 1 ELSE NULL END}. Before reduction the
-   * type is {@code INTEGER} (nullable), but after reduction the literal 1 has
-   * type {@code INTEGER NOT NULL}.
-   *
-   * <p>In some situations it is more important to preserve types; in this
-   * case you should use {@code matchNullability = true} (which used to be
-   * the default behavior of this method), and it will cast the literal to
-   * {@code INTEGER} (nullable).
-   *
-   * <p>In other situations, you would rather propagate the new stronger type,
-   * because it may allow further optimizations later; pass
-   * {@code matchNullability = false} and no cast will be added, but you may
-   * need to adjust types elsewhere in the expression tree.
-   *
    * @param rel     Relational expression
    * @param expList List of expressions, modified in place
    * @param predicates Constraints known to hold on input expressions
-   * @param unknownAsFalse Whether UNKNOWN will be treated as FALSE
-   * @param matchNullability Whether Calcite should add a CAST to a literal
-   *                         resulting from simplification and expression if the
-   *                         expression had nullable type and the literal is
-   *                         NOT NULL
+   * @param options ReductionOptions to use
    *
    * @return whether reduction found something to change, and succeeded
    */
   protected static boolean reduceExpressions(RelNode rel, List<RexNode> expList,
-      RelOptPredicateList predicates, boolean unknownAsFalse,
-      boolean matchNullability) {
+      RelOptPredicateList predicates, ReductionOptions options) {
     final RelOptCluster cluster = rel.getCluster();
     final RexBuilder rexBuilder = cluster.getRexBuilder();
     final RexExecutor executor =
@@ -537,15 +602,15 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
         new RexSimplify(rexBuilder, predicates, executor);
 
     // Simplify predicates in place
-    final RexUnknownAs unknownAs = RexUnknownAs.falseIf(unknownAsFalse);
+    final RexUnknownAs unknownAs = RexUnknownAs.falseIf(options.unknownAsFalse);
     final boolean reduced = reduceExpressionsInternal(rel, simplify, unknownAs,
-        expList, predicates);
+        expList, predicates, options.treatDynamicCallsAsNonConstant);
 
     boolean simplified = false;
     for (int i = 0; i < expList.size(); i++) {
       final RexNode expr2 =
           simplify.simplifyPreservingType(expList.get(i), unknownAs,
-              matchNullability);
+              options.matchNullability);
       if (!expr2.equals(expList.get(i))) {
         expList.set(i, expr2);
         simplified = true;
@@ -557,7 +622,7 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
 
   protected static boolean reduceExpressionsInternal(RelNode rel,
       RexSimplify simplify, RexUnknownAs unknownAs, List<RexNode> expList,
-      RelOptPredicateList predicates) {
+      RelOptPredicateList predicates, boolean treatDynamicCallsAsNonConstant) {
     boolean changed = false;
     // Replace predicates on CASE to CASE on predicates.
     changed |= new CaseShuttle().mutate(expList);
@@ -567,7 +632,8 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
     List<Boolean> addCasts = new ArrayList<>();
     final List<RexNode> removableCasts = new ArrayList<>();
     findReducibleExps(rel.getCluster().getTypeFactory(), expList,
-        predicates.constantMap, constExps, addCasts, removableCasts);
+        predicates.constantMap, constExps, addCasts, removableCasts,
+        treatDynamicCallsAsNonConstant);
     if (constExps.isEmpty() && removableCasts.isEmpty()) {
       return changed;
     }
@@ -659,10 +725,10 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
   protected static void findReducibleExps(RelDataTypeFactory typeFactory,
       List<RexNode> exps, ImmutableMap<RexNode, RexNode> constants,
       List<RexNode> constExps, List<Boolean> addCasts,
-      List<RexNode> removableCasts) {
+      List<RexNode> removableCasts, boolean treatDynamicCallsAsNonConstant) {
     ReducibleExprLocator gardener =
         new ReducibleExprLocator(typeFactory, constants, constExps,
-            addCasts, removableCasts);
+            addCasts, removableCasts, treatDynamicCallsAsNonConstant);
     for (RexNode exp : exps) {
       gardener.analyze(exp);
     }
@@ -828,6 +894,8 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
 
     private final RelDataTypeFactory typeFactory;
 
+    private final boolean treatDynamicCallsAsNonConstant;
+
     private final List<Constancy> stack = new ArrayList<>();
 
     private final ImmutableMap<RexNode, RexNode> constants;
@@ -842,7 +910,8 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
 
     ReducibleExprLocator(RelDataTypeFactory typeFactory,
         ImmutableMap<RexNode, RexNode> constants, List<RexNode> constExprs,
-        List<Boolean> addCasts, List<RexNode> removableCasts) {
+        List<Boolean> addCasts, List<RexNode> removableCasts,
+        boolean treatDynamicCallsAsNonConstant) {
       // go deep
       super(true);
       this.typeFactory = typeFactory;
@@ -850,6 +919,7 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
       this.constExprs = constExprs;
       this.addCasts = addCasts;
       this.removableCasts = removableCasts;
+      this.treatDynamicCallsAsNonConstant = treatDynamicCallsAsNonConstant;
     }
 
     public void analyze(RexNode exp) {
@@ -962,10 +1032,10 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
       // be non-deterministic.
       if (!call.getOperator().isDeterministic()) {
         callConstancy = Constancy.NON_CONSTANT;
-      } else if (call.getOperator().isDynamicFunction()) {
-        // We can reduce the call to a constant, but we can't
-        // cache the plan if the function is dynamic.
-        // For now, treat it same as non-deterministic.
+      } else if (treatDynamicCallsAsNonConstant
+          && call.getOperator().isDynamicFunction()) {
+        // In some circumstances,, we should avoid caching the plan if we have dynamic functions.
+        // If desired, treat this situation the same as a non-deterministic function.
         callConstancy = Constancy.NON_CONSTANT;
       }
 
@@ -1069,6 +1139,117 @@ public abstract class ReduceExpressionsRule extends RelOptRule {
       }
     }
   }
+
+  /**
+   * An immutable object capturing the set of options to be used for reducing
+   * expressions.
+   */
+  public static final class ReductionOptions {
+    private final boolean matchNullability;
+    private final boolean unknownAsFalse;
+    private final boolean treatDynamicCallsAsNonConstant;
+
+    private ReductionOptions(
+        boolean matchNullability,
+        boolean unknownAsFalse,
+        boolean treatDynamicCallsAsNonConstant) {
+      this.matchNullability = matchNullability;
+      this.unknownAsFalse = unknownAsFalse;
+      this.treatDynamicCallsAsNonConstant = treatDynamicCallsAsNonConstant;
+    }
+
+    /**
+     * Update the options to control dynamic treatment.
+     *
+     * @param treatDynamicCallsAsNonConstant Whether to treat dynamic functions as
+     *                                       non-constants (defaults to true)
+     * @return A new set of ReductionOptions
+     */
+    public ReductionOptions treatDynamicCallsAsNonConstant(
+        boolean treatDynamicCallsAsNonConstant) {
+      return new ReductionOptions(
+          this.matchNullability,
+          this.unknownAsFalse,
+          treatDynamicCallsAsNonConstant);
+    }
+
+    /**
+     * <p> Update the options to control nullability matching.
+     *
+     * <p>The {@code matchNullability} flag comes into play when reducing a
+     * expression whose type is nullable. Suppose we are reducing an expression
+     * {@code CASE WHEN 'a' = 'a' THEN 1 ELSE NULL END}. Before reduction the
+     * type is {@code INTEGER} (nullable), but after reduction the literal 1 has
+     * type {@code INTEGER NOT NULL}.
+     *
+     * <p>In some situations it is more important to preserve types; in this
+     * case you should use {@code matchNullability = true} (which used to be
+     * the default behavior of this method), and it will cast the literal to
+     * {@code INTEGER} (nullable).
+     *
+     * <p>In other situations, you would rather propagate the new stronger type,
+     * because it may allow further optimizations later; pass
+     * {@code matchNullability = false} and no cast will be added, but you may
+     * need to adjust types elsewhere in the expression tree.
+     *
+     * @param matchNullability Whether Calcite should add a CAST to a literal
+     *                         resulting from simplification and expression if the
+     *                         expression had nullable type and the literal is
+     *                         NOT NULL
+     * @return A new set of ReductionOptions
+     */
+    public ReductionOptions matchNullability(
+        boolean matchNullability) {
+      return new ReductionOptions(
+          matchNullability,
+          this.unknownAsFalse,
+          this.treatDynamicCallsAsNonConstant);
+    }
+
+    /**
+     * Update the options to control behavior around UNKNOWN handling.
+     *
+     * @param unknownAsFalse Whether UNKNOWN will be treated as FALSE
+     * @return A new set of ReductionOptions
+     */
+    public ReductionOptions unknownAsFalse(boolean unknownAsFalse) {
+      return new ReductionOptions(
+          this.matchNullability,
+          unknownAsFalse,
+          this.treatDynamicCallsAsNonConstant
+          );
+    }
+
+    /**
+     * Whether Calcite should add a CAST to a literal resulting from simplification and
+     * expression if the expression had nullable type and the literal is NOT NULL
+     *
+     * @return True if reduction should cast for nullabilities.
+     */
+    public boolean isMatchNullability() {
+      return matchNullability;
+    }
+
+    /**
+     * Whether UNKNOWN will be treated as false. Typically used in the context of a filter
+     * reduction
+     *
+     * @return If Unknown (null) values are treated as false.
+     */
+    public boolean isUnknownAsFalse() {
+      return unknownAsFalse;
+    }
+
+    /**
+     * Whether to treat dynamic calls as non-constant for purposes of reduction.
+     *
+     * @return True if dynamic calls should be treated as non-constant.
+     */
+    public boolean isTreatDynamicCallsAsNonConstant() {
+      return treatDynamicCallsAsNonConstant;
+    }
+
+  }
 }
 
 // End ReduceExpressionsRule.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/0d0fd9e9/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 ae82438..6f9ea77 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -90,6 +90,7 @@ import org.apache.calcite.rel.rules.ProjectWindowTransposeRule;
 import org.apache.calcite.rel.rules.PruneEmptyRules;
 import org.apache.calcite.rel.rules.PushProjector;
 import org.apache.calcite.rel.rules.ReduceExpressionsRule;
+import org.apache.calcite.rel.rules.ReduceExpressionsRule.ProjectReduceExpressionsRule;
 import org.apache.calcite.rel.rules.SemiJoinFilterTransposeRule;
 import org.apache.calcite.rel.rules.SemiJoinJoinTransposeRule;
 import org.apache.calcite.rel.rules.SemiJoinProjectTransposeRule;
@@ -134,6 +135,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Properties;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 
 import static org.apache.calcite.plan.RelOptRule.none;
 import static org.apache.calcite.plan.RelOptRule.operand;
@@ -286,6 +288,38 @@ public class RelOptRulesTest extends RelOptTestBase {
     sql(sql).with(hepPlanner).checkUnchanged();
   }
 
+  @Test public void testReduceDynamic() {
+    testDynamic(true).get();
+  }
+
+  @Test public void testNoReduceDynamic() {
+    testDynamic(false).get();
+  }
+
+  /**
+   * Test reduction or not of a dynamic function.
+   *
+   * @param allowReduce Whether to allow dynamic functions to be reduced.
+   * @return The supplier to be executed in the context of the original test to ensure correct
+   *         test name mapping.
+   */
+  private Supplier<Void> testDynamic(boolean allowReduce) {
+    HepProgramBuilder builder = new HepProgramBuilder();
+    RelOptRule rule = new ProjectReduceExpressionsRule(
+        LogicalProject.class,
+        ReduceExpressionsRule.DEFAULT_OPTIONS.treatDynamicCallsAsNonConstant(!allowReduce),
+        RelFactories.LOGICAL_BUILDER);
+    builder.addRuleInstance(rule);
+    HepPlanner hepPlanner = new HepPlanner(builder.build());
+    final String sql = "select USER from emp";
+
+    // return a supplier to be executed in the context of the original test method.
+    return () -> {
+      checkPlanning(tester, null, hepPlanner, sql, !allowReduce);
+      return null;
+    };
+  }
+
   @Test public void testProjectToWindowRuleForMultipleWindows() {
     HepProgram preProgram = new HepProgramBuilder()
         .build();

http://git-wip-us.apache.org/repos/asf/calcite/blob/0d0fd9e9/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 23bf02a..1c17663 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -6842,6 +6842,28 @@ LogicalAggregate(group=[{0, 1, 2}])
 ]]>
         </Resource>
     </TestCase>
+    <TestCase name="testReduceDynamic">
+        <Resource name="planAfter">
+            <![CDATA[
+LogicalProject(USER=['sa'])
+  LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalProject(USER=[USER])
+  LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+    </TestCase>
+    <TestCase name="testNoReduceDynamic">
+        <Resource name="planBefore">
+            <![CDATA[
+LogicalProject(USER=[USER])
+  LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+        </Resource>
+    </TestCase>             
     <TestCase name="testReduceNullableToNotNull">
         <Resource name="sql">
             <![CDATA[select