blob: 5fd12fa3e67fea9151911bcec19e58599cefd22a [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.adapter.geode.rel;
import org.apache.calcite.plan.Convention;
import org.apache.calcite.plan.RelOptRule;
import org.apache.calcite.plan.RelOptRuleCall;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.plan.RelRule;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.RelCollations;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.convert.ConverterRule;
import org.apache.calcite.rel.core.Sort;
import org.apache.calcite.rel.logical.LogicalAggregate;
import org.apache.calcite.rel.logical.LogicalFilter;
import org.apache.calcite.rel.logical.LogicalProject;
import org.apache.calcite.rel.type.RelDataType;
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.RexVisitorImpl;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.immutables.value.Value;
import java.util.ArrayList;
import java.util.List;
/**
* Rules and relational operators for {@link GeodeRel#CONVENTION}
* calling convention.
*/
public class GeodeRules {
static final RelOptRule[] RULES = {
GeodeSortLimitRule.INSTANCE,
GeodeFilterRule.INSTANCE,
GeodeProjectRule.INSTANCE,
GeodeAggregateRule.INSTANCE,
};
private GeodeRules() {
}
/**
* Returns 'string' if it is a call to item['string'], null otherwise.
*/
static String isItem(RexCall call) {
if (call.getOperator() != SqlStdOperatorTable.ITEM) {
return null;
}
final RexNode op0 = call.getOperands().get(0);
final RexNode op1 = call.getOperands().get(1);
if (op0 instanceof RexInputRef
&& ((RexInputRef) op0).getIndex() == 0
&& op1 instanceof RexLiteral
&& ((RexLiteral) op1).getValue2() instanceof String) {
return (String) ((RexLiteral) op1).getValue2();
}
return null;
}
static List<String> geodeFieldNames(final RelDataType rowType) {
return SqlValidatorUtil.uniquify(rowType.getFieldNames(), true);
}
/**
* Translator from {@link RexNode} to strings in Geode's expression language.
*/
static class RexToGeodeTranslator extends RexVisitorImpl<String> {
private final List<String> inFields;
protected RexToGeodeTranslator(List<String> inFields) {
super(true);
this.inFields = inFields;
}
@Override public String visitInputRef(RexInputRef inputRef) {
return inFields.get(inputRef.getIndex());
}
@Override public String visitCall(RexCall call) {
final List<String> strings = new ArrayList<>();
visitList(call.operands, strings);
if (call.getOperator() == SqlStdOperatorTable.ITEM) {
final RexNode op1 = call.getOperands().get(1);
if (op1 instanceof RexLiteral) {
if (op1.getType().getSqlTypeName() == SqlTypeName.INTEGER) {
return stripQuotes(strings.get(0)) + "[" + ((RexLiteral) op1).getValue2() + "]";
} else if (op1.getType().getSqlTypeName() == SqlTypeName.CHAR) {
return stripQuotes(strings.get(0)) + "." + ((RexLiteral) op1).getValue2();
}
}
}
return super.visitCall(call);
}
private static String stripQuotes(String s) {
return s.startsWith("'") && s.endsWith("'") ? s.substring(1, s.length() - 1) : s;
}
}
/**
* Rule to convert a {@link LogicalProject} to a {@link GeodeProject}.
*/
private static class GeodeProjectRule extends GeodeConverterRule {
private static final GeodeProjectRule INSTANCE = Config.INSTANCE
.withConversion(LogicalProject.class, Convention.NONE,
GeodeRel.CONVENTION, "GeodeProjectRule")
.withRuleFactory(GeodeProjectRule::new)
.toRule(GeodeProjectRule.class);
protected GeodeProjectRule(Config config) {
super(config);
}
@Override public boolean matches(RelOptRuleCall call) {
LogicalProject project = call.rel(0);
for (RexNode e : project.getProjects()) {
if (e.getType().getSqlTypeName() == SqlTypeName.GEOMETRY) {
// For spatial Functions Drop to Calcite Enumerable
return false;
}
}
return true;
}
@Override public RelNode convert(RelNode rel) {
final LogicalProject project = (LogicalProject) rel;
final RelTraitSet traitSet =
project.getTraitSet().replace(getOutConvention());
return new GeodeProject(
project.getCluster(),
traitSet,
convert(project.getInput(), getOutConvention()),
project.getProjects(),
project.getRowType());
}
}
/**
* Rule to convert {@link org.apache.calcite.rel.core.Aggregate} to a
* {@link GeodeAggregate}.
*/
private static class GeodeAggregateRule extends GeodeConverterRule {
private static final GeodeAggregateRule INSTANCE = Config.INSTANCE
.withConversion(LogicalAggregate.class, Convention.NONE,
GeodeRel.CONVENTION, "GeodeAggregateRule")
.withRuleFactory(GeodeAggregateRule::new)
.toRule(GeodeAggregateRule.class);
protected GeodeAggregateRule(Config config) {
super(config);
}
@Override public RelNode convert(RelNode rel) {
final LogicalAggregate aggregate = (LogicalAggregate) rel;
final RelTraitSet traitSet =
aggregate.getTraitSet().replace(getOutConvention());
return new GeodeAggregate(
aggregate.getCluster(),
traitSet,
convert(aggregate.getInput(), traitSet.simplify()),
aggregate.getGroupSet(),
aggregate.getGroupSets(),
aggregate.getAggCallList());
}
}
/**
* Rule to convert the Limit in {@link org.apache.calcite.rel.core.Sort} to a
* {@link GeodeSort}.
*/
public static class GeodeSortLimitRule
extends RelRule<GeodeSortLimitRule.GeodeSortLimitRuleConfig> {
private static final GeodeSortLimitRule INSTANCE =
ImmutableGeodeSortLimitRuleConfig.builder()
.withOperandSupplier(b ->
b.operand(Sort.class)
// OQL doesn't support offsets (e.g. LIMIT 10 OFFSET 500)
.predicate(sort -> sort.offset == null)
.anyInputs())
.build()
.toRule();
/** Creates a GeodeSortLimitRule. */
protected GeodeSortLimitRule(GeodeSortLimitRuleConfig config) {
super(config);
}
@Override public void onMatch(RelOptRuleCall call) {
final Sort sort = call.rel(0);
final RelTraitSet traitSet = sort.getTraitSet()
.replace(GeodeRel.CONVENTION)
.replace(sort.getCollation());
GeodeSort geodeSort = new GeodeSort(sort.getCluster(), traitSet,
convert(sort.getInput(), traitSet.replace(RelCollations.EMPTY)),
sort.getCollation(), sort.fetch);
call.transformTo(geodeSort);
}
/** Rule configuration. */
@Value.Immutable(singleton = false)
public interface GeodeSortLimitRuleConfig extends RelRule.Config {
@Override default GeodeSortLimitRule toRule() {
return new GeodeSortLimitRule(this);
}
}
}
/**
* Rule to convert a {@link LogicalFilter} to a
* {@link GeodeFilter}.
*/
public static class GeodeFilterRule
extends RelRule<GeodeFilterRule.GeodeFilterRuleConfig> {
private static final GeodeFilterRule INSTANCE =
ImmutableGeodeFilterRuleConfig.builder()
.withOperandSupplier(b0 ->
b0.operand(LogicalFilter.class).oneInput(b1 ->
b1.operand(GeodeTableScan.class).noInputs()))
.build()
.toRule();
/** Creates a GeodeFilterRule. */
protected GeodeFilterRule(GeodeFilterRuleConfig config) {
super(config);
}
@Override public boolean matches(RelOptRuleCall call) {
// Get the condition from the filter operation
LogicalFilter filter = call.rel(0);
RexNode condition = filter.getCondition();
List<String> fieldNames = GeodeRules.geodeFieldNames(filter.getInput().getRowType());
List<RexNode> disjunctions = RelOptUtil.disjunctions(condition);
if (disjunctions.size() != 1) {
return true;
} else {
// Check that all conjunctions are primary field conditions.
condition = disjunctions.get(0);
for (RexNode predicate : RelOptUtil.conjunctions(condition)) {
if (!isEqualityOnKey(predicate, fieldNames)) {
return false;
}
}
}
return true;
}
/**
* Check if the node is a supported predicate (primary field condition).
*
* @param node Condition node to check
* @param fieldNames Names of all columns in the table
* @return True if the node represents an equality predicate on a primary key
*/
private static boolean isEqualityOnKey(RexNode node, List<String> fieldNames) {
if (isBooleanColumnReference(node, fieldNames)) {
return true;
}
if (!SqlKind.COMPARISON.contains(node.getKind())
&& node.getKind() != SqlKind.SEARCH) {
return false;
}
RexCall call = (RexCall) node;
final RexNode left = call.operands.get(0);
final RexNode right = call.operands.get(1);
if (checkConditionContainsInputRefOrLiterals(left, right, fieldNames)) {
return true;
}
return checkConditionContainsInputRefOrLiterals(right, left, fieldNames);
}
private static boolean isBooleanColumnReference(RexNode node, List<String> fieldNames) {
// FIXME Ignore casts for rel and assume they aren't really necessary
if (node.isA(SqlKind.CAST)) {
node = ((RexCall) node).getOperands().get(0);
}
if (node.isA(SqlKind.NOT)) {
node = ((RexCall) node).getOperands().get(0);
}
if (node.isA(SqlKind.INPUT_REF)) {
if (node.getType().getSqlTypeName() == SqlTypeName.BOOLEAN) {
final RexInputRef left1 = (RexInputRef) node;
String name = fieldNames.get(left1.getIndex());
return name != null;
}
}
return false;
}
/**
* Checks whether a condition contains input refs of literals.
*
* @param left Left operand of the equality
* @param right Right operand of the equality
* @param fieldNames Names of all columns in the table
* @return Whether condition is supported
*/
private static boolean checkConditionContainsInputRefOrLiterals(RexNode left,
RexNode right, List<String> fieldNames) {
// FIXME Ignore casts for rel and assume they aren't really necessary
if (left.isA(SqlKind.CAST)) {
left = ((RexCall) left).getOperands().get(0);
}
if (right.isA(SqlKind.CAST)) {
right = ((RexCall) right).getOperands().get(0);
}
if (left.isA(SqlKind.INPUT_REF) && right.isA(SqlKind.LITERAL)) {
final RexInputRef left1 = (RexInputRef) left;
String name = fieldNames.get(left1.getIndex());
return name != null;
} else if (left.isA(SqlKind.INPUT_REF) && right.isA(SqlKind.INPUT_REF)) {
final RexInputRef left1 = (RexInputRef) left;
String leftName = fieldNames.get(left1.getIndex());
final RexInputRef right1 = (RexInputRef) right;
String rightName = fieldNames.get(right1.getIndex());
return (leftName != null) && (rightName != null);
} else if (left.isA(SqlKind.ITEM) && right.isA(SqlKind.LITERAL)) {
return true;
}
return false;
}
@Override public void onMatch(RelOptRuleCall call) {
LogicalFilter filter = call.rel(0);
if (filter.getTraitSet().contains(Convention.NONE)) {
final RelNode converted = convert(filter);
call.transformTo(converted);
}
}
private static RelNode convert(LogicalFilter filter) {
final RelTraitSet traitSet = filter.getTraitSet().replace(GeodeRel.CONVENTION);
return new GeodeFilter(
filter.getCluster(),
traitSet,
convert(filter.getInput(), GeodeRel.CONVENTION),
filter.getCondition());
}
/** Rule configuration. */
@Value.Immutable(singleton = false)
public interface GeodeFilterRuleConfig extends RelRule.Config {
@Override default GeodeFilterRule toRule() {
return new GeodeFilterRule(this);
}
}
}
/**
* Base class for planner rules that convert a relational
* expression to Geode calling convention.
*/
abstract static class GeodeConverterRule extends ConverterRule {
protected GeodeConverterRule(Config config) {
super(config);
}
}
}