All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.druid.sql.calcite.external.BaseUserDefinedTableMacro Maven / Gradle / Ivy

There is a newer version: 31.0.0
Show newest version
/*
 * 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.druid.sql.calcite.external;

import org.apache.calcite.adapter.enumerable.EnumUtils;
import org.apache.calcite.linq4j.tree.BlockBuilder;
import org.apache.calcite.linq4j.tree.Expression;
import org.apache.calcite.linq4j.tree.Expressions;
import org.apache.calcite.linq4j.tree.FunctionExpression;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypeFactoryImpl;
import org.apache.calcite.schema.Function;
import org.apache.calcite.schema.FunctionParameter;
import org.apache.calcite.schema.TableMacro;
import org.apache.calcite.schema.TranslatableTable;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlOperatorBinding;
import org.apache.calcite.sql.type.ArraySqlType;
import org.apache.calcite.sql.type.SqlOperandMetadata;
import org.apache.calcite.sql.type.SqlOperandTypeInference;
import org.apache.calcite.sql.type.SqlReturnTypeInference;
import org.apache.calcite.sql.validate.SqlUserDefinedTableMacro;
import org.apache.calcite.util.NlsString;
import org.apache.druid.java.util.common.IAE;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Druid-specific version of {@link SqlUserDefinedTableMacro} which
 * copies & overrides a bunch of code to handle string array arguments.
 * Would be best if Calcite handled such argument: retire this class if
 * we upgrade to a version of Calcite that handles this task.
 */
public class BaseUserDefinedTableMacro extends SqlUserDefinedTableMacro
{
  protected final TableMacro macro;

  public BaseUserDefinedTableMacro(
      final SqlIdentifier opName,
      final SqlReturnTypeInference returnTypeInference,
      final SqlOperandTypeInference operandTypeInference,
      final SqlOperandMetadata operandMetadata,
      final TableMacro tableMacro
  )
  {
    super(opName, SqlKind.OTHER_FUNCTION, returnTypeInference, operandTypeInference, operandMetadata, tableMacro);

    // Because Calcite's copy of the macro is private
    this.macro = tableMacro;
  }

  /**
   * Copy of Calcite method {@link SqlUserDefinedTableMacro#getTable} to add array and named parameter handling.
   */
  @Override
  public TranslatableTable getTable(SqlOperatorBinding callBinding)
  {
    List arguments = convertArguments(callBinding, macro, getNameAsId(), true);
    return macro.apply(arguments);
  }

  /**
   * Similar to Calcite method {@link SqlUserDefinedTableMacro#convertArguments}, but with array and
   * named parameter handling.
   */
  public static List convertArguments(
      SqlOperatorBinding callBinding,
      Function function,
      SqlIdentifier opName,
      boolean failOnNonLiteral
  )
  {
    final RelDataTypeFactory typeFactory = callBinding.getTypeFactory();
    final List parameters = function.getParameters();
    final List arguments = new ArrayList<>(callBinding.getOperandCount());

    for (int i = 0; i < parameters.size(); i++) {
      final FunctionParameter parameter = parameters.get(i);
      final RelDataType type = parameter.getType(typeFactory);
      try {
        final Object o;

        if (callBinding.isOperandLiteral(i, true)) {
          o = callBinding.getOperandLiteralValue(i, Object.class);
        } else {
          throw new NonLiteralException();
        }

        final Object o2 = coerce(o, type);
        arguments.add(o2);
      }
      catch (NonLiteralException e) {
        if (failOnNonLiteral) {
          throw new IAE(
              "All arguments of call to macro %s should be literal. Actual argument #%d (%s) is not literal",
              opName,
              parameter.getOrdinal(),
              parameter.getName()
          );
        }

        final Object value;
        if (type.isNullable()) {
          value = null;
        } else {
          // Odd default, given that we don't know the type is numeric. But this is what Calcite does upstream
          // in SqlUserDefinedTableMacro.
          value = 0L;
        }
        arguments.add(value);
      }
    }

    return arguments;
  }

  // Copy of Calcite method with Druid-specific code added
  private static Object coerce(Object o, RelDataType type) throws NonLiteralException
  {
    if (o == null) {
      return null;
    }
    // Druid-specific code to handle arrays. Although the type
    // is called an ARRAY in SQL, the actual argument is a generic
    // list, which we then convert to another list with the elements
    // coerced to the declared element type.
    if (type instanceof ArraySqlType) {
      RelDataType elementType = ((ArraySqlType) type).getComponentType();
      if (!(elementType instanceof RelDataTypeFactoryImpl.JavaType)) {
        throw new NonLiteralException();
      }

      // If a list (ARRAY), then coerce each member.
      if (!(o instanceof List)) {
        throw new NonLiteralException();
      }
      List arg = (List) o;
      List revised = new ArrayList<>(arg.size());
      for (Object value : arg) {
        Object element = coerce(value, elementType);
        if (element == null) {
          throw new NonLiteralException();
        }
        revised.add(element);
      }
      return revised;
    }
    if (!(type instanceof RelDataTypeFactoryImpl.JavaType)) {
      // If the type can't be converted, raise an error. Calcite returns null
      // which causes odd downstream failures that are hard to diagnose.
      throw new NonLiteralException();
    }
    final RelDataTypeFactoryImpl.JavaType javaType =
        (RelDataTypeFactoryImpl.JavaType) type;
    final Class clazz = javaType.getJavaClass();
    //noinspection unchecked
    if (clazz.isAssignableFrom(o.getClass())) {
      return o;
    }
    if (clazz == String.class && o instanceof NlsString) {
      return ((NlsString) o).getValue();
    }
    // We need optimization here for constant folding.
    // Not all the expressions can be interpreted (e.g. ternary), so
    // we rely on optimization capabilities to fold non-interpretable
    // expressions.
    BlockBuilder bb = new BlockBuilder();
    final Expression expr =
        EnumUtils.convert(Expressions.constant(o), clazz);
    bb.add(Expressions.return_(null, expr));
    final FunctionExpression convert =
        Expressions.lambda(bb.toBlock(), Collections.emptyList());
    return convert.compile().dynamicInvoke();
  }

  /**
   * Thrown when a non-literal occurs in an argument to a user-defined
   * table macro.
   */
  private static class NonLiteralException extends Exception
  {
  }
}