com.hazelcast.org.apache.calcite.rel.rules.PruneEmptyRules Maven / Gradle / Ivy
/*
* 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 com.hazelcast.org.apache.calcite.rel.rules;
import com.hazelcast.org.apache.calcite.plan.RelOptRule;
import com.hazelcast.org.apache.calcite.plan.RelOptRuleCall;
import com.hazelcast.org.apache.calcite.plan.RelOptUtil;
import com.hazelcast.org.apache.calcite.plan.RelRule;
import com.hazelcast.org.apache.calcite.plan.RelTraitSet;
import com.hazelcast.org.apache.calcite.plan.hep.HepRelVertex;
import com.hazelcast.org.apache.calcite.plan.volcano.RelSubset;
import com.hazelcast.org.apache.calcite.rel.RelNode;
import com.hazelcast.org.apache.calcite.rel.SingleRel;
import com.hazelcast.org.apache.calcite.rel.core.Aggregate;
import com.hazelcast.org.apache.calcite.rel.core.Filter;
import com.hazelcast.org.apache.calcite.rel.core.Join;
import com.hazelcast.org.apache.calcite.rel.core.JoinRelType;
import com.hazelcast.org.apache.calcite.rel.core.Project;
import com.hazelcast.org.apache.calcite.rel.core.Sort;
import com.hazelcast.org.apache.calcite.rel.core.Values;
import com.hazelcast.org.apache.calcite.rel.logical.LogicalIntersect;
import com.hazelcast.org.apache.calcite.rel.logical.LogicalMinus;
import com.hazelcast.org.apache.calcite.rel.logical.LogicalUnion;
import com.hazelcast.org.apache.calcite.rel.logical.LogicalValues;
import com.hazelcast.org.apache.calcite.rex.RexDynamicParam;
import com.hazelcast.org.apache.calcite.rex.RexLiteral;
import com.hazelcast.org.apache.calcite.tools.RelBuilder;
import com.hazelcast.org.apache.calcite.tools.RelBuilderFactory;
import org.immutables.value.Value;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
/**
* Collection of rules which remove sections of a query plan known never to
* produce any rows.
*
* Conventionally, the way to represent an empty relational expression is
* with a {@link Values} that has no tuples.
*
* @see LogicalValues#createEmpty
*/
public abstract class PruneEmptyRules {
//~ Static fields/initializers ---------------------------------------------
/**
* Abstract prune empty rule that implements SubstitutionRule interface.
*/
protected abstract static class PruneEmptyRule
extends RelRule
implements SubstitutionRule {
protected PruneEmptyRule(Config config) {
super(config);
}
@Override public boolean autoPruneOld() {
return true;
}
/** Rule configuration. */
public interface Config extends RelRule.Config {
@Override PruneEmptyRule toRule();
}
}
/**
* Rule that removes empty children of a
* {@link com.hazelcast.org.apache.calcite.rel.logical.LogicalUnion}.
*
* Examples:
*
*
* - Union(Rel, Empty, Rel2) becomes Union(Rel, Rel2)
*
- Union(Rel, Empty, Empty) becomes Rel
*
- Union(Empty, Empty) becomes Empty
*
*/
public static final RelOptRule UNION_INSTANCE =
ImmutableUnionEmptyPruneRuleConfig.of()
.withOperandSupplier(b0 ->
b0.operand(LogicalUnion.class).unorderedInputs(b1 ->
b1.operand(Values.class)
.predicate(Values::isEmpty).noInputs()))
.withDescription("Union")
.toRule();
/**
* Rule that removes empty children of a
* {@link com.hazelcast.org.apache.calcite.rel.logical.LogicalMinus}.
*
* Examples:
*
*
* - Minus(Rel, Empty, Rel2) becomes Minus(Rel, Rel2)
*
- Minus(Empty, Rel) becomes Empty
*
*/
public static final RelOptRule MINUS_INSTANCE =
ImmutableMinusEmptyPruneRuleConfig.of()
.withOperandSupplier(b0 ->
b0.operand(LogicalMinus.class).unorderedInputs(b1 ->
b1.operand(Values.class).predicate(Values::isEmpty)
.noInputs()))
.withDescription("Minus")
.toRule();
/**
* Rule that converts a
* {@link com.hazelcast.org.apache.calcite.rel.logical.LogicalIntersect} to
* empty if any of its children are empty.
*
* Examples:
*
*
* - Intersect(Rel, Empty, Rel2) becomes Empty
*
- Intersect(Empty, Rel) becomes Empty
*
*/
public static final RelOptRule INTERSECT_INSTANCE =
ImmutableIntersectEmptyPruneRuleConfig.of()
.withOperandSupplier(b0 ->
b0.operand(LogicalIntersect.class).unorderedInputs(b1 ->
b1.operand(Values.class).predicate(Values::isEmpty)
.noInputs()))
.withDescription("Intersect")
.toRule();
private static boolean isEmpty(RelNode node) {
if (node instanceof Values) {
return ((Values) node).getTuples().isEmpty();
}
if (node instanceof HepRelVertex) {
return isEmpty(((HepRelVertex) node).getCurrentRel());
}
// Note: relation input might be a RelSubset, so we just iterate over the relations
// in order to check if the subset is equivalent to an empty relation.
if (!(node instanceof RelSubset)) {
return false;
}
RelSubset subset = (RelSubset) node;
for (RelNode rel : subset.getRels()) {
if (isEmpty(rel)) {
return true;
}
}
return false;
}
/**
* Rule that converts a {@link com.hazelcast.org.apache.calcite.rel.logical.LogicalProject}
* to empty if its child is empty.
*
* Examples:
*
*
* - Project(Empty) becomes Empty
*
*/
public static final RelOptRule PROJECT_INSTANCE =
ImmutableRemoveEmptySingleRuleConfig.of()
.withDescription("PruneEmptyProject")
.withOperandFor(Project.class, project -> true)
.toRule();
/**
* Rule that converts a {@link com.hazelcast.org.apache.calcite.rel.logical.LogicalFilter}
* to empty if its child is empty.
*
* Examples:
*
*
* - Filter(Empty) becomes Empty
*
*/
public static final RelOptRule FILTER_INSTANCE =
ImmutableRemoveEmptySingleRuleConfig.of()
.withDescription("PruneEmptyFilter")
.withOperandFor(Filter.class, singleRel -> true)
.toRule();
/**
* Rule that converts a {@link com.hazelcast.org.apache.calcite.rel.core.Sort}
* to empty if its child is empty.
*
* Examples:
*
*
* - Sort(Empty) becomes Empty
*
*/
public static final RelOptRule SORT_INSTANCE =
ImmutableRemoveEmptySingleRuleConfig.of()
.withDescription("PruneEmptySort")
.withOperandFor(Sort.class, singleRel -> true)
.toRule();
/**
* Rule that converts a {@link com.hazelcast.org.apache.calcite.rel.core.Sort}
* to empty if it has {@code LIMIT 0}.
*
* Examples:
*
*
* - Sort[fetch=0] becomes Empty
*
*/
public static final RelOptRule SORT_FETCH_ZERO_INSTANCE =
ImmutableSortFetchZeroRuleConfig.of()
.withOperandSupplier(b ->
b.operand(Sort.class).anyInputs())
.withDescription("PruneSortLimit0")
.toRule();
/**
* Rule that converts an {@link com.hazelcast.org.apache.calcite.rel.core.Aggregate}
* to empty if its child is empty.
*
* Examples:
*
*
* - {@code Aggregate(key: [1, 3], Empty)} → {@code Empty}
*
*
- {@code Aggregate(key: [], Empty)} is unchanged, because an aggregate
* without a GROUP BY key always returns 1 row, even over empty input
*
*
* @see AggregateValuesRule
*/
public static final RelOptRule AGGREGATE_INSTANCE =
ImmutableRemoveEmptySingleRuleConfig.of()
.withDescription("PruneEmptyAggregate")
.withOperandFor(Aggregate.class, Aggregate::isNotGrandTotal)
.toRule();
/**
* Rule that converts a {@link com.hazelcast.org.apache.calcite.rel.core.Join}
* to empty if its left child is empty.
*
* Examples:
*
*
* - Join(Empty, Scan(Dept), INNER) becomes Empty
*
- Join(Empty, Scan(Dept), LEFT) becomes Empty
*
- Join(Empty, Scan(Dept), SEMI) becomes Empty
*
- Join(Empty, Scan(Dept), ANTI) becomes Empty
*
*/
public static final RelOptRule JOIN_LEFT_INSTANCE =
ImmutableJoinLeftEmptyRuleConfig.of()
.withOperandSupplier(b0 ->
b0.operand(Join.class).inputs(
b1 -> b1.operand(Values.class)
.predicate(Values::isEmpty).noInputs(),
b2 -> b2.operand(RelNode.class).anyInputs()))
.withDescription("PruneEmptyJoin(left)")
.toRule();
/**
* Rule that converts a {@link com.hazelcast.org.apache.calcite.rel.core.Join}
* to empty if its right child is empty.
*
* Examples:
*
*
* - Join(Scan(Emp), Empty, INNER) becomes Empty
*
- Join(Scan(Emp), Empty, RIGHT) becomes Empty
*
- Join(Scan(Emp), Empty, SEMI) becomes Empty
*
- Join(Scan(Emp), Empty, ANTI) becomes Scan(Emp)
*
*/
public static final RelOptRule JOIN_RIGHT_INSTANCE =
ImmutableJoinRightEmptyRuleConfig.of()
.withOperandSupplier(b0 ->
b0.operand(Join.class).inputs(
b1 -> b1.operand(RelNode.class).anyInputs(),
b2 -> b2.operand(Values.class).predicate(Values::isEmpty)
.noInputs()))
.withDescription("PruneEmptyJoin(right)")
.toRule();
/** Planner rule that converts a single-rel (e.g. project, sort, aggregate or
* filter) on top of the empty relational expression into empty. */
public static class RemoveEmptySingleRule extends PruneEmptyRule {
/** Creates a RemoveEmptySingleRule. */
RemoveEmptySingleRule(RemoveEmptySingleRuleConfig config) {
super(config);
}
@Deprecated // to be removed before 2.0
public RemoveEmptySingleRule(Class clazz,
String description) {
this(ImmutableRemoveEmptySingleRuleConfig.of().withDescription(description)
.as(ImmutableRemoveEmptySingleRuleConfig.class)
.withOperandFor(clazz, singleRel -> true));
}
@Deprecated // to be removed before 2.0
public RemoveEmptySingleRule(Class clazz,
Predicate predicate, RelBuilderFactory relBuilderFactory,
String description) {
this(ImmutableRemoveEmptySingleRuleConfig.of().withRelBuilderFactory(relBuilderFactory)
.withDescription(description)
.as(ImmutableRemoveEmptySingleRuleConfig.class)
.withOperandFor(clazz, predicate));
}
@SuppressWarnings({"Guava", "UnnecessaryMethodReference"})
@Deprecated // to be removed before 2.0
public RemoveEmptySingleRule(Class clazz,
com.hazelcast.com.google.common.base.Predicate predicate,
RelBuilderFactory relBuilderFactory, String description) {
this(ImmutableRemoveEmptySingleRuleConfig.of().withRelBuilderFactory(relBuilderFactory)
.withDescription(description)
.as(ImmutableRemoveEmptySingleRuleConfig.class)
.withOperandFor(clazz, predicate::apply));
}
@Override public void onMatch(RelOptRuleCall call) {
SingleRel singleRel = call.rel(0);
RelNode emptyValues = call.builder().push(singleRel).empty().build();
RelTraitSet traits = singleRel.getTraitSet();
// propagate all traits (except convention) from the original singleRel into the empty values
if (emptyValues.getConvention() != null) {
traits = traits.replace(emptyValues.getConvention());
}
emptyValues = emptyValues.copy(traits, Collections.emptyList());
call.transformTo(emptyValues);
}
/** Rule configuration. */
@Value.Immutable
public interface RemoveEmptySingleRuleConfig extends PruneEmptyRule.Config {
@Override default RemoveEmptySingleRule toRule() {
return new RemoveEmptySingleRule(this);
}
/** Defines an operand tree for the given classes. */
default RemoveEmptySingleRuleConfig withOperandFor(Class relClass,
Predicate predicate) {
return withOperandSupplier(b0 ->
b0.operand(relClass).predicate(predicate).oneInput(b1 ->
b1.operand(Values.class).predicate(Values::isEmpty).noInputs()))
.as(RemoveEmptySingleRuleConfig.class);
}
}
}
/** Configuration for a rule that prunes empty inputs from a Minus. */
@Value.Immutable
public interface UnionEmptyPruneRuleConfig extends PruneEmptyRule.Config {
@Override default PruneEmptyRule toRule() {
return new PruneEmptyRule(this) {
@Override public void onMatch(RelOptRuleCall call) {
final LogicalUnion union = call.rel(0);
final List inputs = union.getInputs();
assert inputs != null;
final RelBuilder builder = call.builder();
int nonEmptyInputs = 0;
for (RelNode input : inputs) {
if (!isEmpty(input)) {
builder.push(input);
nonEmptyInputs++;
}
}
assert nonEmptyInputs < inputs.size()
: "planner promised us at least one Empty child: "
+ RelOptUtil.toString(union);
if (nonEmptyInputs == 0) {
builder.push(union).empty();
} else {
builder.union(union.all, nonEmptyInputs);
builder.convert(union.getRowType(), true);
}
call.transformTo(builder.build());
}
};
}
}
/** Configuration for a rule that prunes empty inputs from a Minus. */
@Value.Immutable
public interface MinusEmptyPruneRuleConfig extends PruneEmptyRule.Config {
@Override default PruneEmptyRule toRule() {
return new PruneEmptyRule(this) {
@Override public void onMatch(RelOptRuleCall call) {
final LogicalMinus minus = call.rel(0);
final List inputs = minus.getInputs();
assert inputs != null;
int nonEmptyInputs = 0;
final RelBuilder builder = call.builder();
for (RelNode input : inputs) {
if (!isEmpty(input)) {
builder.push(input);
nonEmptyInputs++;
} else if (nonEmptyInputs == 0) {
// If the first input of Minus is empty, the whole thing is
// empty.
break;
}
}
assert nonEmptyInputs < inputs.size()
: "planner promised us at least one Empty child: "
+ RelOptUtil.toString(minus);
if (nonEmptyInputs == 0) {
builder.push(minus).empty();
} else {
builder.minus(minus.all, nonEmptyInputs);
builder.convert(minus.getRowType(), true);
}
call.transformTo(builder.build());
}
};
}
}
/** Configuration for a rule that prunes an Intersect if any of its inputs
* is empty. */
@Value.Immutable
public interface IntersectEmptyPruneRuleConfig extends PruneEmptyRule.Config {
@Override default PruneEmptyRule toRule() {
return new PruneEmptyRule(this) {
@Override public void onMatch(RelOptRuleCall call) {
LogicalIntersect intersect = call.rel(0);
final RelBuilder builder = call.builder();
builder.push(intersect).empty();
call.transformTo(builder.build());
}
};
}
}
/** Configuration for a rule that prunes a Sort if it has limit 0. */
@Value.Immutable
public interface SortFetchZeroRuleConfig extends PruneEmptyRule.Config {
@Override default PruneEmptyRule toRule() {
return new PruneEmptyRule(this) {
@Override public void onMatch(RelOptRuleCall call) {
Sort sort = call.rel(0);
if (sort.fetch != null
&& !(sort.fetch instanceof RexDynamicParam)
&& RexLiteral.intValue(sort.fetch) == 0) {
RelNode emptyValues = call.builder().push(sort).empty().build();
RelTraitSet traits = sort.getTraitSet();
// propagate all traits (except convention) from the original sort into the empty values
if (emptyValues.getConvention() != null) {
traits = traits.replace(emptyValues.getConvention());
}
emptyValues = emptyValues.copy(traits, Collections.emptyList());
call.transformTo(emptyValues);
}
}
};
}
}
/** Configuration for rule that prunes a join it its left input is
* empty. */
@Value.Immutable
public interface JoinLeftEmptyRuleConfig extends PruneEmptyRule.Config {
@Override default PruneEmptyRule toRule() {
return new PruneEmptyRule(this) {
@Override public void onMatch(RelOptRuleCall call) {
Join join = call.rel(0);
if (join.getJoinType().generatesNullsOnLeft()) {
// "select * from emp right join dept" is not necessarily empty if
// emp is empty
return;
}
call.transformTo(call.builder().push(join).empty().build());
}
};
}
}
/** Configuration for rule that prunes a join it its right input is
* empty. */
@Value.Immutable
public interface JoinRightEmptyRuleConfig extends PruneEmptyRule.Config {
@Override default PruneEmptyRule toRule() {
return new PruneEmptyRule(this) {
@Override public void onMatch(RelOptRuleCall call) {
Join join = call.rel(0);
if (join.getJoinType().generatesNullsOnRight()) {
// "select * from emp left join dept" is not necessarily empty if
// dept is empty
return;
}
if (join.getJoinType() == JoinRelType.ANTI) {
// In case of anti join: Join(X, Empty, ANTI) becomes X
call.transformTo(join.getLeft());
return;
}
call.transformTo(call.builder().push(join).empty().build());
}
};
}
}
}