IndexCondition.java

/*
 * 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.innodb;

import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelCollations;
import org.apache.calcite.rel.RelFieldCollation;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.util.Pair;

import com.alibaba.innodb.java.reader.comparator.ComparisonOperator;
import com.google.common.collect.ImmutableList;

import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static com.google.common.base.Preconditions.checkState;

import static java.util.Objects.requireNonNull;

/**
 * Index condition.
 *
 * <p>Works in the following places:
 *
 * <ul>
 * <li>In {@link InnodbFilterTranslator}, it is the index condition
 * to push down according to {@link InnodbFilter} by planner rule.
 *
 * <li>In {@link InnodbTableScan}, it represents a full scan by a
 * primary key or a secondary key.
 *
 * <li>In code generation, it indicates the storage engine which index
 * to use and the associated condition if present.
 * </ul>
 */
public class IndexCondition {

  static final IndexCondition EMPTY_CONDITION =
      create("", QueryType.PK_FULL_SCAN,
          null, ComparisonOperator.NOP, ComparisonOperator.NOP,
          ImmutableList.of(), ImmutableList.of());

  /** Field names per row type. */
  private final List<String> fieldNames;
  private final String indexName;
  private final List<String> indexColumnNames;
  private final RelCollation implicitCollation;
  private final List<RexNode> pushDownConditions;
  private final List<RexNode> remainderConditions;

  private final QueryType queryType;
  private final List<Object> pointQueryKey;
  private final ComparisonOperator rangeQueryLowerOp;
  private final ComparisonOperator rangeQueryUpperOp;
  private final List<Object> rangeQueryLowerKey;
  private final List<Object> rangeQueryUpperKey;

  /** Constructor that assigns all fields. All other constructors call this. */
  private IndexCondition(
      List<String> fieldNames,
      String indexName,
      List<String> indexColumnNames,
      @Nullable RelCollation implicitCollation,
      @Nullable List<RexNode> pushDownConditions,
      @Nullable List<RexNode> remainderConditions,
      QueryType queryType,
      @Nullable List<Object> pointQueryKey,
      ComparisonOperator rangeQueryLowerOp,
      ComparisonOperator rangeQueryUpperOp,
      List<Object> rangeQueryLowerKey,
      List<Object> rangeQueryUpperKey) {
    this.fieldNames = fieldNames;
    this.indexName = indexName;
    this.indexColumnNames = indexColumnNames;
    this.implicitCollation =
        implicitCollation != null ? implicitCollation
            : deduceImplicitCollation(fieldNames, indexColumnNames);
    this.pushDownConditions =
        pushDownConditions == null ? ImmutableList.of()
            : ImmutableList.copyOf(pushDownConditions);
    this.remainderConditions =
        remainderConditions == null ? ImmutableList.of()
            : ImmutableList.copyOf(remainderConditions);
    this.queryType = queryType;
    this.pointQueryKey =
        pointQueryKey == null ? ImmutableList.of()
            : ImmutableList.copyOf(pointQueryKey);
    this.rangeQueryLowerOp = requireNonNull(rangeQueryLowerOp, "rangeQueryLowerOp");
    this.rangeQueryUpperOp = requireNonNull(rangeQueryUpperOp, "rangeQueryUpperOp");
    this.rangeQueryLowerKey = ImmutableList.copyOf(rangeQueryLowerKey);
    this.rangeQueryUpperKey = ImmutableList.copyOf(rangeQueryUpperKey);
  }

  static IndexCondition create(
      List<String> fieldNames,
      String indexName,
      List<String> indexColumnNames,
      QueryType queryType) {
    return new IndexCondition(fieldNames, indexName, indexColumnNames, null,
        null, null, queryType, null, ComparisonOperator.NOP,
        ComparisonOperator.NOP, ImmutableList.of(), ImmutableList.of());
  }

  /**
   * Creates a new instance for {@link InnodbFilterTranslator} to build
   * index condition which can be pushed down.
   */
  static IndexCondition create(
      List<String> fieldNames,
      String indexName,
      List<String> indexColumnNames,
      List<RexNode> pushDownConditions,
      List<RexNode> remainderConditions) {
    return new IndexCondition(fieldNames, indexName, indexColumnNames, null,
        pushDownConditions, remainderConditions, QueryType.PK_FULL_SCAN, null,
        ComparisonOperator.NOP, ComparisonOperator.NOP, ImmutableList.of(),
        ImmutableList.of());
  }

  /**
   * Creates a new instance for code generation to build query parameters
   * for underlying storage engine <code>Innodb-java-reader</code>.
   */
  public static IndexCondition create(
      String indexName,
      QueryType queryType,
      @Nullable List<Object> pointQueryKey,
      ComparisonOperator rangeQueryLowerOp,
      ComparisonOperator rangeQueryUpperOp,
      List<Object> rangeQueryLowerKey,
      List<Object> rangeQueryUpperKey) {
    return new IndexCondition(ImmutableList.of(), indexName, ImmutableList.of(),
        null, null, null, queryType, pointQueryKey, rangeQueryLowerOp,
        rangeQueryUpperOp, rangeQueryLowerKey, rangeQueryUpperKey);
  }

  /** Returns whether there are any push down conditions. */
  boolean canPushDown() {
    return !pushDownConditions.isEmpty();
  }

  public RelCollation getImplicitCollation() {
    return implicitCollation;
  }

  /**
   * Infers the implicit correlation from the index.
   *
   * @param indexColumnNames index column names
   * @return the collation of the filtered results
   */
  private static RelCollation deduceImplicitCollation(List<String> fieldNames,
      List<String> indexColumnNames) {
    requireNonNull(fieldNames, "field names must not be null");
    List<RelFieldCollation> keyCollations = new ArrayList<>(indexColumnNames.size());
    for (String keyColumnName : indexColumnNames) {
      int fieldIndex = fieldNames.indexOf(keyColumnName);
      keyCollations.add(
          new RelFieldCollation(fieldIndex, RelFieldCollation.Direction.ASCENDING));
    }
    return RelCollations.of(keyCollations);
  }

  public IndexCondition withFieldNames(List<String> fieldNames) {
    if (Objects.equals(fieldNames, this.fieldNames)) {
      return this;
    }
    return new IndexCondition(fieldNames, indexName, indexColumnNames,
        implicitCollation, pushDownConditions, remainderConditions,
        queryType, pointQueryKey, rangeQueryLowerOp, rangeQueryUpperOp,
        rangeQueryLowerKey, rangeQueryUpperKey);
  }

  public String getIndexName() {
    return indexName;
  }

  public IndexCondition withIndexName(String indexName) {
    if (Objects.equals(indexName, this.indexName)) {
      return this;
    }
    return new IndexCondition(fieldNames, indexName, indexColumnNames,
        implicitCollation, pushDownConditions, remainderConditions,
        queryType, pointQueryKey, rangeQueryLowerOp, rangeQueryUpperOp,
        rangeQueryLowerKey, rangeQueryUpperKey);
  }

  public IndexCondition withIndexColumnNames(List<String> indexColumnNames) {
    if (Objects.equals(indexColumnNames, this.indexColumnNames)) {
      return this;
    }
    return new IndexCondition(fieldNames, indexName, indexColumnNames,
        implicitCollation, pushDownConditions, remainderConditions,
        queryType, pointQueryKey, rangeQueryLowerOp, rangeQueryUpperOp,
        rangeQueryLowerKey, rangeQueryUpperKey);
  }

  public List<RexNode> getPushDownConditions() {
    return pushDownConditions;
  }

  public IndexCondition withPushDownConditions(List<RexNode> pushDownConditions) {
    if (Objects.equals(pushDownConditions, this.pushDownConditions)) {
      return this;
    }
    return new IndexCondition(fieldNames, indexName, indexColumnNames,
        implicitCollation, pushDownConditions, remainderConditions,
        queryType, pointQueryKey, rangeQueryLowerOp, rangeQueryUpperOp,
        rangeQueryLowerKey, rangeQueryUpperKey);
  }

  public List<RexNode> getRemainderConditions() {
    return remainderConditions;
  }

  public IndexCondition withRemainderConditions(List<RexNode> remainderConditions) {
    if (Objects.equals(remainderConditions, this.remainderConditions)) {
      return this;
    }
    return new IndexCondition(fieldNames, indexName, indexColumnNames,
        implicitCollation, pushDownConditions, remainderConditions,
        queryType, pointQueryKey, rangeQueryLowerOp, rangeQueryUpperOp,
        rangeQueryLowerKey, rangeQueryUpperKey);
  }

  public QueryType getQueryType() {
    return queryType;
  }

  public IndexCondition withQueryType(QueryType queryType) {
    if (queryType == this.queryType) {
      return this;
    }
    return new IndexCondition(fieldNames, indexName, indexColumnNames,
        implicitCollation, pushDownConditions, remainderConditions,
        queryType, pointQueryKey, rangeQueryLowerOp, rangeQueryUpperOp,
        rangeQueryLowerKey, rangeQueryUpperKey);
  }

  public List<Object> getPointQueryKey() {
    return pointQueryKey;
  }

  public IndexCondition withPointQueryKey(List<Object> pointQueryKey) {
    if (pointQueryKey == this.pointQueryKey) {
      return this;
    }
    return new IndexCondition(fieldNames, indexName, indexColumnNames,
        implicitCollation, pushDownConditions, remainderConditions,
        queryType, pointQueryKey, rangeQueryLowerOp, rangeQueryUpperOp,
        rangeQueryLowerKey, rangeQueryUpperKey);
  }

  public ComparisonOperator getRangeQueryLowerOp() {
    return rangeQueryLowerOp;
  }

  public IndexCondition withRangeQueryLowerOp(ComparisonOperator rangeQueryLowerOp) {
    if (rangeQueryLowerOp == this.rangeQueryLowerOp) {
      return this;
    }
    return new IndexCondition(fieldNames, indexName, indexColumnNames,
        implicitCollation, pushDownConditions, remainderConditions,
        queryType, pointQueryKey, rangeQueryLowerOp, rangeQueryUpperOp,
        rangeQueryLowerKey, rangeQueryUpperKey);
  }

  public ComparisonOperator getRangeQueryUpperOp() {
    return rangeQueryUpperOp;
  }

  public IndexCondition withRangeQueryUpperOp(ComparisonOperator rangeQueryUpperOp) {
    if (rangeQueryUpperOp == this.rangeQueryUpperOp) {
      return this;
    }
    return new IndexCondition(fieldNames, indexName, indexColumnNames,
        implicitCollation, pushDownConditions, remainderConditions,
        queryType, pointQueryKey, rangeQueryLowerOp, rangeQueryUpperOp,
        rangeQueryLowerKey, rangeQueryUpperKey);
  }

  public List<Object> getRangeQueryLowerKey() {
    return rangeQueryLowerKey;
  }

  public IndexCondition withRangeQueryLowerKey(List<Object> rangeQueryLowerKey) {
    if (rangeQueryLowerKey == this.rangeQueryLowerKey) {
      return this;
    }
    return new IndexCondition(fieldNames, indexName, indexColumnNames,
        implicitCollation, pushDownConditions, remainderConditions,
        queryType, pointQueryKey, rangeQueryLowerOp, rangeQueryUpperOp,
        rangeQueryLowerKey, rangeQueryUpperKey);
  }

  public List<Object> getRangeQueryUpperKey() {
    return rangeQueryUpperKey;
  }

  public IndexCondition withRangeQueryUpperKey(List<Object> rangeQueryUpperKey) {
    if (rangeQueryUpperKey == this.rangeQueryUpperKey) {
      return this;
    }
    return new IndexCondition(fieldNames, indexName, indexColumnNames,
        implicitCollation, pushDownConditions, remainderConditions,
        queryType, pointQueryKey, rangeQueryLowerOp, rangeQueryUpperOp,
        rangeQueryLowerKey, rangeQueryUpperKey);
  }

  public boolean nameMatch(String name) {
    return name != null && name.equalsIgnoreCase(indexName);
  }

  @Override public String toString() {
    final StringBuilder builder = new StringBuilder("(");
    builder.append(queryType).append(", index=").append(indexName);
    if (queryType == QueryType.PK_POINT_QUERY
        || queryType == QueryType.SK_POINT_QUERY) {
      checkState(pointQueryKey.size() == indexColumnNames.size());
      append(builder, indexColumnNames, pointQueryKey, "=");
    } else {
      if (!rangeQueryLowerKey.isEmpty()) {
        append(builder, indexColumnNames, rangeQueryLowerKey, rangeQueryLowerOp.value());
      }
      if (!rangeQueryUpperKey.isEmpty()) {
        append(builder, indexColumnNames, rangeQueryUpperKey, rangeQueryUpperOp.value());
      }
    }
    builder.append(")");
    return builder.toString();
  }

  private static void append(StringBuilder builder, List<String> keyColumnNames,
      List<Object> key, String op) {
    builder.append(", ");
    for (Pair<String, Object> value : Pair.zip(keyColumnNames, key)) {
      builder.append(value.getKey());
      builder.append(op);
      builder.append(value.getValue());
      builder.append(",");
    }
    builder.deleteCharAt(builder.length() - 1);
  }
}