| /* |
| * 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); |
| } |
| } |
| } |