blob: 3a2c5162be813395cafce1a718af5800574ed619 [file] [log] [blame]
/*
* 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.test;
import org.apache.calcite.plan.RelOptListener;
import org.apache.calcite.plan.RelOptMaterialization;
import org.apache.calcite.plan.hep.HepMatchOrder;
import org.apache.calcite.plan.hep.HepPlanner;
import org.apache.calcite.plan.hep.HepProgram;
import org.apache.calcite.plan.hep.HepProgramBuilder;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelRoot;
import org.apache.calcite.rel.externalize.RelDotWriter;
import org.apache.calcite.rel.logical.LogicalIntersect;
import org.apache.calcite.rel.logical.LogicalUnion;
import org.apache.calcite.rel.rules.CoerceInputsRule;
import org.apache.calcite.rel.rules.CoreRules;
import org.apache.calcite.sql.SqlExplainLevel;
import com.google.common.collect.ImmutableList;
import org.junit.jupiter.api.Test;
import java.io.PrintWriter;
import java.io.StringWriter;
import static org.apache.calcite.test.Matchers.isLinux;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* HepPlannerTest is a unit test for {@link HepPlanner}. See
* {@link RelOptRulesTest} for an explanation of how to add tests; the tests in
* this class are targeted at exercising the planner, and use specific rules for
* convenience only, whereas the tests in that class are targeted at exercising
* specific rules, and use the planner for convenience only. Hence the split.
*/
class HepPlannerTest extends RelOptTestBase {
//~ Static fields/initializers ---------------------------------------------
private static final String UNION_TREE =
"(select name from dept union select ename from emp)"
+ " union (select ename from bonus)";
private static final String COMPLEX_UNION_TREE = "select * from (\n"
+ " select ENAME, 50011895 as cat_id, '1' as cat_name, 1 as require_free_postage, 0 as require_15return, 0 as require_48hour,1 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50011895 union all\n"
+ " select ENAME, 50013023 as cat_id, '2' as cat_name, 0 as require_free_postage, 0 as require_15return, 0 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50013023 union all\n"
+ " select ENAME, 50013032 as cat_id, '3' as cat_name, 0 as require_free_postage, 0 as require_15return, 0 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50013032 union all\n"
+ " select ENAME, 50013024 as cat_id, '4' as cat_name, 0 as require_free_postage, 0 as require_15return, 0 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50013024 union all\n"
+ " select ENAME, 50004204 as cat_id, '5' as cat_name, 0 as require_free_postage, 0 as require_15return, 0 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50004204 union all\n"
+ " select ENAME, 50013043 as cat_id, '6' as cat_name, 0 as require_free_postage, 0 as require_15return, 0 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50013043 union all\n"
+ " select ENAME, 290903 as cat_id, '7' as cat_name, 1 as require_free_postage, 0 as require_15return, 0 as require_48hour,1 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 290903 union all\n"
+ " select ENAME, 50008261 as cat_id, '8' as cat_name, 1 as require_free_postage, 0 as require_15return, 0 as require_48hour,1 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50008261 union all\n"
+ " select ENAME, 124478013 as cat_id, '9' as cat_name, 0 as require_free_postage, 0 as require_15return, 1 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 124478013 union all\n"
+ " select ENAME, 124472005 as cat_id, '10' as cat_name, 0 as require_free_postage, 0 as require_15return, 1 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 124472005 union all\n"
+ " select ENAME, 50013475 as cat_id, '11' as cat_name, 0 as require_free_postage, 1 as require_15return, 1 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50013475 union all\n"
+ " select ENAME, 50018263 as cat_id, '12' as cat_name, 0 as require_free_postage, 1 as require_15return, 1 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50018263 union all\n"
+ " select ENAME, 50013498 as cat_id, '13' as cat_name, 0 as require_free_postage, 1 as require_15return, 1 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50013498 union all\n"
+ " select ENAME, 350511 as cat_id, '14' as cat_name, 0 as require_free_postage, 1 as require_15return, 1 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 350511 union all\n"
+ " select ENAME, 50019790 as cat_id, '15' as cat_name, 0 as require_free_postage, 1 as require_15return, 1 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50019790 union all\n"
+ " select ENAME, 50015382 as cat_id, '16' as cat_name, 0 as require_free_postage, 1 as require_15return, 1 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50015382 union all\n"
+ " select ENAME, 350503 as cat_id, '17' as cat_name, 0 as require_free_postage, 1 as require_15return, 1 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 350503 union all\n"
+ " select ENAME, 350401 as cat_id, '18' as cat_name, 0 as require_free_postage, 1 as require_15return, 1 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 350401 union all\n"
+ " select ENAME, 50015560 as cat_id, '19' as cat_name, 0 as require_free_postage, 0 as require_15return, 0 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50015560 union all\n"
+ " select ENAME, 122658003 as cat_id, '20' as cat_name, 0 as require_free_postage, 1 as require_15return, 1 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 122658003 union all\n"
+ " select ENAME, 50022371 as cat_id, '100' as cat_name, 0 as require_free_postage, 0 as require_15return, 0 as require_48hour,0 as require_insurance from emp where EMPNO = 20171216 and MGR = 0 and ENAME = 'Y' and SAL = 50022371\n"
+ ") a";
//~ Methods ----------------------------------------------------------------
protected DiffRepository getDiffRepos() {
return DiffRepository.lookup(HepPlannerTest.class);
}
@Test void testRuleClass() {
// Verify that an entire class of rules can be applied.
HepProgramBuilder programBuilder = HepProgram.builder();
programBuilder.addRuleClass(CoerceInputsRule.class);
HepPlanner planner =
new HepPlanner(
programBuilder.build());
planner.addRule(
CoerceInputsRule.Config.DEFAULT
.withCoerceNames(false)
.withConsumerRelClass(LogicalUnion.class)
.toRule());
planner.addRule(
CoerceInputsRule.Config.DEFAULT
.withCoerceNames(false)
.withConsumerRelClass(LogicalIntersect.class)
.withDescription("CoerceInputsRule:Intersection") // TODO
.toRule());
final String sql = "(select name from dept union select ename from emp)\n"
+ "intersect (select fname from customer.contact)";
sql(sql).with(planner).check();
}
@Test void testRuleDescription() {
// Verify that a rule can be applied via its description.
HepProgramBuilder programBuilder = HepProgram.builder();
programBuilder.addRuleByDescription("FilterToCalcRule");
HepPlanner planner =
new HepPlanner(
programBuilder.build());
planner.addRule(CoreRules.FILTER_TO_CALC);
final String sql = "select name from sales.dept where deptno=12";
sql(sql).with(planner).check();
}
/**
* Ensures {@link org.apache.calcite.rel.AbstractRelNode} digest does not include
* full digest tree.
*/
@Test void relDigestLength() {
HepProgramBuilder programBuilder = HepProgram.builder();
HepPlanner planner =
new HepPlanner(
programBuilder.build());
StringBuilder sb = new StringBuilder();
final int n = 10;
sb.append("select * from (");
sb.append("select name from sales.dept");
for (int i = 0; i < n; i++) {
sb.append(" union all select name from sales.dept");
}
sb.append(")");
RelRoot root = tester.convertSqlToRel(sb.toString());
planner.setRoot(root.rel);
RelNode best = planner.findBestExp();
// Good digest should look like rel#66:LogicalProject(input=rel#64:LogicalUnion)
// Bad digest includes full tree like rel#66:LogicalProject(input=rel#64:LogicalUnion(...))
// So the assertion is to ensure digest includes LogicalUnion exactly once
assertIncludesExactlyOnce("best.getDescription()",
best.toString(), "LogicalUnion");
assertIncludesExactlyOnce("best.getDigest()",
best.getDigest(), "LogicalUnion");
}
@Test void testPlanToDot() {
HepProgramBuilder programBuilder = HepProgram.builder();
HepPlanner planner =
new HepPlanner(
programBuilder.build());
RelRoot root = tester.convertSqlToRel("select name from sales.dept");
planner.setRoot(root.rel);
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
RelDotWriter planWriter = new RelDotWriter(pw, SqlExplainLevel.EXPPLAN_ATTRIBUTES, false);
planner.getRoot().explain(planWriter);
String planStr = sw.toString();
assertThat(
planStr, isLinux("digraph {\n"
+ "\"LogicalTableScan\\ntable = [CATALOG, SA\\nLES, DEPT]\\n\" -> "
+ "\"LogicalProject\\nNAME = $1\\n\" [label=\"0\"]\n"
+ "}\n"));
}
private void assertIncludesExactlyOnce(String message, String digest, String substring) {
int pos = 0;
int cnt = 0;
while (pos >= 0) {
pos = digest.indexOf(substring, pos + 1);
if (pos > 0) {
cnt++;
}
}
assertEquals(1, cnt,
() -> message + " should include <<" + substring + ">> exactly once"
+ ", actual value is " + digest);
}
@Test void testMatchLimitOneTopDown() {
// Verify that only the top union gets rewritten.
HepProgramBuilder programBuilder = HepProgram.builder();
programBuilder.addMatchOrder(HepMatchOrder.TOP_DOWN);
programBuilder.addMatchLimit(1);
programBuilder.addRuleInstance(CoreRules.UNION_TO_DISTINCT);
sql(UNION_TREE).with(programBuilder.build()).check();
}
@Test void testMatchLimitOneBottomUp() {
// Verify that only the bottom union gets rewritten.
HepProgramBuilder programBuilder = HepProgram.builder();
programBuilder.addMatchLimit(1);
programBuilder.addMatchOrder(HepMatchOrder.BOTTOM_UP);
programBuilder.addRuleInstance(CoreRules.UNION_TO_DISTINCT);
sql(UNION_TREE).with(programBuilder.build()).check();
}
@Test void testMatchUntilFixpoint() {
// Verify that both unions get rewritten.
HepProgramBuilder programBuilder = HepProgram.builder();
programBuilder.addMatchLimit(HepProgram.MATCH_UNTIL_FIXPOINT);
programBuilder.addRuleInstance(CoreRules.UNION_TO_DISTINCT);
sql(UNION_TREE).with(programBuilder.build()).check();
}
@Test void testReplaceCommonSubexpression() {
// Note that here it may look like the rule is firing
// twice, but actually it's only firing once on the
// common sub-expression. The purpose of this test
// is to make sure the planner can deal with
// rewriting something used as a common sub-expression
// twice by the same parent (the join in this case).
final String sql = "select d1.deptno from (select * from dept) d1,\n"
+ "(select * from dept) d2";
sql(sql).withRule(CoreRules.PROJECT_REMOVE).check();
}
/** Tests that if two relational expressions are equivalent, the planner
* notices, and only applies the rule once. */
@Test void testCommonSubExpression() {
// In the following,
// (select 1 from dept where abs(-1)=20)
// occurs twice, but it's a common sub-expression, so the rule should only
// apply once.
HepProgramBuilder programBuilder = HepProgram.builder();
programBuilder.addRuleInstance(CoreRules.FILTER_TO_CALC);
final HepTestListener listener = new HepTestListener(0);
HepPlanner planner = new HepPlanner(programBuilder.build());
planner.addListener(listener);
final String sql = "(select 1 from dept where abs(-1)=20)\n"
+ "union all\n"
+ "(select 1 from dept where abs(-1)=20)";
planner.setRoot(tester.convertSqlToRel(sql).rel);
RelNode bestRel = planner.findBestExp();
assertThat(bestRel.getInput(0).equals(bestRel.getInput(1)), is(true));
assertThat(listener.getApplyTimes() == 1, is(true));
}
@Test void testSubprogram() {
// Verify that subprogram gets re-executed until fixpoint.
// In this case, the first time through we limit it to generate
// only one calc; the second time through it will generate
// a second calc, and then merge them.
HepProgramBuilder subprogramBuilder = HepProgram.builder();
subprogramBuilder.addMatchOrder(HepMatchOrder.TOP_DOWN);
subprogramBuilder.addMatchLimit(1);
subprogramBuilder.addRuleInstance(CoreRules.PROJECT_TO_CALC);
subprogramBuilder.addRuleInstance(CoreRules.FILTER_TO_CALC);
subprogramBuilder.addRuleInstance(CoreRules.CALC_MERGE);
HepProgramBuilder programBuilder = HepProgram.builder();
programBuilder.addSubprogram(subprogramBuilder.build());
final String sql = "select upper(ename) from\n"
+ "(select lower(ename) as ename from emp where empno = 100)";
sql(sql).with(programBuilder.build()).check();
}
@Test void testGroup() {
// Verify simultaneous application of a group of rules.
// Intentionally add them in the wrong order to make sure
// that order doesn't matter within the group.
HepProgramBuilder programBuilder = HepProgram.builder();
programBuilder.addGroupBegin();
programBuilder.addRuleInstance(CoreRules.CALC_MERGE);
programBuilder.addRuleInstance(CoreRules.PROJECT_TO_CALC);
programBuilder.addRuleInstance(CoreRules.FILTER_TO_CALC);
programBuilder.addGroupEnd();
final String sql = "select upper(name) from dept where deptno=20";
sql(sql).with(programBuilder.build()).check();
}
@Test void testGC() {
HepProgramBuilder programBuilder = HepProgram.builder();
programBuilder.addMatchOrder(HepMatchOrder.TOP_DOWN);
programBuilder.addRuleInstance(CoreRules.CALC_MERGE);
programBuilder.addRuleInstance(CoreRules.PROJECT_TO_CALC);
programBuilder.addRuleInstance(CoreRules.FILTER_TO_CALC);
HepPlanner planner = new HepPlanner(programBuilder.build());
planner.setRoot(
tester.convertSqlToRel("select upper(name) from dept where deptno=20").rel);
planner.findBestExp();
// Reuse of HepPlanner (should trigger GC).
planner.setRoot(
tester.convertSqlToRel("select upper(name) from dept where deptno=20").rel);
planner.findBestExp();
}
@Test void testRelNodeCacheWithDigest() {
HepProgramBuilder programBuilder = HepProgram.builder();
HepPlanner planner =
new HepPlanner(
programBuilder.build());
String query = "(select n_nationkey from SALES.CUSTOMER) union all\n"
+ "(select n_name from CUSTOMER_MODIFIABLEVIEW)";
sql(query).withTester(t -> createDynamicTester())
.withDecorrelation(true)
.with(programBuilder.build())
.with(planner)
.checkUnchanged();
}
@Test void testRuleApplyCount() {
final long applyTimes1 = checkRuleApplyCount(HepMatchOrder.ARBITRARY);
assertThat(applyTimes1, is(316L));
final long applyTimes2 = checkRuleApplyCount(HepMatchOrder.DEPTH_FIRST);
assertThat(applyTimes2, is(87L));
}
@Test void testMaterialization() {
HepPlanner planner = new HepPlanner(HepProgram.builder().build());
RelNode tableRel = tester.convertSqlToRel("select * from dept").rel;
RelNode queryRel = tableRel;
RelOptMaterialization mat1 = new RelOptMaterialization(
tableRel, queryRel, null, ImmutableList.of("default", "mv"));
planner.addMaterialization(mat1);
assertEquals(planner.getMaterializations().size(), 1);
assertEquals(planner.getMaterializations().get(0), mat1);
planner.clear();
assertEquals(planner.getMaterializations().size(), 0);
}
private long checkRuleApplyCount(HepMatchOrder matchOrder) {
final HepProgramBuilder programBuilder = HepProgram.builder();
programBuilder.addMatchOrder(matchOrder);
programBuilder.addRuleInstance(CoreRules.FILTER_REDUCE_EXPRESSIONS);
programBuilder.addRuleInstance(CoreRules.PROJECT_REDUCE_EXPRESSIONS);
final HepTestListener listener = new HepTestListener(0);
HepPlanner planner = new HepPlanner(programBuilder.build());
planner.addListener(listener);
planner.setRoot(tester.convertSqlToRel(COMPLEX_UNION_TREE).rel);
planner.findBestExp();
return listener.getApplyTimes();
}
/** Listener for HepPlannerTest; counts how many times rules fire. */
private static class HepTestListener implements RelOptListener {
private long applyTimes;
HepTestListener(long applyTimes) {
this.applyTimes = applyTimes;
}
long getApplyTimes() {
return applyTimes;
}
@Override public void relEquivalenceFound(RelEquivalenceEvent event) {
}
@Override public void ruleAttempted(RuleAttemptedEvent event) {
if (event.isBefore()) {
++applyTimes;
}
}
@Override public void ruleProductionSucceeded(RuleProductionEvent event) {
}
@Override public void relDiscarded(RelDiscardedEvent event) {
}
@Override public void relChosen(RelChosenEvent event) {
}
}
}