blob: 862ca1cc9e66f9058cef26f6b4ec83cf18b02c36 [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.RelOptCluster;
import org.apache.calcite.plan.RelOptCost;
import org.apache.calcite.plan.RelOptPlanner;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.plan.volcano.VolcanoPlanner;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.Project;
import org.apache.calcite.rel.logical.LogicalCalc;
import org.apache.calcite.rel.metadata.DefaultRelMetadataProvider;
import org.apache.calcite.rel.metadata.JaninoRelMetadataProvider;
import org.apache.calcite.rel.metadata.MetadataHandlerProvider;
import org.apache.calcite.rel.metadata.ProxyingMetadataHandlerProvider;
import org.apache.calcite.rel.metadata.RelColumnOrigin;
import org.apache.calcite.rel.metadata.RelMetadataProvider;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.rex.RexProgram;
import org.apache.calcite.runtime.SqlFunctions;
import org.apache.calcite.sql.SqlExplainLevel;
import org.apache.calcite.sql.test.SqlTestFactory;
import org.apache.calcite.sql.test.SqlTester;
import org.apache.calcite.tools.RelBuilder;
import org.apache.calcite.util.ImmutableBitSet;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import org.hamcrest.Matcher;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Parameters for a Metadata test.
*/
public class RelMetadataFixture {
/** Default fixture.
*
* <p>Use this, or call the {@code withXxx} methods to make one with the
* properties you need. Fixtures are immutable, so whatever your test does
* to this fixture, it won't break other tests. */
public static final RelMetadataFixture DEFAULT =
new RelMetadataFixture(SqlToRelFixture.TESTER,
SqlTestFactory.INSTANCE, MetadataConfig.JANINO, RelSupplier.NONE,
false, r -> r)
.withFactory(f ->
f.withValidatorConfig(c -> c.withIdentifierExpansion(true))
.withSqlToRelConfig(c ->
c.withRelBuilderConfigTransform(b ->
b.withAggregateUnique(true)
.withPruneInputOfAggregate(false))));
public final SqlTester tester;
public final SqlTestFactory factory;
public final MetadataConfig metadataConfig;
public final RelSupplier relSupplier;
public final boolean convertAsCalc;
public final UnaryOperator<RelNode> relTransform;
private RelMetadataFixture(SqlTester tester,
SqlTestFactory factory, MetadataConfig metadataConfig,
RelSupplier relSupplier,
boolean convertAsCalc, UnaryOperator<RelNode> relTransform) {
this.tester = tester;
this.factory = factory;
this.metadataConfig = metadataConfig;
this.relSupplier = relSupplier;
this.convertAsCalc = convertAsCalc;
this.relTransform = relTransform;
}
//~ 'With' methods ---------------------------------------------------------
// Each method returns a copy of this fixture, changing the value of one
// property.
/** Creates a copy of this fixture that uses a given SQL query. */
public RelMetadataFixture withSql(String sql) {
final RelSupplier relSupplier = RelSupplier.of(sql);
if (relSupplier.equals(this.relSupplier)) {
return this;
}
return new RelMetadataFixture(tester, factory, metadataConfig, relSupplier,
convertAsCalc, relTransform);
}
/** Creates a copy of this fixture that uses a given function to create a
* {@link RelNode}. */
public RelMetadataFixture withRelFn(Function<RelBuilder, RelNode> relFn) {
final RelSupplier relSupplier =
RelSupplier.of(builder -> {
metadataConfig.applyMetadata(builder.getCluster());
return relFn.apply(builder);
});
if (relSupplier.equals(this.relSupplier)) {
return this;
}
return new RelMetadataFixture(tester, factory, metadataConfig, relSupplier,
convertAsCalc, relTransform);
}
public RelMetadataFixture withFactory(
UnaryOperator<SqlTestFactory> transform) {
final SqlTestFactory factory = transform.apply(this.factory);
return new RelMetadataFixture(tester, factory, metadataConfig, relSupplier,
convertAsCalc, relTransform);
}
public RelMetadataFixture withTester(UnaryOperator<SqlTester> transform) {
final SqlTester tester = transform.apply(this.tester);
return new RelMetadataFixture(tester, factory, metadataConfig, relSupplier,
convertAsCalc, relTransform);
}
public RelMetadataFixture withMetadataConfig(MetadataConfig metadataConfig) {
if (metadataConfig.equals(this.metadataConfig)) {
return this;
}
return new RelMetadataFixture(tester, factory, metadataConfig, relSupplier,
convertAsCalc, relTransform);
}
public RelMetadataFixture convertingProjectAsCalc() {
if (convertAsCalc) {
return this;
}
return new RelMetadataFixture(tester, factory, metadataConfig, relSupplier,
true, relTransform);
}
public RelMetadataFixture withCatalogReaderFactory(
SqlTestFactory.CatalogReaderFactory catalogReaderFactory) {
return withFactory(t -> t.withCatalogReader(catalogReaderFactory));
}
public RelMetadataFixture withCluster(UnaryOperator<RelOptCluster> factory) {
return withFactory(f -> f.withCluster(factory));
}
public RelMetadataFixture withRelTransform(UnaryOperator<RelNode> relTransform) {
final UnaryOperator<RelNode> relTransform1 =
this.relTransform.andThen(relTransform)::apply;
return new RelMetadataFixture(tester, factory, metadataConfig, relSupplier,
convertAsCalc, relTransform1);
}
//~ Helper methods ---------------------------------------------------------
// Don't use them too much. Write an assertXxx method if possible.
/** Only for use by RelSupplier. Must be package-private. */
RelNode sqlToRel(String sql) {
return tester.convertSqlToRel(factory, sql, false, false).rel;
}
/** Creates a {@link RelNode} from this fixture's supplier
* (see {@link #withSql(String)} and {@link #withRelFn(Function)}). */
public RelNode toRel() {
final RelNode rel = relSupplier.apply2(this);
metadataConfig.applyMetadata(rel.getCluster());
if (convertAsCalc) {
Project project = (Project) rel;
Preconditions.checkArgument(project.getVariablesSet().isEmpty(),
"Calc does not allow variables");
RexProgram program = RexProgram.create(
project.getInput().getRowType(),
project.getProjects(),
null,
project.getRowType(),
project.getCluster().getRexBuilder());
return LogicalCalc.create(project.getInput(), program);
}
return relTransform.apply(rel);
}
//~ Methods that execute tests ---------------------------------------------
/** Checks the CPU component of
* {@link RelNode#computeSelfCost(RelOptPlanner, RelMetadataQuery)}. */
@SuppressWarnings({"UnusedReturnValue"})
public RelMetadataFixture assertCpuCost(Matcher<Double> matcher,
String reason) {
RelNode rel = toRel();
RelOptCost cost = computeRelSelfCost(rel);
assertThat(reason + "\n"
+ "sql:" + relSupplier + "\n"
+ "plan:" + RelOptUtil.toString(rel, SqlExplainLevel.ALL_ATTRIBUTES),
cost.getCpu(), matcher);
return this;
}
private static RelOptCost computeRelSelfCost(RelNode rel) {
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
RelOptPlanner planner = new VolcanoPlanner();
return rel.computeSelfCost(planner, mq);
}
/** Checks {@link RelMetadataQuery#areRowsUnique(RelNode)} for all
* values of {@code ignoreNulls}. */
@SuppressWarnings({"UnusedReturnValue"})
public RelMetadataFixture assertRowsUnique(Matcher<Boolean> matcher,
String reason) {
return assertRowsUnique(false, matcher, reason)
.assertRowsUnique(true, matcher, reason);
}
/** Checks {@link RelMetadataQuery#areRowsUnique(RelNode)}. */
public RelMetadataFixture assertRowsUnique(boolean ignoreNulls,
Matcher<Boolean> matcher, String reason) {
RelNode rel = toRel();
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
Boolean rowsUnique = mq.areRowsUnique(rel, ignoreNulls);
assertThat(reason + "\n"
+ "sql:" + relSupplier + "\n"
+ "plan:" + RelOptUtil.toString(rel, SqlExplainLevel.ALL_ATTRIBUTES),
rowsUnique, matcher);
return this;
}
/** Checks {@link RelMetadataQuery#getPercentageOriginalRows(RelNode)}. */
@SuppressWarnings({"UnusedReturnValue"})
public RelMetadataFixture assertPercentageOriginalRows(Matcher<Double> matcher) {
RelNode rel = toRel();
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
Double result = mq.getPercentageOriginalRows(rel);
assertNotNull(result);
assertThat(result, matcher);
return this;
}
private RelMetadataFixture checkColumnOrigin(
Consumer<Set<RelColumnOrigin>> action) {
RelNode rel = toRel();
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
final Set<RelColumnOrigin> columnOrigins = mq.getColumnOrigins(rel, 0);
action.accept(columnOrigins);
return this;
}
/** Checks that {@link RelMetadataQuery#getColumnOrigins(RelNode, int)}
* for column 0 returns no origins. */
@SuppressWarnings({"UnusedReturnValue"})
public RelMetadataFixture assertColumnOriginIsEmpty() {
return checkColumnOrigin(result -> {
assertNotNull(result);
assertTrue(result.isEmpty());
});
}
private static void checkColumnOrigin(
RelColumnOrigin rco,
String expectedTableName,
String expectedColumnName,
boolean expectedDerived) {
RelOptTable actualTable = rco.getOriginTable();
List<String> actualTableName = actualTable.getQualifiedName();
assertThat(
Iterables.getLast(actualTableName),
equalTo(expectedTableName));
assertThat(
actualTable.getRowType()
.getFieldList()
.get(rco.getOriginColumnOrdinal())
.getName(),
equalTo(expectedColumnName));
assertThat(rco.isDerived(), equalTo(expectedDerived));
}
/** Checks that {@link RelMetadataQuery#getColumnOrigins(RelNode, int)}
* for column 0 returns one origin. */
@SuppressWarnings({"UnusedReturnValue"})
public RelMetadataFixture assertColumnOriginSingle(String expectedTableName,
String expectedColumnName, boolean expectedDerived) {
return checkColumnOrigin(result -> {
assertNotNull(result);
assertThat(result.size(), is(1));
RelColumnOrigin rco = result.iterator().next();
checkColumnOrigin(rco, expectedTableName, expectedColumnName,
expectedDerived);
});
}
/** Checks that {@link RelMetadataQuery#getColumnOrigins(RelNode, int)}
* for column 0 returns two origins. */
@SuppressWarnings({"UnusedReturnValue"})
public RelMetadataFixture assertColumnOriginDouble(
String expectedTableName1, String expectedColumnName1,
String expectedTableName2, String expectedColumnName2,
boolean expectedDerived) {
assertThat("required so that the test mechanism works", expectedTableName1,
not(is(expectedTableName2)));
return checkColumnOrigin(result -> {
assertNotNull(result);
assertThat(result.size(), is(2));
for (RelColumnOrigin rco : result) {
RelOptTable actualTable = rco.getOriginTable();
List<String> actualTableName = actualTable.getQualifiedName();
String actualUnqualifiedName = Iterables.getLast(actualTableName);
if (actualUnqualifiedName.equals(expectedTableName1)) {
checkColumnOrigin(rco, expectedTableName1, expectedColumnName1,
expectedDerived);
} else {
checkColumnOrigin(rco, expectedTableName2, expectedColumnName2,
expectedDerived);
}
}
});
}
/** Checks result of getting unique keys for SQL. */
@SuppressWarnings({"UnusedReturnValue"})
public RelMetadataFixture assertThatUniqueKeysAre(
ImmutableBitSet... expectedUniqueKeys) {
RelNode rel = toRel();
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
Set<ImmutableBitSet> result = mq.getUniqueKeys(rel);
assertThat(result, notNullValue());
assertEquals(ImmutableSortedSet.copyOf(expectedUniqueKeys),
ImmutableSortedSet.copyOf(result),
() -> "unique keys, sql: " + relSupplier + ", rel: " + RelOptUtil.toString(rel));
checkUniqueConsistent(rel);
return this;
}
/**
* Asserts that {@link RelMetadataQuery#getUniqueKeys(RelNode)}
* and {@link RelMetadataQuery#areColumnsUnique(RelNode, ImmutableBitSet)}
* return consistent results.
*/
private static void checkUniqueConsistent(RelNode rel) {
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
final Set<ImmutableBitSet> uniqueKeys = mq.getUniqueKeys(rel);
assertThat(uniqueKeys, notNullValue());
final ImmutableBitSet allCols =
ImmutableBitSet.range(0, rel.getRowType().getFieldCount());
for (ImmutableBitSet key : allCols.powerSet()) {
Boolean result2 = mq.areColumnsUnique(rel, key);
assertEquals(isUnique(uniqueKeys, key), SqlFunctions.isTrue(result2),
() -> "areColumnsUnique. key: " + key + ", uniqueKeys: " + uniqueKeys
+ ", rel: " + RelOptUtil.toString(rel));
}
}
/**
* Returns whether {@code key} is unique, that is, whether it or a subset
* is in {@code uniqueKeys}.
*/
private static boolean isUnique(Set<ImmutableBitSet> uniqueKeys,
ImmutableBitSet key) {
for (ImmutableBitSet uniqueKey : uniqueKeys) {
if (key.contains(uniqueKey)) {
return true;
}
}
return false;
}
/** Checks {@link RelMetadataQuery#getRowCount(RelNode)},
* {@link RelMetadataQuery#getMaxRowCount(RelNode)},
* and {@link RelMetadataQuery#getMinRowCount(RelNode)}. */
@SuppressWarnings({"UnusedReturnValue"})
public RelMetadataFixture assertThatRowCount(Matcher<Number> rowCountMatcher,
Matcher<Number> minRowCountMatcher, Matcher<Number> maxRowCountMatcher) {
final RelNode rel = toRel();
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
final Double rowCount = mq.getRowCount(rel);
assertThat(rowCount, notNullValue());
assertThat(rowCount, rowCountMatcher);
final Double min = mq.getMinRowCount(rel);
assertThat(min, notNullValue());
assertThat(min, minRowCountMatcher);
final Double max = mq.getMaxRowCount(rel);
assertThat(max, notNullValue());
assertThat(max, maxRowCountMatcher);
return this;
}
/** Checks {@link RelMetadataQuery#getSelectivity(RelNode, RexNode)}. */
public RelMetadataFixture assertThatSelectivity(Matcher<Double> matcher) {
RelNode rel = toRel();
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
Double result = mq.getSelectivity(rel, null);
assertThat(result, notNullValue());
assertThat(result, matcher);
return this;
}
/** Checks
* {@link RelMetadataQuery#getDistinctRowCount(RelNode, ImmutableBitSet, RexNode)}
* with a null predicate. */
public RelMetadataFixture assertThatDistinctRowCount(ImmutableBitSet groupKey,
Matcher<Double> matcher) {
return assertThatDistinctRowCount(r -> groupKey, matcher);
}
/** Checks
* {@link RelMetadataQuery#getDistinctRowCount(RelNode, ImmutableBitSet, RexNode)}
* with a null predicate, deriving the group key from the {@link RelNode}. */
public RelMetadataFixture assertThatDistinctRowCount(
Function<RelNode, ImmutableBitSet> groupKeyFn,
Matcher<Double> matcher) {
final RelNode rel = toRel();
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
final ImmutableBitSet groupKey = groupKeyFn.apply(rel);
Double result = mq.getDistinctRowCount(rel, groupKey, null);
assertThat(result, matcher);
return this;
}
/** Checks the {@link RelNode} produced by {@link #toRel}. */
public RelMetadataFixture assertThatRel(Matcher<RelNode> matcher) {
final RelNode rel = toRel();
assertThat(rel, matcher);
return this;
}
/** Shorthand for a call to {@link #assertThatNodeTypeCount(Matcher)}
* with a constant map. */
@SuppressWarnings({"rawtypes", "unchecked", "UnusedReturnValue"})
public RelMetadataFixture assertThatNodeTypeCountIs(
Class<? extends RelNode> k0, Integer v0, Object... rest) {
final ImmutableMap.Builder<Class<? extends RelNode>, Integer> b =
ImmutableMap.builder();
b.put(k0, v0);
for (int i = 0; i < rest.length;) {
b.put((Class) rest[i++], (Integer) rest[i++]);
}
return assertThatNodeTypeCount(is(b.build()));
}
/** Checks the number of each sub-class of {@link RelNode},
* calling {@link RelMetadataQuery#getNodeTypes(RelNode)}. */
public RelMetadataFixture assertThatNodeTypeCount(
Matcher<Map<Class<? extends RelNode>, Integer>> matcher) {
final RelNode rel = toRel();
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
final Multimap<Class<? extends RelNode>, RelNode> result = mq.getNodeTypes(rel);
assertThat(result, notNullValue());
final Map<Class<? extends RelNode>, Integer> resultCount = new HashMap<>();
for (Map.Entry<Class<? extends RelNode>, Collection<RelNode>> e : result.asMap().entrySet()) {
resultCount.put(e.getKey(), e.getValue().size());
}
assertThat(resultCount, matcher);
return this;
}
/** Checks {@link RelMetadataQuery#getUniqueKeys(RelNode)}. */
public RelMetadataFixture assertThatUniqueKeys(
Matcher<Iterable<ImmutableBitSet>> matcher) {
final RelNode rel = toRel();
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
final Set<ImmutableBitSet> result = mq.getUniqueKeys(rel);
assertThat(result, matcher);
return this;
}
/** Checks {@link RelMetadataQuery#areColumnsUnique(RelNode, ImmutableBitSet)}. */
public RelMetadataFixture assertThatAreColumnsUnique(ImmutableBitSet columns,
Matcher<Boolean> matcher) {
return assertThatAreColumnsUnique(r -> columns, r -> r, matcher);
}
/** Checks {@link RelMetadataQuery#areColumnsUnique(RelNode, ImmutableBitSet)},
* deriving parameters via functions. */
public RelMetadataFixture assertThatAreColumnsUnique(
Function<RelNode, ImmutableBitSet> columnsFn,
UnaryOperator<RelNode> relFn,
Matcher<Boolean> matcher) {
RelNode rel = toRel();
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
final ImmutableBitSet columns = columnsFn.apply(rel);
final RelNode rel2 = relFn.apply(rel);
final Boolean areColumnsUnique = mq.areColumnsUnique(rel2, columns);
assertThat(areColumnsUnique, matcher);
return this;
}
/** Checks {@link RelMetadataQuery#areRowsUnique(RelNode)}. */
@SuppressWarnings({"UnusedReturnValue"})
public RelMetadataFixture assertThatAreRowsUnique(Matcher<Boolean> matcher) {
RelNode rel = toRel();
final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
final Boolean areRowsUnique = mq.areRowsUnique(rel);
assertThat(areRowsUnique, matcher);
return this;
}
/**
* A configuration that describes how metadata should be configured.
*/
public static class MetadataConfig {
static final MetadataConfig JANINO =
new MetadataConfig("Janino",
JaninoRelMetadataProvider::of,
RelMetadataQuery.THREAD_PROVIDERS::get,
true);
static final MetadataConfig PROXYING =
new MetadataConfig("Proxying",
ProxyingMetadataHandlerProvider::new,
() -> DefaultRelMetadataProvider.INSTANCE,
false);
static final MetadataConfig NOP =
new MetadataConfig("Nop",
ProxyingMetadataHandlerProvider::new,
() -> DefaultRelMetadataProvider.INSTANCE,
false) {
@Override void applyMetadata(RelOptCluster cluster,
RelMetadataProvider provider,
Function<MetadataHandlerProvider, RelMetadataQuery> supplierFactory) {
// do nothing
}
};
public final String name;
public final Function<RelMetadataProvider, MetadataHandlerProvider> converter;
public final Supplier<RelMetadataProvider> defaultProviderSupplier;
public final boolean isCaching;
public MetadataConfig(String name,
Function<RelMetadataProvider, MetadataHandlerProvider> converter,
Supplier<RelMetadataProvider> defaultProviderSupplier,
boolean isCaching) {
this.name = name;
this.converter = converter;
this.defaultProviderSupplier = defaultProviderSupplier;
this.isCaching = isCaching;
}
public MetadataHandlerProvider getDefaultHandlerProvider() {
return converter.apply(defaultProviderSupplier.get());
}
void applyMetadata(RelOptCluster cluster) {
applyMetadata(cluster, defaultProviderSupplier.get());
}
void applyMetadata(RelOptCluster cluster,
RelMetadataProvider provider) {
applyMetadata(cluster, provider, RelMetadataQuery::new);
}
void applyMetadata(RelOptCluster cluster,
RelMetadataProvider provider,
Function<MetadataHandlerProvider, RelMetadataQuery> supplierFactory) {
cluster.setMetadataProvider(provider);
cluster.setMetadataQuerySupplier(() ->
supplierFactory.apply(converter.apply(provider)));
cluster.invalidateMetadataQuery();
}
public boolean isCaching() {
return isCaching;
}
@Override public String toString() {
return name;
}
}
}