blob: f94242a5c2f141a8d83cfe78b9306d308b69abe6 [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.adapter.enumerable.EnumerableConvention;
import org.apache.calcite.plan.Context;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptPlanner;
import org.apache.calcite.plan.RelOptRule;
import org.apache.calcite.plan.RelOptUtil;
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.plan.volcano.VolcanoPlanner;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelRoot;
import org.apache.calcite.rel.core.RelFactories;
import org.apache.calcite.rel.metadata.ChainedRelMetadataProvider;
import org.apache.calcite.rel.metadata.DefaultRelMetadataProvider;
import org.apache.calcite.rel.metadata.RelMetadataProvider;
import org.apache.calcite.runtime.FlatLists;
import org.apache.calcite.runtime.Hook;
import org.apache.calcite.sql.test.SqlTestFactory;
import org.apache.calcite.sql.validate.SqlConformance;
import org.apache.calcite.sql2rel.RelDecorrelator;
import org.apache.calcite.sql2rel.SqlToRelConverter;
import org.apache.calcite.tools.RelBuilder;
import org.apache.calcite.util.Closer;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* RelOptTestBase is an abstract base for tests which exercise a planner and/or
* rules via {@link DiffRepository}.
*/
abstract class RelOptTestBase extends SqlToRelTestBase {
//~ Methods ----------------------------------------------------------------
@Override protected Tester createTester() {
return super.createTester().withDecorrelation(false);
}
protected Tester createDynamicTester() {
return getTesterWithDynamicTable();
}
/**
* Checks the plan for a given {@link RelNode} supplier before/after executing a given rule,
* with a pre-program to prepare the tree.
*
* @param tester Tester
* @param preProgram Program to execute before comparing before state
* @param planner Planner
* @param relFn {@link RelNode} supplier
* @param unchanged Whether the rule is to have no effect
*/
private void checkPlanning(Tester tester, HepProgram preProgram,
RelOptPlanner planner, Function<RelBuilder, RelNode> relFn, boolean unchanged) {
RelNode relInitial = relFn.apply(RelBuilder.create(RelBuilderTest.config().build()));
checkPlanning(tester, preProgram, planner, relInitial, unchanged);
}
/**
* Checks the plan for a SQL statement before/after executing a given rule,
* with a pre-program to prepare the tree.
*
* @param tester Tester
* @param preProgram Program to execute before comparing before state
* @param planner Planner
* @param sql SQL query
* @param unchanged Whether the rule is to have no effect
*/
private void checkPlanning(Tester tester, HepProgram preProgram,
RelOptPlanner planner, String sql, boolean unchanged) {
final DiffRepository diffRepos = getDiffRepos();
String sql2 = diffRepos.expand("sql", sql);
final RelRoot root = tester.convertSqlToRel(sql2);
final RelNode relInitial = root.rel;
checkPlanning(tester, preProgram, planner, relInitial, unchanged);
}
private void checkPlanning(Tester tester, HepProgram preProgram,
RelOptPlanner planner, RelNode relInitial, boolean unchanged) {
assertNotNull(relInitial);
final DiffRepository diffRepos = getDiffRepos();
List<RelMetadataProvider> list = new ArrayList<>();
list.add(DefaultRelMetadataProvider.INSTANCE);
RelMetadataProvider plannerChain =
ChainedRelMetadataProvider.of(list);
final RelOptCluster cluster = relInitial.getCluster();
cluster.setMetadataProvider(plannerChain);
RelNode relBefore;
if (preProgram == null) {
relBefore = relInitial;
} else {
HepPlanner prePlanner = new HepPlanner(preProgram);
prePlanner.setRoot(relInitial);
relBefore = prePlanner.findBestExp();
}
assertThat(relBefore, notNullValue());
final String planBefore = NL + RelOptUtil.toString(relBefore);
diffRepos.assertEquals("planBefore", "${planBefore}", planBefore);
SqlToRelTestBase.assertValid(relBefore);
if (planner instanceof VolcanoPlanner) {
relBefore = planner.changeTraits(relBefore,
relBefore.getTraitSet().replace(EnumerableConvention.INSTANCE));
}
planner.setRoot(relBefore);
RelNode r = planner.findBestExp();
if (tester.isLateDecorrelate()) {
final String planMid = NL + RelOptUtil.toString(r);
diffRepos.assertEquals("planMid", "${planMid}", planMid);
SqlToRelTestBase.assertValid(r);
final RelBuilder relBuilder =
RelFactories.LOGICAL_BUILDER.create(cluster, null);
r = RelDecorrelator.decorrelateQuery(r, relBuilder);
}
final String planAfter = NL + RelOptUtil.toString(r);
if (unchanged) {
assertThat(planAfter, is(planBefore));
} else {
diffRepos.assertEquals("planAfter", "${planAfter}", planAfter);
if (planBefore.equals(planAfter)) {
throw new AssertionError("Expected plan before and after is the same.\n"
+ "You must use unchanged=true or call checkUnchanged");
}
}
SqlToRelTestBase.assertValid(r);
}
/** Sets the SQL statement for a test. */
Sql sql(String sql) {
final Sql s =
new Sql(tester, sql, null, null, ImmutableMap.of(), ImmutableList.of(), null);
return s.withRelBuilderConfig(b -> b.withPruneInputOfAggregate(false));
}
/** Initiates a test case with a given {@link RelNode} supplier. */
Sql relFn(Function<RelBuilder, RelNode> relFn) {
return sql("?").relFn(relFn);
}
/** Allows fluent testing. */
class Sql {
private final Tester tester;
private final String sql;
private HepProgram preProgram;
private final RelOptPlanner planner;
private final ImmutableMap<Hook, Consumer> hooks;
private ImmutableList<Function<Tester, Tester>> transforms;
private final @Nullable Function<RelBuilder, RelNode> relFn;
Sql(Tester tester, String sql, HepProgram preProgram, RelOptPlanner planner,
ImmutableMap<Hook, Consumer> hooks,
ImmutableList<Function<Tester, Tester>> transforms,
@Nullable Function<RelBuilder, RelNode> relFn) {
this.tester = Objects.requireNonNull(tester, "tester");
this.sql = Objects.requireNonNull(sql, "sql");
if (sql.contains(" \n")) {
throw new AssertionError("trailing whitespace");
}
this.preProgram = preProgram;
this.planner = planner;
this.hooks = Objects.requireNonNull(hooks, "hooks");
this.transforms = Objects.requireNonNull(transforms, "transforms");
this.relFn = relFn;
}
Sql relFn(Function<RelBuilder, RelNode> relFn) {
return new Sql(tester, sql, null, null, ImmutableMap.of(), ImmutableList.of(), relFn);
}
public Sql withTester(UnaryOperator<Tester> transform) {
final Tester tester2 = transform.apply(tester);
return new Sql(tester2, sql, preProgram, planner, hooks, transforms, relFn);
}
public Sql withPre(HepProgram preProgram) {
return new Sql(tester, sql, preProgram, planner, hooks, transforms, relFn);
}
public Sql withPreRule(RelOptRule... rules) {
final HepProgramBuilder builder = HepProgram.builder();
for (RelOptRule rule : rules) {
builder.addRuleInstance(rule);
}
return withPre(builder.build());
}
public Sql with(HepPlanner hepPlanner) {
return new Sql(tester, sql, preProgram, hepPlanner, hooks, transforms, relFn);
}
public Sql with(HepProgram program) {
final HepPlanner hepPlanner = new HepPlanner(program);
return new Sql(tester, sql, preProgram, hepPlanner, hooks, transforms, relFn);
}
public Sql withRule(RelOptRule... rules) {
final HepProgramBuilder builder = HepProgram.builder();
for (RelOptRule rule : rules) {
builder.addRuleInstance(rule);
}
return with(builder.build());
}
/** Adds a transform that will be applied to {@link #tester}
* just before running the query. */
private Sql withTransform(Function<Tester, Tester> transform) {
final ImmutableList<Function<Tester, Tester>> transforms =
FlatLists.append(this.transforms, transform);
return new Sql(tester, sql, preProgram, planner, hooks, transforms, relFn);
}
/** Adds a hook and a handler for that hook. Calcite will create a thread
* hook (by calling {@link Hook#addThread(Consumer)})
* just before running the query, and remove the hook afterwards. */
public <T> Sql withHook(Hook hook, Consumer<T> handler) {
final ImmutableMap<Hook, Consumer> hooks =
FlatLists.append(this.hooks, hook, handler);
return new Sql(tester, sql, preProgram, planner, hooks, transforms, relFn);
}
// CHECKSTYLE: IGNORE 1
/** @deprecated Use {@link #withHook(Hook, Consumer)}. */
@SuppressWarnings("Guava")
@Deprecated // to be removed before 2.0
public <T> Sql withHook(Hook hook,
com.google.common.base.Function<T, Void> handler) {
return withHook(hook, (Consumer<T>) handler::apply);
}
public <V> Sql withProperty(Hook hook, V value) {
return withHook(hook, Hook.propertyJ(value));
}
public Sql expand(final boolean b) {
return withConfig(c -> c.withExpand(b));
}
public Sql withConfig(UnaryOperator<SqlToRelConverter.Config> transform) {
return withTransform(tester -> tester.withConfig(transform));
}
public Sql withRelBuilderConfig(
UnaryOperator<RelBuilder.Config> transform) {
return withConfig(c -> c.addRelBuilderConfigTransform(transform));
}
public Sql withLateDecorrelation(final boolean b) {
return withTransform(tester -> tester.withLateDecorrelation(b));
}
public Sql withDecorrelation(final boolean b) {
return withTransform(tester -> tester.withDecorrelation(b));
}
public Sql withTrim(final boolean b) {
return withTransform(tester -> tester.withTrim(b));
}
public Sql withCatalogReaderFactory(
SqlTestFactory.MockCatalogReaderFactory factory) {
return withTransform(tester -> tester.withCatalogReaderFactory(factory));
}
public Sql withConformance(final SqlConformance conformance) {
return withTransform(tester -> tester.withConformance(conformance));
}
public Sql withContext(final UnaryOperator<Context> transform) {
return withTransform(tester -> tester.withContext(transform));
}
/**
* Checks the plan for a SQL statement before/after executing a given rule,
* with a optional pre-program specified by {@link #withPre(HepProgram)}
* to prepare the tree.
*/
public void check() {
check(false);
}
/**
* Checks that the plan is the same before and after executing a given
* planner. Useful for checking circumstances where rules should not fire.
*/
public void checkUnchanged() {
check(true);
}
@SuppressWarnings("unchecked")
private void check(boolean unchanged) {
try (Closer closer = new Closer()) {
for (Map.Entry<Hook, Consumer> entry : hooks.entrySet()) {
closer.add(entry.getKey().addThread(entry.getValue()));
}
Tester t = tester;
for (Function<Tester, Tester> transform : transforms) {
t = transform.apply(t);
}
if (relFn != null) {
checkPlanning(t, preProgram, planner, relFn, unchanged);
} else {
checkPlanning(t, preProgram, planner, sql, unchanged);
}
}
}
}
}