[CALCITE-3409] Add a method in RelOptMaterializations to allow registering UnifyRule (xzh)

Close apache/calcite#2094
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptMaterializations.java b/core/src/main/java/org/apache/calcite/plan/RelOptMaterializations.java
index abe2fbd..f91f560 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptMaterializations.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptMaterializations.java
@@ -61,6 +61,23 @@
    */
   public static List<Pair<RelNode, List<RelOptMaterialization>>> useMaterializedViews(
       final RelNode rel, List<RelOptMaterialization> materializations) {
+    return useMaterializedViews(rel, materializations, SubstitutionVisitor.DEFAULT_RULES);
+  }
+
+  /**
+   * Returns a list of RelNode transformed from all possible combination of
+   * materialized view uses. Big queries will likely have more than one
+   * transformed RelNode, e.g., (t1 group by c1) join (t2 group by c2).
+   * In addition, you can add custom materialized view recognition rules.
+   * @param rel               the original RelNode
+   * @param materializations  the materialized view list
+   * @param materializationRules the materialized view recognition rules
+   * @return the list of transformed RelNode together with their corresponding
+   *         materialized views used in the transformation.
+   */
+  public static List<Pair<RelNode, List<RelOptMaterialization>>> useMaterializedViews(
+      final RelNode rel, List<RelOptMaterialization> materializations,
+      List<SubstitutionVisitor.UnifyRule> materializationRules) {
     final List<RelOptMaterialization> applicableMaterializations =
         getApplicableMaterializations(rel, materializations);
     final List<Pair<RelNode, List<RelOptMaterialization>>> applied =
@@ -70,7 +87,7 @@
       int count = applied.size();
       for (int i = 0; i < count; i++) {
         Pair<RelNode, List<RelOptMaterialization>> current = applied.get(i);
-        List<RelNode> sub = substitute(current.left, m);
+        List<RelNode> sub = substitute(current.left, m, materializationRules);
         if (!sub.isEmpty()) {
           ImmutableList.Builder<RelOptMaterialization> builder =
               ImmutableList.builder();
@@ -170,7 +187,8 @@
   }
 
   private static List<RelNode> substitute(
-      RelNode root, RelOptMaterialization materialization) {
+      RelNode root, RelOptMaterialization materialization,
+      List<SubstitutionVisitor.UnifyRule> materializationRules) {
     // First, if the materialization is in terms of a star table, rewrite
     // the query in terms of the star table.
     if (materialization.starRelOptTable != null) {
@@ -214,7 +232,10 @@
     hepPlanner.setRoot(root);
     root = hepPlanner.findBestExp();
 
-    return new SubstitutionVisitor(target, root).go(materialization.tableRel);
+    return new SubstitutionVisitor(target, root, ImmutableList.
+        <SubstitutionVisitor.UnifyRule>builder()
+        .addAll(materializationRules)
+        .build()).go(materialization.tableRel);
   }
 
   /**
diff --git a/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java b/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java
index 60a9713..c0899fa 100644
--- a/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java
+++ b/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java
@@ -126,7 +126,7 @@
 public class SubstitutionVisitor {
   private static final boolean DEBUG = CalciteSystemProperty.DEBUG.value();
 
-  protected static final ImmutableList<UnifyRule> DEFAULT_RULES =
+  public static final ImmutableList<UnifyRule> DEFAULT_RULES =
       ImmutableList.of(
           TrivialRule.INSTANCE,
           ScanToCalcUnifyRule.INSTANCE,
@@ -862,7 +862,7 @@
    * <p>The rule declares the query and target types; this allows the
    * engine to fire only a few rules in a given context.</p>
    */
-  protected abstract static class UnifyRule {
+  public abstract static class UnifyRule {
     protected final int slotCount;
     protected final Operand queryOperand;
     protected final Operand targetOperand;
@@ -929,7 +929,7 @@
   /**
    * Arguments to an application of a {@link UnifyRule}.
    */
-  protected class UnifyRuleCall {
+  public class UnifyRuleCall {
     protected final UnifyRule rule;
     public final MutableRel query;
     public final MutableRel target;
@@ -984,7 +984,7 @@
    * contains {@code target}. {@code stopTrying} indicates whether there's
    * no need to do matching for the same query node again.
    */
-  protected static class UnifyResult {
+  public static class UnifyResult {
     private final UnifyRuleCall call;
     private final MutableRel result;
     private final boolean stopTrying;
@@ -999,7 +999,7 @@
   }
 
   /** Abstract base class for implementing {@link UnifyRule}. */
-  protected abstract static class AbstractUnifyRule extends UnifyRule {
+  public abstract static class AbstractUnifyRule extends UnifyRule {
     @SuppressWarnings("method.invocation.invalid")
     protected AbstractUnifyRule(Operand queryOperand, Operand targetOperand,
         int slotCount) {
@@ -1754,7 +1754,7 @@
   }
 
   /** Explain filtering condition and projections from MutableCalc. */
-  private static Pair<RexNode, List<RexNode>> explainCalc(MutableCalc calc) {
+  public static Pair<RexNode, List<RexNode>> explainCalc(MutableCalc calc) {
     final RexShuttle shuttle = getExpandShuttle(calc.program);
     final RexNode condition = shuttle.apply(calc.program.getCondition());
     final List<RexNode> projects = new ArrayList<>();
@@ -2106,7 +2106,7 @@
   }
 
   /** Operand to a {@link UnifyRule}. */
-  protected abstract static class Operand {
+  public abstract static class Operand {
     protected final Class<? extends MutableRel> clazz;
 
     protected Operand(Class<? extends MutableRel> clazz) {
diff --git a/core/src/test/java/org/apache/calcite/materialize/CustomMaterializedViewRecognitionRuleTest.java b/core/src/test/java/org/apache/calcite/materialize/CustomMaterializedViewRecognitionRuleTest.java
new file mode 100644
index 0000000..8aa1653
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/materialize/CustomMaterializedViewRecognitionRuleTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.materialize;
+
+import org.apache.calcite.plan.RelOptMaterialization;
+import org.apache.calcite.plan.RelOptMaterializations;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelTraitDef;
+import org.apache.calcite.plan.SubstitutionVisitor;
+import org.apache.calcite.plan.SubstitutionVisitor.UnifyRule;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.mutable.MutableCalc;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+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.schema.SchemaPlus;
+import org.apache.calcite.schema.impl.AbstractTable;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.parser.SqlParser;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.test.CalciteAssert;
+import org.apache.calcite.test.SqlToRelTestBase;
+import org.apache.calcite.tools.Frameworks;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.NlsString;
+import org.apache.calcite.util.Pair;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.calcite.test.Matchers.isLinux;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Unit tests for {@link RelOptMaterializations#useMaterializedViews}.
+ */
+public class CustomMaterializedViewRecognitionRuleTest extends SqlToRelTestBase {
+
+  public static Frameworks.ConfigBuilder config() {
+    final SchemaPlus rootSchema = Frameworks.createRootSchema(true);
+    rootSchema.add("mv0", new AbstractTable() {
+      @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+        return typeFactory.builder()
+            .add("empno", SqlTypeName.INTEGER)
+            .add("ename", SqlTypeName.VARCHAR)
+            .add("job", SqlTypeName.VARCHAR)
+            .add("mgr", SqlTypeName.SMALLINT)
+            .add("hiredate", SqlTypeName.DATE)
+            .add("sal", SqlTypeName.DECIMAL)
+            .add("comm", SqlTypeName.DECIMAL)
+            .add("deptno", SqlTypeName.TINYINT)
+            .build();
+      }
+    });
+    return Frameworks.newConfigBuilder()
+        .parserConfig(SqlParser.Config.DEFAULT)
+        .defaultSchema(
+            CalciteAssert.addSchema(rootSchema, CalciteAssert.SchemaSpec.SCOTT_WITH_TEMPORAL))
+        .traitDefs((List<RelTraitDef>) null);
+  }
+
+  @Test void testCushionLikeOperatorRecognitionRule() {
+    final RelBuilder relBuilder = RelBuilder.create(config().build());
+    final RelNode query = relBuilder.scan("EMP")
+        .filter(
+            relBuilder.call(SqlStdOperatorTable.LIKE,
+            relBuilder.field(1), relBuilder.literal("ABCD%")))
+        .build();
+    final RelNode target = relBuilder.scan("EMP")
+        .filter(
+            relBuilder.call(SqlStdOperatorTable.LIKE,
+            relBuilder.field(1), relBuilder.literal("ABC%")))
+        .build();
+    final RelNode replacement = relBuilder.scan("mv0").build();
+    final RelOptMaterialization relOptMaterialization =
+        new RelOptMaterialization(replacement,
+            target, null, Lists.newArrayList("mv0"));
+    final List<UnifyRule> rules = new ArrayList<>();
+    rules.addAll(SubstitutionVisitor.DEFAULT_RULES);
+    rules.add(CustomizedMaterializationRule.INSTANCE);
+    final List<Pair<RelNode, List<RelOptMaterialization>>> relOptimized =
+        RelOptMaterializations.useMaterializedViews(query,
+            ImmutableList.of(relOptMaterialization), rules);
+    final String optimized = ""
+        + "LogicalCalc(expr#0..7=[{inputs}], expr#8=['ABCD%'], expr#9=[LIKE($t1, $t8)], proj#0."
+        + ".7=[{exprs}], $condition=[$t9])\n"
+        + "  LogicalProject(empno=[CAST($0):SMALLINT NOT NULL], ename=[CAST($1):VARCHAR(10)], "
+        + "job=[CAST($2):VARCHAR(9)], mgr=[CAST($3):SMALLINT], hiredate=[CAST($4):DATE], "
+        + "sal=[CAST($5):DECIMAL(7, 2)], comm=[CAST($6):DECIMAL(7, 2)], deptno=[CAST($7)"
+        + ":TINYINT])\n"
+        + "    LogicalTableScan(table=[[mv0]])\n";
+    final String relOptimizedStr = RelOptUtil.toString(relOptimized.get(0).getKey());
+    assertThat(relOptimizedStr, isLinux(optimized));
+  }
+
+  /**
+   * A customized materialization rule, which match expression of 'LIKE'
+   * and match by compensation.
+   */
+  private static class CustomizedMaterializationRule
+      extends SubstitutionVisitor.AbstractUnifyRule {
+
+    public static final CustomizedMaterializationRule INSTANCE =
+        new CustomizedMaterializationRule();
+
+    private CustomizedMaterializationRule() {
+      super(operand(MutableCalc.class, query(0)),
+          operand(MutableCalc.class, target(0)), 1);
+    }
+
+    @Override protected SubstitutionVisitor.UnifyResult apply(
+        SubstitutionVisitor.UnifyRuleCall call) {
+      final MutableCalc query = (MutableCalc) call.query;
+      final Pair<RexNode, List<RexNode>> queryExplained = SubstitutionVisitor.explainCalc(query);
+      final RexNode queryCond = queryExplained.left;
+      final List<RexNode> queryProjs = queryExplained.right;
+
+      final MutableCalc target = (MutableCalc) call.target;
+      final Pair<RexNode, List<RexNode>> targetExplained = SubstitutionVisitor.explainCalc(target);
+      final RexNode targetCond = targetExplained.left;
+      final List<RexNode> targetProjs = targetExplained.right;
+      final List parsedQ = parseLikeCondition(queryCond);
+      final List parsedT = parseLikeCondition(targetCond);
+      if (RexUtil.isIdentity(queryProjs, query.getInput().rowType)
+          && RexUtil.isIdentity(targetProjs, target.getInput().rowType)
+          && parsedQ != null && parsedT != null) {
+        if (parsedQ.get(0).equals(parsedT.get(0))) {
+          String literalQ = ((NlsString) parsedQ.get(1)).getValue();
+          String literalT = ((NlsString) parsedT.get(1)).getValue();
+          if (literalQ.endsWith("%") && literalT.endsWith("%")
+              && !literalQ.equals(literalT)
+              && literalQ.startsWith(literalT.substring(0, literalT.length() - 1))) {
+            return call.result(MutableCalc.of(target, query.program));
+          }
+        }
+      }
+      return null;
+    }
+
+    private List parseLikeCondition(RexNode rexNode) {
+      if (rexNode instanceof RexCall) {
+        RexCall rexCall = (RexCall) rexNode;
+        if (rexCall.getKind() == SqlKind.LIKE
+            && rexCall.operands.get(0) instanceof RexInputRef
+            && rexCall.operands.get(1) instanceof RexLiteral) {
+          return ImmutableList.of(rexCall.operands.get(0),
+              ((RexLiteral) (rexCall.operands.get(1))).getValue());
+        }
+      }
+      return null;
+    }
+  }
+
+}