Hook.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.runtime;

import org.apache.calcite.rel.RelRoot;
import org.apache.calcite.util.Holder;
import org.apache.calcite.util.TryThreadLocal;
import org.apache.calcite.util.Util;

import org.apiguardian.api.API;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * Collection of hooks that can be set by observers and are executed at various
 * parts of the query preparation process.
 *
 * <p>For testing and debugging rather than for end-users.
 */
public enum Hook {
  /** Called to get the current time. Use this to return a predictable time
   * in tests. */
  CURRENT_TIME,

  /** Called to get stdin, stdout, stderr.
   * Use this to re-assign streams in tests. */
  STANDARD_STREAMS,

  /** Returns a boolean value, whether RelBuilder should simplify expressions.
   * Default true. */
  REL_BUILDER_SIMPLIFY,

  /** Returns a boolean value, whether the return convention should be
   * {@link org.apache.calcite.interpreter.BindableConvention}.
   * Default false. */
  ENABLE_BINDABLE,

  /** Called with the SQL string and parse tree, in an array. */
  PARSE_TREE,

  /** Converts a SQL string to a
   * {@link org.apache.calcite.jdbc.CalcitePrepare.Query} object. This hook is
   * an opportunity to execute a {@link org.apache.calcite.rel.RelNode} query
   * plan in the JDBC driver rather than the usual SQL string. */
  STRING_TO_QUERY,

  /** Called with the generated Java plan, just before it is compiled by
   * Janino. */
  JAVA_PLAN,

  /** Called before SqlToRelConverter is built. */
  SQL2REL_CONVERTER_CONFIG_BUILDER,

  /** Called with the output of sql-to-rel-converter. */
  CONVERTED,

  /** Called with the created planner. */
  PLANNER,

  /** Called after de-correlation and field trimming, but before
   * optimization. */
  TRIMMED,

  /** Called by the planner after substituting a materialization. */
  SUB,

  /** Called when a constant expression is being reduced. */
  EXPRESSION_REDUCER,

  /** Called to create a Program to optimize the statement. */
  PROGRAM,

  /** Called when materialization is created. */
  CREATE_MATERIALIZATION,

  /** Called with a query that has been generated to send to a back-end system.
   * The query might be a SQL string (for the JDBC adapter), a list of Mongo
   * pipeline expressions (for the MongoDB adapter), et cetera. */
  QUERY_PLAN,

  /**
   * Called when a plan is about to be implemented (e.g. implemented via Enumerable, Bindable,
   * and so on).
   * The hook supplies {@link RelRoot} as an argument.
   */
  @API(since = "1.22", status = API.Status.EXPERIMENTAL)
  PLAN_BEFORE_IMPLEMENTATION;

  @SuppressWarnings("ImmutableEnumChecker")
  private final List<Consumer<Object>> handlers =
      new CopyOnWriteArrayList<>();

  @SuppressWarnings("ImmutableEnumChecker")
  private final TryThreadLocal<List<Consumer<Object>>> threadHandlers =
      TryThreadLocal.withInitial(ArrayList::new);

  /** Adds a handler for this Hook.
   *
   * <p>Returns a {@link Hook.Closeable} so that you can use the following
   * try-finally pattern to prevent leaks:
   *
   * <blockquote><pre>
   *     final Hook.Closeable closeable = Hook.FOO.add(HANDLER);
   *     try {
   *         ...
   *     } finally {
   *         closeable.close();
   *     }</pre>
   * </blockquote>
   *
   * @deprecated this installs a global hook (cross-thread), so it might have greater impact
   *     than expected. Use with caution. Prefer thread-local hooks.
   * @see #addThread(Consumer)
   */
  @API(status = API.Status.MAINTAINED)
  @Deprecated
  public <T> Closeable add(final Consumer<T> handler) {
    //noinspection unchecked
    handlers.add((Consumer<Object>) handler);
    return () -> remove(handler);
  }

  // CHECKSTYLE: IGNORE 1
  /** @deprecated Use {@link #add(Consumer)}. */
  @SuppressWarnings({"Guava", "ReturnValueIgnored"})
  @Deprecated // to be removed before 2.0
  public <T, R> Closeable add(final Function<T, R> handler) {
    return add((Consumer<T>) handler::apply);
  }

  /** Removes a handler from this Hook. */
  private boolean remove(Consumer handler) {
    return handlers.remove(handler);
  }

  /** Adds a handler for this thread. */
  public <T> Closeable addThread(final Consumer<T> handler) {
    //noinspection unchecked
    threadHandlers.get().add((Consumer<Object>) handler);
    return () -> removeThread(handler);
  }

  // CHECKSTYLE: IGNORE 1
  /** @deprecated Use {@link #addThread(Consumer)}. */
  @SuppressWarnings("Guava")
  @Deprecated // to be removed before 2.0
  public <T, R> Closeable addThread(
      final com.google.common.base.Function<T, R> handler) {
    return addThread(functionConsumer(handler));
  }

  /** Converts a Guava function into a JDK consumer. */
  @SuppressWarnings("Guava")
  private static <T, R> Consumer<T> functionConsumer(
      com.google.common.base.Function<T, R> handler) {
    return t -> {
      // Squash ErrorProne warnings that the return of the function is not
      // used.
      R r = handler.apply(t);
      Util.discard(r);
    };
  }

  /** Removes a thread handler from this Hook. */
  @SuppressWarnings({"rawtypes", "UnusedReturnValue"})
  private boolean removeThread(Consumer handler) {
    return threadHandlers.get().remove(handler);
  }

  // CHECKSTYLE: IGNORE 1
  /** @deprecated Use {@link #propertyJ}. */
  @SuppressWarnings("Guava")
  @Deprecated // return type will change in 2.0
  public static <V> com.google.common.base.Function<Holder<V>, Void> property(final V v) {
    return holder -> {
      holder.set(v);
      return null;
    };
  }

  /** Returns a function that, when a hook is called, will "return" a given
   * value. (Because of the way hooks work, it "returns" the value by writing
   * into a {@link Holder}. */
  public static <V> Consumer<Holder<V>> propertyJ(final V v) {
    return holder -> {
      holder.set(v);
    };
  }

  /** Runs all handlers registered for this Hook, with the given argument. */
  public void run(Object arg) {
    for (Consumer<Object> handler : handlers) {
      handler.accept(arg);
    }
    for (Consumer<Object> handler : threadHandlers.get()) {
      handler.accept(arg);
    }
  }

  /** Returns the value of a property hook.
   * (Property hooks take a {@link Holder} as an argument.) */
  public <V> V get(V defaultValue) {
    final Holder<V> holder = Holder.of(defaultValue);
    run(holder);
    return holder.get();
  }

  /** Removes a Hook after use. */
  public interface Closeable extends AutoCloseable {
    /** Closeable that does nothing. */
    Closeable EMPTY = () -> { };

    // override, removing "throws"
    @Override void close();
  }
}