RelHint.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.rel.hint;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

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

import static java.util.Objects.requireNonNull;

/**
 * Hint attached to a relation expression.
 *
 * <p>A hint can be used to:
 *
 * <ul>
 *   <li>Enforce planner: there's no perfect planner, so it makes sense to implement hints to
 *   allow user better control the execution. For instance, "never merge this subquery with others",
 *   "treat those tables as leading ones" in the join ordering, etc.</li>
 *   <li>Append meta data/statistics: Some statistics like ���table index for scan��� and
 *   ���skew info of some shuffle keys��� are somewhat dynamic for the query, it would be very
 *   convenient to config them with hints because our planning metadata from the planner is very
 *   often not that accurate.</li>
 *   <li>Operator resource constraints: For many cases, we would give a default resource
 *   configuration for the execution operators, i.e. min parallelism or
 *   managed memory (resource consuming UDF) or special resource requirement (GPU or SSD disk)
 *   and so on, it would be very flexible to profile the resource with hints per query
 *   (instead of the Job).</li>
 * </ul>
 *
 * <p>In order to support hint override, each hint has a {@code inheritPath} (integers list) to
 * record its propagate path from the root node, number `0` represents the hint was propagated
 * along the first(left) child, number `1` represents the hint was propagated along the
 * second(right) child. Given a relational expression tree with initial attached hints:
 *
 * <blockquote><pre>
 *            Filter (Hint1)
 *                |
 *               Join
 *              /    \
 *            Scan  Project (Hint2)
 *                     |
 *                    Scan2
 * </pre></blockquote>
 *
 * <p>The plan would have hints path as follows (assumes each hint can be propagated to all
 * child nodes):
 *
 * <blockquote><ul>
 *   <li>Filter &#8594; {Hint1[]}</li>
 *   <li>Join &#8594; {Hint1[0]}</li>
 *   <li>Scan &#8594; {Hint1[0, 0]}</li>
 *   <li>Project &#8594; {Hint1[0,1], Hint2[]}</li>
 *   <li>Scan2 &#8594; {[Hint1[0, 1, 0], Hint2[0]}</li>
 * </ul></blockquote>
 *
 * <p>{@code listOptions} and {@code kvOptions} are supposed to contain the same information,
 * they are mutually exclusive, that means, they can not both be non-empty.
 *
 * <p>RelHint is immutable.
 */
public class RelHint {
  //~ Instance fields --------------------------------------------------------

  public final ImmutableList<Integer> inheritPath;
  public final String hintName;
  public final List<String> listOptions;
  public final Map<String, String> kvOptions;

  //~ Constructors -----------------------------------------------------------

  /**
   * Creates a {@code RelHint}.
   *
   * @param inheritPath Hint inherit path
   * @param hintName    Hint name
   * @param listOption  Hint options as string list
   * @param kvOptions   Hint options as string key value pair
   */
  private RelHint(
      Iterable<Integer> inheritPath,
      String hintName,
      @Nullable List<String> listOption,
      @Nullable Map<String, String> kvOptions) {
    this.inheritPath = ImmutableList.copyOf(inheritPath);
    this.hintName = requireNonNull(hintName, "hintName");
    this.listOptions = listOption == null ? ImmutableList.of() : ImmutableList.copyOf(listOption);
    this.kvOptions = kvOptions == null ? ImmutableMap.of() : ImmutableMap.copyOf(kvOptions);
  }

  //~ Methods ----------------------------------------------------------------

  /** Creates a hint builder with specified hint name. */
  public static Builder builder(String hintName) {
    return new Builder(hintName);
  }

  /**
   * Returns a copy of this hint with specified inherit path.
   *
   * @param inheritPath Hint path
   * @return the new {@code RelHint}
   */
  public RelHint copy(List<Integer> inheritPath) {
    requireNonNull(inheritPath, "inheritPath");
    return new RelHint(inheritPath, hintName, listOptions, kvOptions);
  }

  @Override public boolean equals(@Nullable Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    RelHint hint = (RelHint) o;
    return inheritPath.equals(hint.inheritPath)
        && hintName.equals(hint.hintName)
        && Objects.equals(listOptions, hint.listOptions)
        && Objects.equals(kvOptions, hint.kvOptions);
  }

  @Override public int hashCode() {
    return Objects.hash(this.hintName, this.inheritPath,
        this.listOptions, this.kvOptions);
  }

  @Override public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append("[")
        .append(this.hintName)
        .append(" inheritPath:")
        .append(this.inheritPath);
    if (!this.listOptions.isEmpty() || !this.kvOptions.isEmpty()) {
      builder.append(" options:");
      if (!this.listOptions.isEmpty()) {
        builder.append(this.listOptions);
      } else {
        builder.append(this.kvOptions);
      }
    }
    builder.append("]");
    return builder.toString();
  }

  //~ Inner Class ------------------------------------------------------------

  /** Builder for {@link RelHint}. */
  public static class Builder {
    private final String hintName;
    private List<Integer> inheritPath;

    private List<String> listOptions;
    private Map<String, String> kvOptions;

    private Builder(String hintName) {
      this.listOptions = new ArrayList<>();
      this.kvOptions = new LinkedHashMap<>();
      this.hintName = hintName;
      this.inheritPath = ImmutableList.of();
    }

    /** Sets up the inherit path with given integer list. */
    public Builder inheritPath(Iterable<Integer> inheritPath) {
      this.inheritPath = ImmutableList.copyOf(inheritPath);
      return this;
    }

    /** Sets up the inherit path with given integer array. */
    public Builder inheritPath(Integer... inheritPath) {
      this.inheritPath = Arrays.asList(inheritPath);
      return this;
    }

    /** Add a hint option as string. */
    public Builder hintOption(String hintOption) {
      requireNonNull(hintOption, "hintOption");
      checkState(this.kvOptions.isEmpty(),
          "List options and key value options can not be mixed in");
      this.listOptions.add(hintOption);
      return this;
    }

    /** Add multiple string hint options. */
    public Builder hintOptions(Iterable<String> hintOptions) {
      requireNonNull(hintOptions, "hintOptions");
      checkState(this.kvOptions.isEmpty(),
          "List options and key value options can not be mixed in");
      this.listOptions = ImmutableList.copyOf(hintOptions);
      return this;
    }

    /** Add a hint option as string key-value pair. */
    public Builder hintOption(String optionKey, String optionValue) {
      requireNonNull(optionKey, "optionKey");
      requireNonNull(optionValue, "optionValue");
      checkState(this.listOptions.isEmpty(),
          "List options and key value options can not be mixed in");
      this.kvOptions.put(optionKey, optionValue);
      return this;
    }

    /** Add multiple string key-value pair hint options. */
    public Builder hintOptions(Map<String, String> kvOptions) {
      requireNonNull(kvOptions, "kvOptions");
      checkState(this.listOptions.isEmpty(),
          "List options and key value options can not be mixed in");
      this.kvOptions = kvOptions;
      return this;
    }

    public RelHint build() {
      return new RelHint(this.inheritPath, this.hintName, this.listOptions, this.kvOptions);
    }
  }
}