blob: 2e534f968d1f88cbc6b9e4133ae575f7831cc232 [file] [log] [blame]
/*
* 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<Object> 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<Object> convertArguments(
SqlOperatorBinding callBinding,
Function function,
SqlIdentifier opName,
boolean failOnNonLiteral
)
{
final RelDataTypeFactory typeFactory = callBinding.getTypeFactory();
final List<FunctionParameter> parameters = function.getParameters();
final List<Object> 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<Object> 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
{
}
}