[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;
+ }
+ }
+
+}