This is an automated email from the ASF dual-hosted git repository. jhyde pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/calcite.git
commit 19edf52c76c6a1507721f5bd37f2a33497aa0c4c Author: Julian Hyde <[email protected]> AuthorDate: Mon Jun 18 14:10:50 2018 -0700 [CALCITE-2569] UDFs that are table functions must implement SqlTableFunction and have CURSOR as their return type Validate that table functions are not used in contexts that require scalar functions, such as the SELECT clause. Change operand type ANY to mean 'scalar expression of any type (but not a cursor)', and add operand type IGNORE to mean skip validation - for an operand that is not an expression. TABLE is one of the few operators that accepts a CURSOR operand. Add SqlKind.INTERVAL_QUALIFIER. --- .../calcite/adapter/enumerable/EnumUtils.java | 20 ++- .../org/apache/calcite/model/ModelHandler.java | 5 +- .../org/apache/calcite/rel/core/RelFactories.java | 15 ++- .../apache/calcite/runtime/CalciteResource.java | 3 + .../schema/impl/ReflectiveFunctionBase.java | 5 + .../calcite/schema/impl/ScalarFunctionImpl.java | 31 +++++ .../java/org/apache/calcite/sql/SqlAsOperator.java | 2 +- .../org/apache/calcite/sql/SqlCallBinding.java | 86 ++++++++++++- .../apache/calcite/sql/SqlIntervalQualifier.java | 5 + .../main/java/org/apache/calcite/sql/SqlKind.java | 3 + .../java/org/apache/calcite/sql/SqlLiteral.java | 40 +++--- .../org/apache/calcite/sql/SqlOperatorBinding.java | 12 ++ .../org/apache/calcite/sql/SqlOverOperator.java | 2 +- .../org/apache/calcite/sql/SqlTableFunction.java | 33 +++++ .../main/java/org/apache/calcite/sql/SqlUtil.java | 24 ++-- .../apache/calcite/sql/SqlWindowTableFunction.java | 91 +++++++------- .../apache/calcite/sql/SqlWithinGroupOperator.java | 4 +- .../sql/fun/SqlArgumentAssignmentOperator.java | 2 +- .../sql/fun/SqlCollectionTableOperator.java | 2 +- .../calcite/sql/type/FamilyOperandTypeChecker.java | 18 ++- .../org/apache/calcite/sql/type/OperandTypes.java | 7 ++ .../org/apache/calcite/sql/type/SqlTypeFamily.java | 5 +- .../apache/calcite/sql/validate/AggVisitor.java | 22 ++-- .../calcite/sql/validate/ProcedureNamespace.java | 27 ++--- .../sql/validate/SqlUserDefinedTableFunction.java | 40 +++--- .../sql/validate/SqlUserDefinedTableMacro.java | 135 +++++---------------- .../calcite/sql/validate/SqlValidatorImpl.java | 21 ++++ .../apache/calcite/sql2rel/SqlToRelConverter.java | 5 +- .../calcite/runtime/CalciteResource.properties | 1 + .../apache/calcite/test/MockSqlOperatorTable.java | 111 ++++++++++------- .../calcite/test/SqlOperatorBindingTest.java | 89 +++++++++----- .../org/apache/calcite/test/SqlValidatorTest.java | 39 +++++- .../org/apache/calcite/test/TableFunctionTest.java | 32 +++-- .../apache/calcite/test/ExampleFunctionTest.java | 2 +- 34 files changed, 609 insertions(+), 330 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java index 846e523..87fd196 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java @@ -32,6 +32,7 @@ import org.apache.calcite.linq4j.tree.ConstantUntypedNull; import org.apache.calcite.linq4j.tree.Expression; import org.apache.calcite.linq4j.tree.ExpressionType; import org.apache.calcite.linq4j.tree.Expressions; +import org.apache.calcite.linq4j.tree.FunctionExpression; import org.apache.calcite.linq4j.tree.MethodCallExpression; import org.apache.calcite.linq4j.tree.MethodDeclaration; import org.apache.calcite.linq4j.tree.ParameterExpression; @@ -321,7 +322,7 @@ public class EnumUtils { */ public static Expression convert(Expression operand, Type toType) { final Type fromType = operand.getType(); - return EnumUtils.convert(operand, fromType, toType); + return convert(operand, fromType, toType); } /** @@ -541,6 +542,23 @@ public class EnumUtils { return Expressions.convert_(operand, toType); } + /** Converts a value to a given class. */ + public static <T> T evaluate(Object o, Class<T> clazz) { + // 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. + //noinspection unchecked + clazz = Primitive.box(clazz); + BlockBuilder bb = new BlockBuilder(); + final Expression expr = + convert(Expressions.constant(o), clazz); + bb.add(Expressions.return_(null, expr)); + final FunctionExpression convert = + Expressions.lambda(bb.toBlock(), ImmutableList.of()); + return clazz.cast(convert.compile().dynamicInvoke()); + } + private static boolean isA(Type fromType, Primitive primitive) { return Primitive.of(fromType) == primitive || Primitive.ofBox(fromType) == primitive; diff --git a/core/src/main/java/org/apache/calcite/model/ModelHandler.java b/core/src/main/java/org/apache/calcite/model/ModelHandler.java index edc367c..a674520 100644 --- a/core/src/main/java/org/apache/calcite/model/ModelHandler.java +++ b/core/src/main/java/org/apache/calcite/model/ModelHandler.java @@ -24,6 +24,7 @@ import org.apache.calcite.materialize.Lattice; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.schema.AggregateFunction; +import org.apache.calcite.schema.Function; import org.apache.calcite.schema.ScalarFunction; import org.apache.calcite.schema.Schema; import org.apache.calcite.schema.SchemaFactory; @@ -145,8 +146,8 @@ public class ModelHandler { return; } if (methodName != null && methodName.equals("*")) { - for (Map.Entry<String, ScalarFunction> entry - : ScalarFunctionImpl.createAll(clazz).entries()) { + for (Map.Entry<String, Function> entry + : ScalarFunctionImpl.functions(clazz).entries()) { String name = entry.getKey(); if (upCase) { name = name.toUpperCase(Locale.ROOT); diff --git a/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java b/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java index 7b49647..2649dce 100644 --- a/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java +++ b/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java @@ -46,9 +46,14 @@ import org.apache.calcite.rel.logical.LogicalUnion; import org.apache.calcite.rel.logical.LogicalValues; import org.apache.calcite.rel.metadata.RelColumnMapping; import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rex.RexCall; +import org.apache.calcite.rex.RexCallBinding; import org.apache.calcite.rex.RexLiteral; import org.apache.calcite.rex.RexNode; import org.apache.calcite.sql.SqlKind; +import org.apache.calcite.sql.SqlOperatorBinding; +import org.apache.calcite.sql.SqlTableFunction; +import org.apache.calcite.sql.type.SqlReturnTypeInference; import org.apache.calcite.tools.RelBuilder; import org.apache.calcite.tools.RelBuilderFactory; import org.apache.calcite.util.ImmutableBitSet; @@ -512,8 +517,16 @@ public class RelFactories { @Override public RelNode createTableFunctionScan(RelOptCluster cluster, List<RelNode> inputs, RexNode rexCall, Type elementType, Set<RelColumnMapping> columnMappings) { + final RexCall call = (RexCall) rexCall; + final SqlOperatorBinding callBinding = + new RexCallBinding(cluster.getTypeFactory(), call.getOperator(), + call.operands, ImmutableList.of()); + final SqlTableFunction operator = (SqlTableFunction) call.getOperator(); + final SqlReturnTypeInference rowTypeInference = + operator.getRowTypeInference(); + final RelDataType rowType = rowTypeInference.inferReturnType(callBinding); return LogicalTableFunctionScan.create(cluster, inputs, rexCall, - elementType, rexCall.getType(), columnMappings); + elementType, rowType, columnMappings); } } diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java index 53fe755..7987546 100644 --- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java +++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java @@ -554,6 +554,9 @@ public interface CalciteResource { ExInst<CalciteException> illegalArgumentForTableFunctionCall(String a0, String a1, String a2); + @BaseMessage("Cannot call table function here: ''{0}''") + ExInst<CalciteException> cannotCallTableFunctionHere(String a0); + @BaseMessage("''{0}'' is not a valid datetime format") ExInst<CalciteException> invalidDatetimeFormat(String a0); diff --git a/core/src/main/java/org/apache/calcite/schema/impl/ReflectiveFunctionBase.java b/core/src/main/java/org/apache/calcite/schema/impl/ReflectiveFunctionBase.java index 2ce8160..d01daf7 100644 --- a/core/src/main/java/org/apache/calcite/schema/impl/ReflectiveFunctionBase.java +++ b/core/src/main/java/org/apache/calcite/schema/impl/ReflectiveFunctionBase.java @@ -113,6 +113,11 @@ public abstract class ReflectiveFunctionBase implements Function { final int ordinal = builder.size(); builder.add( new FunctionParameter() { + @Override public String toString() { + return ordinal + ": " + name + " " + type.getSimpleName() + + (optional ? "?" : ""); + } + public int getOrdinal() { return ordinal; } diff --git a/core/src/main/java/org/apache/calcite/schema/impl/ScalarFunctionImpl.java b/core/src/main/java/org/apache/calcite/schema/impl/ScalarFunctionImpl.java index 1322607..4c2dc01 100644 --- a/core/src/main/java/org/apache/calcite/schema/impl/ScalarFunctionImpl.java +++ b/core/src/main/java/org/apache/calcite/schema/impl/ScalarFunctionImpl.java @@ -24,8 +24,10 @@ import org.apache.calcite.linq4j.function.SemiStrict; import org.apache.calcite.linq4j.function.Strict; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeFactory; +import org.apache.calcite.schema.Function; import org.apache.calcite.schema.ImplementableFunction; import org.apache.calcite.schema.ScalarFunction; +import org.apache.calcite.schema.TableFunction; import org.apache.calcite.sql.SqlOperatorBinding; import com.google.common.collect.ImmutableMultimap; @@ -52,6 +54,7 @@ public class ScalarFunctionImpl extends ReflectiveFunctionBase * Creates {@link org.apache.calcite.schema.ScalarFunction} for each method in * a given class. */ + @Deprecated // to be removed before 2.0 public static ImmutableMultimap<String, ScalarFunction> createAll( Class<?> clazz) { final ImmutableMultimap.Builder<String, ScalarFunction> builder = @@ -71,6 +74,34 @@ public class ScalarFunctionImpl extends ReflectiveFunctionBase } /** + * Returns a map of all functions based on the methods in a given class. + * It is keyed by method names and maps to both + * {@link org.apache.calcite.schema.ScalarFunction} + * and {@link org.apache.calcite.schema.TableFunction}. + */ + public static ImmutableMultimap<String, Function> functions(Class<?> clazz) { + final ImmutableMultimap.Builder<String, Function> builder = + ImmutableMultimap.builder(); + for (Method method : clazz.getMethods()) { + if (method.getDeclaringClass() == Object.class) { + continue; + } + if (!Modifier.isStatic(method.getModifiers()) + && !classHasPublicZeroArgsConstructor(clazz)) { + continue; + } + final TableFunction tableFunction = TableFunctionImpl.create(method); + if (tableFunction != null) { + builder.put(method.getName(), tableFunction); + } else { + final ScalarFunction function = create(method); + builder.put(method.getName(), function); + } + } + return builder.build(); + } + + /** * Creates {@link org.apache.calcite.schema.ScalarFunction} from given class. * * <p>If a method of the given name is not found or it does not suit, diff --git a/core/src/main/java/org/apache/calcite/sql/SqlAsOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlAsOperator.java index e90340d..514c29a 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlAsOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlAsOperator.java @@ -51,7 +51,7 @@ public class SqlAsOperator extends SqlSpecialOperator { true, ReturnTypes.ARG0, InferTypes.RETURN_TYPE, - OperandTypes.ANY_ANY); + OperandTypes.ANY_IGNORE); } protected SqlAsOperator(String name, SqlKind kind, int prec, diff --git a/core/src/main/java/org/apache/calcite/sql/SqlCallBinding.java b/core/src/main/java/org/apache/calcite/sql/SqlCallBinding.java index bd70dd4..d944812 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlCallBinding.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlCallBinding.java @@ -16,11 +16,15 @@ */ package org.apache.calcite.sql; +import org.apache.calcite.adapter.enumerable.EnumUtils; import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeFactoryImpl; import org.apache.calcite.runtime.CalciteException; import org.apache.calcite.runtime.Resources; +import org.apache.calcite.sql.fun.SqlLiteralChainOperator; import org.apache.calcite.sql.fun.SqlStdOperatorTable; import org.apache.calcite.sql.parser.SqlParserPos; +import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.sql.validate.SelectScope; import org.apache.calcite.sql.validate.SqlMonotonicity; import org.apache.calcite.sql.validate.SqlValidator; @@ -28,12 +32,16 @@ import org.apache.calcite.sql.validate.SqlValidatorException; import org.apache.calcite.sql.validate.SqlValidatorNamespace; import org.apache.calcite.sql.validate.SqlValidatorScope; import org.apache.calcite.sql.validate.SqlValidatorUtil; +import org.apache.calcite.util.ImmutableNullableList; import org.apache.calcite.util.NlsString; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; import static org.apache.calcite.util.Static.RESOURCE; @@ -212,20 +220,86 @@ public class SqlCallBinding extends SqlOperatorBinding { } @Override public <T> T getOperandLiteralValue(int ordinal, Class<T> clazz) { - try { - final SqlNode node = call.operand(ordinal); - return SqlLiteral.unchain(node).getValueAs(clazz); - } catch (IllegalArgumentException e) { + final SqlNode node = operand(ordinal); + return valueAs(node, clazz); + } + + @Override public Object getOperandLiteralValue(int ordinal, RelDataType type) { + if (!(type instanceof RelDataTypeFactoryImpl.JavaType)) { return null; } + final Class<?> clazz = ((RelDataTypeFactoryImpl.JavaType) type).getJavaClass(); + final Object o = getOperandLiteralValue(ordinal, Object.class); + if (o == null) { + return null; + } + if (clazz.isInstance(o)) { + return clazz.cast(o); + } + final Object o2 = o instanceof NlsString ? ((NlsString) o).getValue() : o; + return EnumUtils.evaluate(o2, clazz); + } + + private <T> T valueAs(SqlNode node, Class<T> clazz) { + final SqlLiteral literal; + switch (node.getKind()) { + case ARRAY_VALUE_CONSTRUCTOR: + final List<Object> list = new ArrayList<>(); + for (SqlNode o : ((SqlCall) node).getOperandList()) { + list.add(valueAs(o, Object.class)); + } + return clazz.cast(ImmutableNullableList.copyOf(list)); + + case MAP_VALUE_CONSTRUCTOR: + final ImmutableMap.Builder<Object, Object> builder2 = + ImmutableMap.builder(); + final List<SqlNode> operands = ((SqlCall) node).getOperandList(); + for (int i = 0; i < operands.size(); i += 2) { + final SqlNode key = operands.get(i); + final SqlNode value = operands.get(i + 1); + builder2.put(Objects.requireNonNull(valueAs(key, Object.class)), + Objects.requireNonNull(valueAs(value, Object.class))); + } + return clazz.cast(builder2.build()); + + case CAST: + return valueAs(((SqlCall) node).operand(0), clazz); + + case LITERAL: + literal = (SqlLiteral) node; + if (literal.getTypeName() == SqlTypeName.NULL) { + return null; + } + return literal.getValueAs(clazz); + + case LITERAL_CHAIN: + literal = SqlLiteralChainOperator.concatenateOperands((SqlCall) node); + return literal.getValueAs(clazz); + + case INTERVAL_QUALIFIER: + final SqlIntervalQualifier q = (SqlIntervalQualifier) node; + final SqlIntervalLiteral.IntervalValue intervalValue = + new SqlIntervalLiteral.IntervalValue(q, 1, q.toString()); + literal = new SqlLiteral(intervalValue, q.typeName(), q.pos); + return literal.getValueAs(clazz); + + case DEFAULT: + return null; // currently NULL is the only default value + + default: + if (SqlUtil.isNullLiteral(node, true)) { + return null; // NULL literal + } + return null; // not a literal + } } @Override public boolean isOperandNull(int ordinal, boolean allowCast) { - return SqlUtil.isNullLiteral(call.operand(ordinal), allowCast); + return SqlUtil.isNullLiteral(operand(ordinal), allowCast); } @Override public boolean isOperandLiteral(int ordinal, boolean allowCast) { - return SqlUtil.isLiteral(call.operand(ordinal), allowCast); + return SqlUtil.isLiteral(operand(ordinal), allowCast); } @Override public int getOperandCount() { diff --git a/core/src/main/java/org/apache/calcite/sql/SqlIntervalQualifier.java b/core/src/main/java/org/apache/calcite/sql/SqlIntervalQualifier.java index 032b5db..e0d0cd9 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlIntervalQualifier.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlIntervalQualifier.java @@ -33,6 +33,7 @@ import java.math.BigDecimal; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.Nonnull; import static org.apache.calcite.util.Static.RESOURCE; @@ -127,6 +128,10 @@ public class SqlIntervalQualifier extends SqlNode { //~ Methods ---------------------------------------------------------------- + @Nonnull @Override public SqlKind getKind() { + return SqlKind.INTERVAL_QUALIFIER; + } + public SqlTypeName typeName() { switch (timeUnitRange) { case YEAR: diff --git a/core/src/main/java/org/apache/calcite/sql/SqlKind.java b/core/src/main/java/org/apache/calcite/sql/SqlKind.java index 1fee9bf..caaa6ba 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlKind.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlKind.java @@ -120,6 +120,9 @@ public enum SqlKind { /** A literal. */ LITERAL, + /** Interval qualifier. */ + INTERVAL_QUALIFIER, + /** * Function that is not a special function. * diff --git a/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java b/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java index 7ce5fa1..c3b7981 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java @@ -17,6 +17,7 @@ package org.apache.calcite.sql; import org.apache.calcite.avatica.util.TimeUnitRange; +import org.apache.calcite.rel.metadata.NullSentinel; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.sql.fun.SqlLiteralChainOperator; @@ -43,6 +44,7 @@ import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; import java.util.Calendar; import java.util.Objects; +import javax.annotation.Nonnull; import static org.apache.calcite.util.Static.RESOURCE; @@ -234,7 +236,7 @@ public class SqlLiteral extends SqlNode { return new SqlLiteral(value, typeName, pos); } - public SqlKind getKind() { + public @Nonnull SqlKind getKind() { return SqlKind.LITERAL; } @@ -253,24 +255,33 @@ public class SqlLiteral extends SqlNode { } /** - * Returns the value of this literal as a particular type. + * Returns the value of this literal as a given Java type. * - * <p>The type might be the internal type, or other convenient types. - * For example, numeric literals' values are stored internally as + * <p>Which type you may ask for depends on {@link #typeName}. + * You may always ask for the type where we store the value internally + * (as defined by {@link #valueMatchesType(Object, SqlTypeName)}), but may + * ask for other convenient types. + * + * <p>For example, numeric literals' values are stored internally as * {@link BigDecimal}, but other numeric types such as {@link Long} and * {@link Double} are also allowed. * + * <p>The result is never null. For the NULL literal, returns + * a {@link NullSentinel#INSTANCE}. + * * @param clazz Desired value type * @param <T> Value type - * @return Value of the literal + * @return Value of the literal in desired type, never null * * @throws AssertionError if the value type is not supported */ - public <T> T getValueAs(Class<T> clazz) { + @Nonnull public <T> T getValueAs(Class<T> clazz) { if (clazz.isInstance(value)) { return clazz.cast(value); } switch (typeName) { + case NULL: + return clazz.cast(NullSentinel.INSTANCE); case CHAR: if (clazz == String.class) { return clazz.cast(((NlsString) value).getValue()); @@ -445,11 +456,10 @@ public class SqlLiteral extends SqlNode { assert SqlTypeUtil.inCharFamily(literal.getTypeName()); return (NlsString) literal.value; } - if (node instanceof SqlIntervalQualifier) { - SqlIntervalQualifier qualifier = (SqlIntervalQualifier) node; - return qualifier.timeUnitRange; - } switch (node.getKind()) { + case INTERVAL_QUALIFIER: + //noinspection ConstantConditions + return ((SqlIntervalQualifier) node).timeUnitRange; case CAST: assert node instanceof SqlCall; return value(((SqlCall) node).operand(0)); @@ -485,7 +495,6 @@ public class SqlLiteral extends SqlNode { return literal.value.toString(); } else if (node instanceof SqlCall && ((SqlCall) node).getOperator() == SqlStdOperatorTable.CAST) { - //noinspection deprecation return stringValue(((SqlCall) node).operand(0)); } else { throw new AssertionError("invalid string literal: " + node); @@ -499,16 +508,17 @@ public class SqlLiteral extends SqlNode { * and cannot be unchained. */ public static SqlLiteral unchain(SqlNode node) { - if (node instanceof SqlLiteral) { + switch (node.getKind()) { + case LITERAL: return (SqlLiteral) node; - } else if (SqlUtil.isLiteralChain(node)) { + case LITERAL_CHAIN: return SqlLiteralChainOperator.concatenateOperands((SqlCall) node); - } else if (node instanceof SqlIntervalQualifier) { + case INTERVAL_QUALIFIER: final SqlIntervalQualifier q = (SqlIntervalQualifier) node; return new SqlLiteral( new SqlIntervalLiteral.IntervalValue(q, 1, q.toString()), q.typeName(), q.pos); - } else { + default: throw new IllegalArgumentException("invalid literal: " + node); } } diff --git a/core/src/main/java/org/apache/calcite/sql/SqlOperatorBinding.java b/core/src/main/java/org/apache/calcite/sql/SqlOperatorBinding.java index 21d1fd9..aabf09e 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlOperatorBinding.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlOperatorBinding.java @@ -135,6 +135,18 @@ public abstract class SqlOperatorBinding { throw new UnsupportedOperationException(); } + /** + * Gets the value of a literal operand as a Calcite type. + * + * @param ordinal zero-based ordinal of operand of interest + * @param type Desired valued type + * + * @return value of operand + */ + public Object getOperandLiteralValue(int ordinal, RelDataType type) { + throw new UnsupportedOperationException(); + } + @Deprecated // to be removed before 2.0 public Comparable getOperandLiteralValue(int ordinal) { return getOperandLiteralValue(ordinal, Comparable.class); diff --git a/core/src/main/java/org/apache/calcite/sql/SqlOverOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlOverOperator.java index 5f6cb3f..99b7a63 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlOverOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlOverOperator.java @@ -52,7 +52,7 @@ public class SqlOverOperator extends SqlBinaryOperator { true, ReturnTypes.ARG0_FORCE_NULLABLE, null, - OperandTypes.ANY_ANY); + OperandTypes.ANY_IGNORE); } //~ Methods ---------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/sql/SqlTableFunction.java b/core/src/main/java/org/apache/calcite/sql/SqlTableFunction.java new file mode 100644 index 0000000..af79dd5 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/sql/SqlTableFunction.java @@ -0,0 +1,33 @@ +/* + * 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.sql; + +import org.apache.calcite.sql.type.SqlReturnTypeInference; + +/** + * A function that returns a table. + */ +public interface SqlTableFunction { + /** + * Returns the record type of the table yielded by this function when + * applied to given arguments. Only literal arguments are passed, + * non-literal are replaced with default values (null, 0, false, etc). + * + * @return strategy to infer the row type of a call to this function + */ + SqlReturnTypeInference getRowTypeInference(); +} diff --git a/core/src/main/java/org/apache/calcite/sql/SqlUtil.java b/core/src/main/java/org/apache/calcite/sql/SqlUtil.java index 18dc6bb..9bfdd92 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlUtil.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlUtil.java @@ -216,16 +216,22 @@ public abstract class SqlUtil { if (node instanceof SqlLiteral) { return true; } - if (allowCast) { - if (node.getKind() == SqlKind.CAST) { - SqlCall call = (SqlCall) node; - if (isLiteral(call.operand(0), false)) { - // node is "CAST(literal as type)" - return true; - } - } + if (!allowCast) { + return false; + } + switch (node.getKind()) { + case CAST: + // "CAST(e AS type)" is literal if "e" is literal + return isLiteral(((SqlCall) node).operand(0), true); + case MAP_VALUE_CONSTRUCTOR: + case ARRAY_VALUE_CONSTRUCTOR: + return ((SqlCall) node).getOperandList().stream() + .allMatch(o -> isLiteral(o, true)); + case DEFAULT: + return true; // DEFAULT is always NULL + default: + return false; } - return false; } /** diff --git a/core/src/main/java/org/apache/calcite/sql/SqlWindowTableFunction.java b/core/src/main/java/org/apache/calcite/sql/SqlWindowTableFunction.java index 309efd5..f17a383 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlWindowTableFunction.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlWindowTableFunction.java @@ -17,31 +17,46 @@ package org.apache.calcite.sql; import org.apache.calcite.rel.type.RelDataType; -import org.apache.calcite.rel.type.RelDataTypeField; -import org.apache.calcite.rel.type.RelDataTypeFieldImpl; -import org.apache.calcite.rel.type.RelRecordType; +import org.apache.calcite.rel.type.RelDataTypeFactory; +import org.apache.calcite.sql.type.ReturnTypes; import org.apache.calcite.sql.type.SqlReturnTypeInference; import org.apache.calcite.sql.type.SqlTypeName; +import org.apache.calcite.sql.validate.SqlNameMatcher; import org.apache.calcite.sql.validate.SqlValidator; -import java.util.ArrayList; import java.util.List; import static org.apache.calcite.util.Static.RESOURCE; /** - * Base class for table-valued function windowing operator (TUMBLE, HOP and SESSION). + * Base class for a table-valued function that computes windows. Examples + * include {@code TUMBLE}, {@code HOP} and {@code SESSION}. */ -public class SqlWindowTableFunction extends SqlFunction { +public class SqlWindowTableFunction extends SqlFunction + implements SqlTableFunction { + /** + * Type-inference strategy whereby the row type of a table function call is a + * ROW, which is combined from the row type of operand #0 (which is a TABLE) + * and two additional fields. The fields are as follows: + * + * <ol> + * <li>{@code window_start}: TIMESTAMP type to indicate a window's start + * <li>{@code window_end}: TIMESTAMP type to indicate a window's end + * </ol> + */ + public static final SqlReturnTypeInference ARG0_TABLE_FUNCTION_WINDOWING = + SqlWindowTableFunction::inferRowType; + + /** Creates a window table function with a given name. */ public SqlWindowTableFunction(String name) { - super(name, - SqlKind.OTHER_FUNCTION, - ARG0_TABLE_FUNCTION_WINDOWING, - null, - null, + super(name, SqlKind.OTHER_FUNCTION, ReturnTypes.CURSOR, null, null, SqlFunctionCategory.SYSTEM); } + @Override public SqlReturnTypeInference getRowTypeInference() { + return ARG0_TABLE_FUNCTION_WINDOWING; + } + protected boolean throwValidationSignatureErrorOrReturnFalse(SqlCallBinding callBinding, boolean throwOnFailure) { if (throwOnFailure) { @@ -53,16 +68,10 @@ public class SqlWindowTableFunction extends SqlFunction { protected void validateColumnNames(SqlValidator validator, List<String> fieldNames, List<SqlNode> unvalidatedColumnNames) { - for (SqlNode descOperand: unvalidatedColumnNames) { + final SqlNameMatcher matcher = validator.getCatalogReader().nameMatcher(); + for (SqlNode descOperand : unvalidatedColumnNames) { final String colName = ((SqlIdentifier) descOperand).getSimple(); - boolean matches = false; - for (String field : fieldNames) { - if (validator.getCatalogReader().nameMatcher().matches(field, colName)) { - matches = true; - break; - } - } - if (!matches) { + if (matcher.frequency(fieldNames, colName) == 0) { throw SqlUtil.newContextException(descOperand.getParserPosition(), RESOURCE.unknownIdentifier(colName)); } @@ -70,7 +79,9 @@ public class SqlWindowTableFunction extends SqlFunction { } /** - * Overrides SqlOperator.argumentMustBeScalar because the first parameter of + * {@inheritDoc} + * + * <p>Overrides because the first parameter of * table-value function windowing is an explicit TABLE parameter, * which is not scalar. */ @@ -78,29 +89,17 @@ public class SqlWindowTableFunction extends SqlFunction { return ordinal != 0; } - /** - * Type-inference strategy whereby the result type of a table function call is - * a ROW, which is combined from the operand #0(TABLE parameter)'s schema and - * two additional fields. The fields are as follows: - * - * <ol> - * <li>window_start: TIMESTAMP type to indicate a window's start.</li> - * <li>window_end: TIMESTAMP type to indicate a window's end.</li> - * </ol> - */ - public static final SqlReturnTypeInference ARG0_TABLE_FUNCTION_WINDOWING = - opBinding -> { - RelDataType inputRowType = opBinding.getOperandType(0); - List<RelDataTypeField> newFields = new ArrayList<>(inputRowType.getFieldList()); - RelDataType timestampType = opBinding.getTypeFactory().createSqlType(SqlTypeName.TIMESTAMP); - - RelDataTypeField windowStartField = - new RelDataTypeFieldImpl("window_start", newFields.size(), timestampType); - newFields.add(windowStartField); - RelDataTypeField windowEndField = - new RelDataTypeFieldImpl("window_end", newFields.size(), timestampType); - newFields.add(windowEndField); - - return new RelRecordType(inputRowType.getStructKind(), newFields); - }; + /** Helper for {@link #ARG0_TABLE_FUNCTION_WINDOWING}. */ + private static RelDataType inferRowType(SqlOperatorBinding opBinding) { + final RelDataType inputRowType = opBinding.getOperandType(0); + final RelDataTypeFactory typeFactory = opBinding.getTypeFactory(); + final RelDataType timestampType = + typeFactory.createSqlType(SqlTypeName.TIMESTAMP); + return typeFactory.builder() + .kind(inputRowType.getStructKind()) + .addAll(inputRowType.getFieldList()) + .add("window_start", timestampType) + .add("window_end", timestampType) + .build(); + } } diff --git a/core/src/main/java/org/apache/calcite/sql/SqlWithinGroupOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlWithinGroupOperator.java index b20fee9..d5402a6 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlWithinGroupOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlWithinGroupOperator.java @@ -38,7 +38,7 @@ public class SqlWithinGroupOperator extends SqlBinaryOperator { public SqlWithinGroupOperator() { super("WITHIN GROUP", SqlKind.WITHIN_GROUP, 100, true, ReturnTypes.ARG0, - null, OperandTypes.ANY_ANY); + null, OperandTypes.ANY_IGNORE); } @Override public void unparse(SqlWriter writer, SqlCall call, int leftPrec, int rightPrec) { @@ -48,7 +48,7 @@ public class SqlWithinGroupOperator extends SqlBinaryOperator { final SqlWriter.Frame orderFrame = writer.startList(SqlWriter.FrameTypeEnum.ORDER_BY_LIST, "(", ")"); writer.keyword("ORDER BY"); - ((SqlNodeList) call.operand(1)).unparse(writer, 0, 0); + call.operand(1).unparse(writer, 0, 0); writer.endList(orderFrame); } diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlArgumentAssignmentOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlArgumentAssignmentOperator.java index b5298d9..f303eda 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlArgumentAssignmentOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlArgumentAssignmentOperator.java @@ -37,7 +37,7 @@ import org.apache.calcite.sql.type.ReturnTypes; class SqlArgumentAssignmentOperator extends SqlAsOperator { SqlArgumentAssignmentOperator() { super("=>", SqlKind.ARGUMENT_ASSIGNMENT, 20, true, ReturnTypes.ARG0, - InferTypes.RETURN_TYPE, OperandTypes.ANY_ANY); + InferTypes.RETURN_TYPE, OperandTypes.ANY_IGNORE); } @Override public void unparse(SqlWriter writer, SqlCall call, int leftPrec, diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlCollectionTableOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlCollectionTableOperator.java index 95ac447..83dddc7 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlCollectionTableOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlCollectionTableOperator.java @@ -37,7 +37,7 @@ public class SqlCollectionTableOperator extends SqlFunctionalOperator { public SqlCollectionTableOperator(String name, SqlModality modality) { super(name, SqlKind.COLLECTION_TABLE, 200, true, ReturnTypes.ARG0, null, - OperandTypes.ANY); + OperandTypes.CURSOR); this.modality = modality; } diff --git a/core/src/main/java/org/apache/calcite/sql/type/FamilyOperandTypeChecker.java b/core/src/main/java/org/apache/calcite/sql/type/FamilyOperandTypeChecker.java index a37848d..de9e186 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/FamilyOperandTypeChecker.java +++ b/core/src/main/java/org/apache/calcite/sql/type/FamilyOperandTypeChecker.java @@ -65,8 +65,22 @@ public class FamilyOperandTypeChecker implements SqlSingleOperandTypeChecker, SqlNode node, int iFormalOperand, boolean throwOnFailure) { - SqlTypeFamily family = families.get(iFormalOperand); - if (family == SqlTypeFamily.ANY) { + final SqlTypeFamily family = families.get(iFormalOperand); + switch (family) { + case ANY: + final RelDataType type = callBinding.getValidator() + .deriveType(callBinding.getScope(), node); + SqlTypeName typeName = type.getSqlTypeName(); + + if (typeName == SqlTypeName.CURSOR) { + // We do not allow CURSOR operands, even for ANY + if (throwOnFailure) { + throw callBinding.newValidationSignatureError(); + } + return false; + } + // fall through + case IGNORE: // no need to check return true; } diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java index f23cd2a..37d0250 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java +++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java @@ -441,9 +441,16 @@ public abstract class OperandTypes { public static final SqlSingleOperandTypeChecker ANY_ANY = family(SqlTypeFamily.ANY, SqlTypeFamily.ANY); + public static final SqlSingleOperandTypeChecker ANY_IGNORE = + family(SqlTypeFamily.ANY, SqlTypeFamily.IGNORE); + public static final SqlSingleOperandTypeChecker IGNORE_ANY = + family(SqlTypeFamily.IGNORE, SqlTypeFamily.ANY); public static final SqlSingleOperandTypeChecker ANY_NUMERIC = family(SqlTypeFamily.ANY, SqlTypeFamily.NUMERIC); + public static final SqlSingleOperandTypeChecker CURSOR = + family(SqlTypeFamily.CURSOR); + /** * Parameter type-checking strategy where type must a nullable time interval, * nullable time interval. diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java index 3a689c5..1666e83 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java +++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java @@ -72,7 +72,10 @@ public enum SqlTypeFamily implements RelDataTypeFamily { ANY, CURSOR, COLUMN_LIST, - GEO; + GEO, + /** Like ANY, but do not even validate the operand. It may not be an + * expression. */ + IGNORE; private static final Map<Integer, SqlTypeFamily> JDBC_TYPE_TO_FAMILY = ImmutableMap.<Integer, SqlTypeFamily>builder() diff --git a/core/src/main/java/org/apache/calcite/sql/validate/AggVisitor.java b/core/src/main/java/org/apache/calcite/sql/validate/AggVisitor.java index 10c384a..3f63e34 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/AggVisitor.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/AggVisitor.java @@ -18,6 +18,7 @@ package org.apache.calcite.sql.validate; import org.apache.calcite.sql.SqlCall; import org.apache.calcite.sql.SqlFunction; +import org.apache.calcite.sql.SqlIdentifier; import org.apache.calcite.sql.SqlKind; import org.apache.calcite.sql.SqlOperator; import org.apache.calcite.sql.SqlOperatorTable; @@ -87,15 +88,18 @@ abstract class AggVisitor extends SqlBasicVisitor<Void> { final SqlFunction sqlFunction = (SqlFunction) operator; if (sqlFunction.getFunctionType().isUserDefinedNotSpecificFunction()) { final List<SqlOperator> list = new ArrayList<>(); - opTab.lookupOperatorOverloads(sqlFunction.getSqlIdentifier(), - sqlFunction.getFunctionType(), SqlSyntax.FUNCTION, list, - nameMatcher); - for (SqlOperator operator2 : list) { - if (operator2.isAggregator() && !operator2.requiresOver()) { - // If nested aggregates disallowed or found aggregate at invalid - // level - if (aggregate) { - found(call); + final SqlIdentifier identifier = sqlFunction.getSqlIdentifier(); + if (identifier != null) { + opTab.lookupOperatorOverloads(identifier, + sqlFunction.getFunctionType(), SqlSyntax.FUNCTION, list, + nameMatcher); + for (SqlOperator operator2 : list) { + if (operator2.isAggregator() && !operator2.requiresOver()) { + // If nested aggregates disallowed or found aggregate at invalid + // level + if (aggregate) { + found(call); + } } } } diff --git a/core/src/main/java/org/apache/calcite/sql/validate/ProcedureNamespace.java b/core/src/main/java/org/apache/calcite/sql/validate/ProcedureNamespace.java index 46f4534..c4ffb6d 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/ProcedureNamespace.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/ProcedureNamespace.java @@ -21,6 +21,8 @@ import org.apache.calcite.sql.SqlCall; import org.apache.calcite.sql.SqlCallBinding; import org.apache.calcite.sql.SqlNode; import org.apache.calcite.sql.SqlOperator; +import org.apache.calcite.sql.SqlTableFunction; +import org.apache.calcite.sql.type.SqlReturnTypeInference; import org.apache.calcite.sql.type.SqlTypeName; /** @@ -54,21 +56,18 @@ public class ProcedureNamespace extends AbstractNamespace { final SqlOperator operator = call.getOperator(); final SqlCallBinding callBinding = new SqlCallBinding(validator, scope, call); - if (operator instanceof SqlUserDefinedTableFunction) { - assert type.getSqlTypeName() == SqlTypeName.CURSOR - : "User-defined table function should have CURSOR type, not " + type; - final SqlUserDefinedTableFunction udf = - (SqlUserDefinedTableFunction) operator; - return udf.getRowType(validator.typeFactory, callBinding.operands()); - } else if (operator instanceof SqlUserDefinedTableMacro) { - assert type.getSqlTypeName() == SqlTypeName.CURSOR - : "User-defined table macro should have CURSOR type, not " + type; - final SqlUserDefinedTableMacro udf = - (SqlUserDefinedTableMacro) operator; - return udf.getTable(validator.typeFactory, callBinding.operands()) - .getRowType(validator.typeFactory); + if (!(operator instanceof SqlTableFunction)) { + throw new IllegalArgumentException("Argument must be a table function: " + + operator.getNameAsId()); } - return type; + final SqlTableFunction tableFunction = (SqlTableFunction) operator; + if (type.getSqlTypeName() != SqlTypeName.CURSOR) { + throw new IllegalArgumentException("Table function should have CURSOR " + + "type, not " + type); + } + final SqlReturnTypeInference rowTypeInference = + tableFunction.getRowTypeInference(); + return rowTypeInference.inferReturnType(callBinding); } public SqlNode getNode() { diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlUserDefinedTableFunction.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlUserDefinedTableFunction.java index 622b20b..735d299 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlUserDefinedTableFunction.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlUserDefinedTableFunction.java @@ -17,11 +17,11 @@ package org.apache.calcite.sql.validate; import org.apache.calcite.rel.type.RelDataType; -import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.schema.TableFunction; import org.apache.calcite.sql.SqlFunctionCategory; import org.apache.calcite.sql.SqlIdentifier; -import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.SqlOperatorBinding; +import org.apache.calcite.sql.SqlTableFunction; import org.apache.calcite.sql.type.SqlOperandTypeChecker; import org.apache.calcite.sql.type.SqlOperandTypeInference; import org.apache.calcite.sql.type.SqlReturnTypeInference; @@ -35,7 +35,8 @@ import java.util.List; * <p>Created by the validator, after resolving a function call to a function * defined in a Calcite schema. */ -public class SqlUserDefinedTableFunction extends SqlUserDefinedFunction { +public class SqlUserDefinedTableFunction extends SqlUserDefinedFunction + implements SqlTableFunction { public SqlUserDefinedTableFunction(SqlIdentifier opName, SqlReturnTypeInference returnTypeInference, SqlOperandTypeInference operandTypeInference, @@ -54,22 +55,15 @@ public class SqlUserDefinedTableFunction extends SqlUserDefinedFunction { return (TableFunction) super.getFunction(); } - /** - * Returns the record type of the table yielded by this function when - * applied to given arguments. Only literal arguments are passed, - * non-literal are replaced with default values (null, 0, false, etc). - * - * @param typeFactory Type factory - * @param operandList arguments of a function call (only literal arguments - * are passed, nulls for non-literal ones) - * @return row type of the table - */ - public RelDataType getRowType(RelDataTypeFactory typeFactory, - List<SqlNode> operandList) { + @Override public SqlReturnTypeInference getRowTypeInference() { + return this::inferRowType; + } + + private RelDataType inferRowType(SqlOperatorBinding callBinding) { List<Object> arguments = - SqlUserDefinedTableMacro.convertArguments(typeFactory, operandList, - function, getNameAsId(), false); - return getFunction().getRowType(typeFactory, arguments); + SqlUserDefinedTableMacro.convertArguments(callBinding, function, + getNameAsId(), false); + return getFunction().getRowType(callBinding.getTypeFactory(), arguments); } /** @@ -77,15 +71,13 @@ public class SqlUserDefinedTableFunction extends SqlUserDefinedFunction { * applied to given arguments. Only literal arguments are passed, * non-literal are replaced with default values (null, 0, false, etc). * - * @param operandList arguments of a function call (only literal arguments - * are passed, nulls for non-literal ones) + * @param callBinding Operand bound to arguments * @return element type of the table (e.g. {@code Object[].class}) */ - public Type getElementType(RelDataTypeFactory typeFactory, - List<SqlNode> operandList) { + public Type getElementType(SqlOperatorBinding callBinding) { List<Object> arguments = - SqlUserDefinedTableMacro.convertArguments(typeFactory, operandList, - function, getNameAsId(), false); + SqlUserDefinedTableMacro.convertArguments(callBinding, function, + getNameAsId(), false); return getFunction().getElementType(arguments); } } diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlUserDefinedTableMacro.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlUserDefinedTableMacro.java index 3d4dbd7..b9066dd 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlUserDefinedTableMacro.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlUserDefinedTableMacro.java @@ -16,39 +16,27 @@ */ package org.apache.calcite.sql.validate; -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.linq4j.Ord; 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.SqlCall; import org.apache.calcite.sql.SqlFunction; import org.apache.calcite.sql.SqlFunctionCategory; import org.apache.calcite.sql.SqlIdentifier; import org.apache.calcite.sql.SqlKind; -import org.apache.calcite.sql.SqlLiteral; -import org.apache.calcite.sql.SqlNode; -import org.apache.calcite.sql.SqlUtil; +import org.apache.calcite.sql.SqlOperatorBinding; +import org.apache.calcite.sql.SqlTableFunction; import org.apache.calcite.sql.type.SqlOperandTypeChecker; import org.apache.calcite.sql.type.SqlOperandTypeInference; import org.apache.calcite.sql.type.SqlReturnTypeInference; -import org.apache.calcite.util.ImmutableNullableList; -import org.apache.calcite.util.NlsString; -import org.apache.calcite.util.Pair; import org.apache.calcite.util.Util; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; @@ -58,7 +46,8 @@ import java.util.Objects; * <p>Created by the validator, after resolving a function call to a function * defined in a Calcite schema. */ -public class SqlUserDefinedTableMacro extends SqlFunction { +public class SqlUserDefinedTableMacro extends SqlFunction + implements SqlTableFunction { private final TableMacro tableMacro; public SqlUserDefinedTableMacro(SqlIdentifier opName, @@ -79,10 +68,9 @@ public class SqlUserDefinedTableMacro extends SqlFunction { } /** Returns the table in this UDF, or null if there is no table. */ - public TranslatableTable getTable(RelDataTypeFactory typeFactory, - List<SqlNode> operandList) { - List<Object> arguments = convertArguments(typeFactory, operandList, - tableMacro, getNameAsId(), true); + public TranslatableTable getTable(SqlOperatorBinding callBinding) { + List<Object> arguments = + convertArguments(callBinding, tableMacro, getNameAsId(), true); return tableMacro.apply(arguments); } @@ -90,111 +78,46 @@ public class SqlUserDefinedTableMacro extends SqlFunction { * Converts arguments from {@link org.apache.calcite.sql.SqlNode} to * java object format. * - * @param typeFactory type factory used to convert the arguments - * @param operandList input arguments + * @param callBinding Operator bound to arguments * @param function target function to get parameter types from * @param opName name of the operator to use in error message * @param failOnNonLiteral true when conversion should fail on non-literal * @return converted list of arguments */ - public static List<Object> convertArguments(RelDataTypeFactory typeFactory, - List<SqlNode> operandList, Function function, - SqlIdentifier opName, - boolean failOnNonLiteral) { - List<Object> arguments = new ArrayList<>(operandList.size()); - // Construct a list of arguments, if they are all constants. - for (Pair<FunctionParameter, SqlNode> pair - : Pair.zip(function.getParameters(), operandList)) { - try { - final Object o = getValue(pair.right); - final Object o2 = coerce(o, pair.left.getType(typeFactory)); - arguments.add(o2); - } catch (NonLiteralException e) { + static List<Object> convertArguments(SqlOperatorBinding callBinding, + Function function, SqlIdentifier opName, boolean failOnNonLiteral) { + RelDataTypeFactory typeFactory = callBinding.getTypeFactory(); + List<Object> arguments = new ArrayList<>(callBinding.getOperandCount()); + Ord.forEach(function.getParameters(), (parameter, i) -> { + final RelDataType type = parameter.getType(typeFactory); + final Object value; + if (callBinding.isOperandLiteral(i, true)) { + value = callBinding.getOperandLiteralValue(i, type); + } else { if (failOnNonLiteral) { throw new IllegalArgumentException("All arguments of call to macro " + opName + " should be literal. Actual argument #" - + pair.left.getOrdinal() + " (" + pair.left.getName() - + ") is not literal: " + pair.right); + + parameter.getOrdinal() + " (" + parameter.getName() + + ") is not literal"); } - final RelDataType type = pair.left.getType(typeFactory); - final Object value; if (type.isNullable()) { value = null; } else { value = 0L; } - arguments.add(value); } - } + arguments.add(value); + }); return arguments; } - private static Object getValue(SqlNode right) throws NonLiteralException { - switch (right.getKind()) { - case ARRAY_VALUE_CONSTRUCTOR: - final List<Object> list = new ArrayList<>(); - for (SqlNode o : ((SqlCall) right).getOperandList()) { - list.add(getValue(o)); - } - return ImmutableNullableList.copyOf(list); - case MAP_VALUE_CONSTRUCTOR: - final ImmutableMap.Builder<Object, Object> builder2 = - ImmutableMap.builder(); - final List<SqlNode> operands = ((SqlCall) right).getOperandList(); - for (int i = 0; i < operands.size(); i += 2) { - final SqlNode key = operands.get(i); - final SqlNode value = operands.get(i + 1); - builder2.put(getValue(key), getValue(value)); - } - return builder2.build(); - case CAST: - return getValue(((SqlCall) right).operand(0)); - default: - if (SqlUtil.isNullLiteral(right, true)) { - return null; - } - if (SqlUtil.isLiteral(right)) { - return ((SqlLiteral) right).getValue(); - } - if (right.getKind() == SqlKind.DEFAULT) { - return null; // currently NULL is the only default value - } - throw new NonLiteralException(); - } - } - - private static Object coerce(Object o, RelDataType type) { - if (o == null) { - return null; - } - if (!(type instanceof RelDataTypeFactoryImpl.JavaType)) { - return null; - } - final RelDataTypeFactoryImpl.JavaType javaType = - (RelDataTypeFactoryImpl.JavaType) type; - final Class clazz = javaType.getJavaClass(); - //noinspection unchecked - if (clazz.isAssignableFrom(o.getClass())) { - return o; - } - if (o instanceof NlsString) { - return coerce(((NlsString) o).getValue(), type); - } - // 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(); + @Override public SqlReturnTypeInference getRowTypeInference() { + return this::inferRowType; } - /** Thrown when a non-literal occurs in an argument to a user-defined - * table macro. */ - private static class NonLiteralException extends Exception { + private RelDataType inferRowType(SqlOperatorBinding callBinding) { + final RelDataTypeFactory typeFactory = callBinding.getTypeFactory(); + final TranslatableTable table = getTable(callBinding); + return table.getRowType(typeFactory); } } diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index a81f359..0919ee2 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -71,6 +71,7 @@ import org.apache.calcite.sql.SqlSelect; import org.apache.calcite.sql.SqlSelectKeyword; import org.apache.calcite.sql.SqlSnapshot; import org.apache.calcite.sql.SqlSyntax; +import org.apache.calcite.sql.SqlTableFunction; import org.apache.calcite.sql.SqlUnresolvedFunction; import org.apache.calcite.sql.SqlUpdate; import org.apache.calcite.sql.SqlUtil; @@ -3958,9 +3959,26 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { */ private void validateGroupByItem(SqlSelect select, SqlNode groupByItem) { final SqlValidatorScope groupByScope = getGroupScope(select); + validateGroupByExpr(groupByItem, groupByScope); groupByScope.validateExpr(groupByItem); } + private void validateGroupByExpr(SqlNode groupByItem, + SqlValidatorScope groupByScope) { + switch (groupByItem.getKind()) { + case GROUPING_SETS: + case ROLLUP: + case CUBE: + final SqlCall call = (SqlCall) groupByItem; + for (SqlNode operand : call.getOperandList()) { + validateExpr(operand, groupByScope); + } + break; + default: + validateExpr(groupByItem, groupByScope); + } + } + /** * Validates an item in the ORDER BY clause of a SELECT statement. * @@ -4218,6 +4236,9 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { throw newValidationError(expr, RESOURCE.absentOverClause()); } + if (op instanceof SqlTableFunction) { + throw RESOURCE.cannotCallTableFunctionHere(op.getName()).ex(); + } } // Call on the expression to validate itself. diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java index 8b696b5..6483786 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java @@ -2480,8 +2480,7 @@ public class SqlToRelConverter { if (operator instanceof SqlUserDefinedTableMacro) { final SqlUserDefinedTableMacro udf = (SqlUserDefinedTableMacro) operator; - final TranslatableTable table = - udf.getTable(typeFactory, callBinding.operands()); + final TranslatableTable table = udf.getTable(callBinding); final RelDataType rowType = table.getRowType(typeFactory); RelOptTable relOptTable = RelOptTableImpl.create(null, rowType, table, udf.getNameAsId().names); @@ -2493,7 +2492,7 @@ public class SqlToRelConverter { Type elementType; if (operator instanceof SqlUserDefinedTableFunction) { SqlUserDefinedTableFunction udtf = (SqlUserDefinedTableFunction) operator; - elementType = udtf.getElementType(typeFactory, callBinding.operands()); + elementType = udtf.getElementType(callBinding); } else { elementType = null; } diff --git a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties index 9f4b453..3a42da3 100644 --- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties +++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties @@ -185,6 +185,7 @@ DynamicParamIllegal=Illegal use of dynamic parameter InvalidBoolean=''{0}'' is not a valid boolean value ArgumentMustBeValidPrecision=Argument to function ''{0}'' must be a valid precision between ''{1,number,#}'' and ''{2,number,#}'' IllegalArgumentForTableFunctionCall=Wrong arguments for table function ''{0}'' call. Expected ''{1}'', actual ''{2}'' +CannotCallTableFunctionHere=Cannot call table function here: ''{0}'' InvalidDatetimeFormat=''{0}'' is not a valid datetime format InsertIntoAlwaysGenerated=Cannot INSERT into generated column ''{0}'' ArgumentMustHaveScaleZero=Argument to function ''{0}'' must have a scale of 0 diff --git a/core/src/test/java/org/apache/calcite/test/MockSqlOperatorTable.java b/core/src/test/java/org/apache/calcite/test/MockSqlOperatorTable.java index 0da69b8..3cced47 100644 --- a/core/src/test/java/org/apache/calcite/test/MockSqlOperatorTable.java +++ b/core/src/test/java/org/apache/calcite/test/MockSqlOperatorTable.java @@ -18,9 +18,6 @@ package org.apache.calcite.test; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeFactory; -import org.apache.calcite.rel.type.RelDataTypeFieldImpl; -import org.apache.calcite.rel.type.RelRecordType; -import org.apache.calcite.rel.type.StructKind; import org.apache.calcite.sql.SqlAggFunction; import org.apache.calcite.sql.SqlFunction; import org.apache.calcite.sql.SqlFunctionCategory; @@ -29,9 +26,11 @@ import org.apache.calcite.sql.SqlKind; import org.apache.calcite.sql.SqlOperator; import org.apache.calcite.sql.SqlOperatorBinding; import org.apache.calcite.sql.SqlOperatorTable; +import org.apache.calcite.sql.SqlTableFunction; import org.apache.calcite.sql.parser.SqlParserPos; import org.apache.calcite.sql.type.OperandTypes; import org.apache.calcite.sql.type.ReturnTypes; +import org.apache.calcite.sql.type.SqlReturnTypeInference; import org.apache.calcite.sql.type.SqlTypeFamily; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.sql.util.ChainedSqlOperatorTable; @@ -40,7 +39,6 @@ import org.apache.calcite.util.Optionality; import com.google.common.collect.ImmutableList; -import java.util.Arrays; /** * Mock operator table for testing purposes. Contains the standard SQL operator @@ -69,43 +67,81 @@ public class MockSqlOperatorTable extends ChainedSqlOperatorTable { opTab.addOperator(new MyFunction()); opTab.addOperator(new MyAvgAggFunction()); opTab.addOperator(new RowFunction()); + opTab.addOperator(new NotATableFunction()); + opTab.addOperator(new BadTableFunction()); } /** "RAMP" user-defined function. */ - public static class RampFunction extends SqlFunction { + public static class RampFunction extends SqlFunction + implements SqlTableFunction { public RampFunction() { super("RAMP", SqlKind.OTHER_FUNCTION, + ReturnTypes.CURSOR, null, + OperandTypes.NUMERIC, + SqlFunctionCategory.USER_DEFINED_TABLE_FUNCTION); + } + + @Override public SqlReturnTypeInference getRowTypeInference() { + return opBinding -> opBinding.getTypeFactory().builder() + .add("I", SqlTypeName.INTEGER) + .build(); + } + } + + /** Not valid as a table function, even though it returns CURSOR, because + * it does not implement {@link SqlTableFunction}. */ + public static class NotATableFunction extends SqlFunction { + public NotATableFunction() { + super("BAD_RAMP", + SqlKind.OTHER_FUNCTION, + ReturnTypes.CURSOR, null, OperandTypes.NUMERIC, SqlFunctionCategory.USER_DEFINED_FUNCTION); } + } + + /** Another bad table function: declares itself as a table function but does + * not return CURSOR. */ + public static class BadTableFunction extends SqlFunction + implements SqlTableFunction { + public BadTableFunction() { + super("BAD_TABLE_FUNCTION", + SqlKind.OTHER_FUNCTION, + null, + null, + OperandTypes.NUMERIC, + SqlFunctionCategory.USER_DEFINED_TABLE_FUNCTION); + } public RelDataType inferReturnType(SqlOperatorBinding opBinding) { - final RelDataTypeFactory typeFactory = - opBinding.getTypeFactory(); - return typeFactory.builder() + // This is wrong. A table function should return CURSOR. + return opBinding.getTypeFactory().builder() .add("I", SqlTypeName.INTEGER) .build(); } + + @Override public SqlReturnTypeInference getRowTypeInference() { + return this::inferReturnType; + } } /** "DEDUP" user-defined function. */ - public static class DedupFunction extends SqlFunction { + public static class DedupFunction extends SqlFunction + implements SqlTableFunction { public DedupFunction() { super("DEDUP", SqlKind.OTHER_FUNCTION, - null, + ReturnTypes.CURSOR, null, OperandTypes.VARIADIC, - SqlFunctionCategory.USER_DEFINED_FUNCTION); + SqlFunctionCategory.USER_DEFINED_TABLE_FUNCTION); } - public RelDataType inferReturnType(SqlOperatorBinding opBinding) { - final RelDataTypeFactory typeFactory = - opBinding.getTypeFactory(); - return typeFactory.builder() + @Override public SqlReturnTypeInference getRowTypeInference() { + return opBinding -> opBinding.getTypeFactory().builder() .add("NAME", SqlTypeName.VARCHAR, 1024) .build(); } @@ -177,36 +213,25 @@ public class MockSqlOperatorTable extends ChainedSqlOperatorTable { /** "ROW_FUNC" user-defined function whose return type is * nullable row type with non-nullable fields. */ - public static class RowFunction extends SqlFunction { - public RowFunction() { - super("ROW_FUNC", - SqlKind.OTHER_FUNCTION, - null, - null, - OperandTypes.NILADIC, - SqlFunctionCategory.USER_DEFINED_FUNCTION); + public static class RowFunction extends SqlFunction + implements SqlTableFunction { + RowFunction() { + super("ROW_FUNC", SqlKind.OTHER_FUNCTION, ReturnTypes.CURSOR, null, + OperandTypes.NILADIC, SqlFunctionCategory.USER_DEFINED_FUNCTION); } - public RelDataType inferReturnType(SqlOperatorBinding opBinding) { - final RelDataTypeFactory typeFactory = - opBinding.getTypeFactory(); - final RelDataType bigIntNotNull = typeFactory.createSqlType(SqlTypeName.BIGINT); - final RelDataType bigIntNullable = - typeFactory.createTypeWithNullability(bigIntNotNull, true); - return new RelRecordType( - StructKind.FULLY_QUALIFIED, - Arrays.asList( - new RelDataTypeFieldImpl( - "NOT_NULL_FIELD", - 0, - bigIntNotNull), - new RelDataTypeFieldImpl( - "NULLABLE_FIELD", - 0, - bigIntNullable) - ), - true - ); + private static RelDataType inferRowType(SqlOperatorBinding opBinding) { + final RelDataTypeFactory typeFactory = opBinding.getTypeFactory(); + final RelDataType bigintType = + typeFactory.createSqlType(SqlTypeName.BIGINT); + return typeFactory.builder() + .add("NOT_NULL_FIELD", bigintType) + .add("NULLABLE_FIELD", bigintType).nullable(true) + .build(); + } + + @Override public SqlReturnTypeInference getRowTypeInference() { + return RowFunction::inferRowType; } } } diff --git a/core/src/test/java/org/apache/calcite/test/SqlOperatorBindingTest.java b/core/src/test/java/org/apache/calcite/test/SqlOperatorBindingTest.java index cac4e3b..4f8ea4e 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlOperatorBindingTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlOperatorBindingTest.java @@ -21,13 +21,15 @@ import org.apache.calcite.jdbc.JavaTypeFactoryImpl; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeSystem; import org.apache.calcite.rex.RexBuilder; +import org.apache.calcite.rex.RexCallBinding; import org.apache.calcite.rex.RexNode; -import org.apache.calcite.rex.RexProgram; -import org.apache.calcite.rex.RexProgramBuilder; import org.apache.calcite.rex.RexUtil; +import org.apache.calcite.sql.SqlCallBinding; +import org.apache.calcite.sql.SqlCharStringLiteral; import org.apache.calcite.sql.SqlDataTypeSpec; import org.apache.calcite.sql.SqlLiteral; import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.SqlOperatorBinding; import org.apache.calcite.sql.SqlUtil; import org.apache.calcite.sql.fun.SqlStdOperatorTable; import org.apache.calcite.sql.parser.SqlParserPos; @@ -39,11 +41,12 @@ import com.google.common.collect.Lists; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertSame; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; /** - * Unit tests for {@link RexProgram} and - * {@link RexProgramBuilder}. + * Unit tests for {@link SqlOperatorBinding} and its sub-classes + * {@link SqlCallBinding} and {@link RexCallBinding}. */ class SqlOperatorBindingTest { private RexBuilder rexBuilder; @@ -51,7 +54,7 @@ class SqlOperatorBindingTest { private SqlDataTypeSpec integerType; @BeforeEach - public void setUp() { + void setUp() { JavaTypeFactory typeFactory = new JavaTypeFactoryImpl(RelDataTypeSystem.DEFAULT); integerDataType = typeFactory.createSqlType(SqlTypeName.INTEGER); integerType = SqlTypeUtil.convertTypeToSpec(integerDataType); @@ -65,24 +68,52 @@ class SqlOperatorBindingTest { * literal</a>. */ @Test void testSqlNodeLiteral() { - final SqlNode literal = SqlLiteral.createExactNumeric( - "0", - SqlParserPos.ZERO); - final SqlNode castLiteral = SqlStdOperatorTable.CAST.createCall( - SqlParserPos.ZERO, - literal, - integerType); - final SqlNode castCastLiteral = SqlStdOperatorTable.CAST.createCall( - SqlParserPos.ZERO, - castLiteral, - integerType); + final SqlParserPos pos = SqlParserPos.ZERO; + final SqlNode zeroLiteral = SqlLiteral.createExactNumeric("0", pos); + final SqlNode oneLiteral = SqlLiteral.createExactNumeric("1", pos); + final SqlNode nullLiteral = SqlLiteral.createNull(pos); + final SqlCharStringLiteral aLiteral = SqlLiteral.createCharString("a", pos); - // SqlLiteral is considered as a Literal - assertSame(true, SqlUtil.isLiteral(literal, true)); - // CAST(SqlLiteral as type) is considered as a Literal - assertSame(true, SqlUtil.isLiteral(castLiteral, true)); - // CAST(CAST(SqlLiteral as type) as type) is NOT considered as a Literal - assertSame(false, SqlUtil.isLiteral(castCastLiteral, true)); + final SqlNode castLiteral = + SqlStdOperatorTable.CAST.createCall(pos, zeroLiteral, integerType); + final SqlNode castCastLiteral = + SqlStdOperatorTable.CAST.createCall(pos, castLiteral, integerType); + final SqlNode mapLiteral = + SqlStdOperatorTable.MAP_VALUE_CONSTRUCTOR.createCall(pos, + aLiteral, oneLiteral); + final SqlNode map2Literal = + SqlStdOperatorTable.MAP_VALUE_CONSTRUCTOR.createCall(pos, + aLiteral, castLiteral); + final SqlNode arrayLiteral = + SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR.createCall(pos, + zeroLiteral, oneLiteral); + final SqlNode defaultCall = SqlStdOperatorTable.DEFAULT.createCall(pos); + + // SqlLiteral is considered a literal + assertThat(SqlUtil.isLiteral(zeroLiteral, false), is(true)); + assertThat(SqlUtil.isLiteral(zeroLiteral, true), is(true)); + // NULL literal is considered a literal + assertThat(SqlUtil.isLiteral(nullLiteral, false), is(true)); + assertThat(SqlUtil.isLiteral(nullLiteral, true), is(true)); + // CAST(SqlLiteral as type) is considered a literal, iff allowCast + assertThat(SqlUtil.isLiteral(castLiteral, false), is(false)); + assertThat(SqlUtil.isLiteral(castLiteral, true), is(true)); + // CAST(CAST(SqlLiteral as type) as type) is considered a literal, + // iff allowCast + assertThat(SqlUtil.isLiteral(castCastLiteral, false), is(false)); + assertThat(SqlUtil.isLiteral(castCastLiteral, true), is(true)); + // MAP['a', 1] and MAP['a', CAST(0 AS INTEGER)] are considered literals, + // iff allowCast + assertThat(SqlUtil.isLiteral(mapLiteral, false), is(false)); + assertThat(SqlUtil.isLiteral(mapLiteral, true), is(true)); + assertThat(SqlUtil.isLiteral(map2Literal, false), is(false)); + assertThat(SqlUtil.isLiteral(map2Literal, true), is(true)); + // ARRAY[0, 1] is considered a literal, iff allowCast + assertThat(SqlUtil.isLiteral(arrayLiteral, false), is(false)); + assertThat(SqlUtil.isLiteral(arrayLiteral, true), is(true)); + // DEFAULT is considered a literal, iff allowCast + assertThat(SqlUtil.isLiteral(defaultCall, false), is(false)); + assertThat(SqlUtil.isLiteral(defaultCall, true), is(true)); } /** Tests {@link org.apache.calcite.rex.RexUtil#isLiteral(RexNode, boolean)}, @@ -105,11 +136,11 @@ class SqlOperatorBindingTest { SqlStdOperatorTable.CAST, Lists.newArrayList(castLiteral)); - // RexLiteral is considered as a Literal - assertSame(true, RexUtil.isLiteral(literal, true)); - // CAST(RexLiteral as type) is considered as a Literal - assertSame(true, RexUtil.isLiteral(castLiteral, true)); - // CAST(CAST(RexLiteral as type) as type) is NOT considered as a Literal - assertSame(false, RexUtil.isLiteral(castCastLiteral, true)); + // RexLiteral is considered a literal + assertThat(RexUtil.isLiteral(literal, true), is(true)); + // CAST(RexLiteral as type) is considered a literal + assertThat(RexUtil.isLiteral(castLiteral, true), is(true)); + // CAST(CAST(RexLiteral as type) as type) is NOT considered a literal + assertThat(RexUtil.isLiteral(castCastLiteral, true), is(false)); } } diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java index 12cdca6..32cc678 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -8078,8 +8078,45 @@ public class SqlValidatorTest extends SqlValidatorTestCase { sql("select * from table(ramp('3'))") .type("RecordType(INTEGER NOT NULL I) NOT NULL"); + sql("select * from table(abs(-1))") + .fails("(?s)Encountered \"abs\" at .*"); + + sql("select * from table(1 + 2)") + .fails("(?s)Encountered \"1\" at .*"); + sql("select * from table(^nonExistentRamp('3')^)") .fails("No match found for function signature NONEXISTENTRAMP\\(<CHARACTER>\\)"); + + sql("select * from table(^bad_ramp(3)^)") + .fails("Argument must be a table function: BAD_RAMP"); + + sql("select * from table(^bad_table_function(3)^)") + .fails("Table function should have CURSOR type, not" + + " RecordType\\(INTEGER I\\)"); + } + + /** Tests that Calcite gives an error if a table function is used anywhere + * that a scalar expression is expected. */ + @Test void testTableFunctionAsExpression() { + sql("select ^ramp(3)^ from (values (1))") + .fails("Cannot call table function here: 'RAMP'"); + sql("select * from (values (1)) where ^ramp(3)^") + .fails("WHERE clause must be a condition"); + sql("select * from (values (1)) where ^ramp(3) and 1 = 1^") + .fails("Cannot apply 'AND' to arguments of type '<CURSOR> AND " + + "<BOOLEAN>'\\. Supported form\\(s\\): '<BOOLEAN> AND <BOOLEAN>'"); + sql("select * from (values (1)) where ^ramp(3) is not null^") + .fails("Cannot apply 'IS NOT NULL' to arguments of type '<CURSOR> IS" + + " NOT NULL'\\. Supported form\\(s\\): '<ANY> IS NOT NULL'"); + sql("select ^sum(ramp(3))^ from (values (1))") + .fails("Cannot apply 'SUM' to arguments of type 'SUM\\(<CURSOR>\\)'\\. " + + "Supported form\\(s\\): 'SUM\\(<NUMERIC>\\)'"); + sql("select * from (values (1)) group by ^ramp(3)^") + .fails("Cannot call table function here: 'RAMP'"); + sql("select count(*) from (values (1)) having ^ramp(3)^") + .fails("HAVING clause must be a condition"); + sql("select * from (values (1)) order by ^ramp(3)^ asc, 1 desc") + .fails("Cannot call table function here: 'RAMP'"); } /** Test case for @@ -11550,6 +11587,6 @@ public class SqlValidatorTest extends SqlValidatorTestCase { MockSqlOperatorTable.addRamp(operatorTable); sql("select * FROM TABLE(ROW_FUNC()) AS T(a, b)") .withOperatorTable(operatorTable) - .type("RecordType(BIGINT A, BIGINT B) NOT NULL"); + .type("RecordType(BIGINT NOT NULL A, BIGINT B) NOT NULL"); } } diff --git a/core/src/test/java/org/apache/calcite/test/TableFunctionTest.java b/core/src/test/java/org/apache/calcite/test/TableFunctionTest.java index 3508dd6..f0f0148 100644 --- a/core/src/test/java/org/apache/calcite/test/TableFunctionTest.java +++ b/core/src/test/java/org/apache/calcite/test/TableFunctionTest.java @@ -145,8 +145,7 @@ class TableFunctionTest { * Tests a table function that implements {@link ScannableTable} and returns * a single column. */ - @Test void testScannableTableFunction() - throws SQLException, ClassNotFoundException { + @Test void testScannableTableFunction() throws SQLException { Connection connection = DriverManager.getConnection("jdbc:calcite:"); CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class); @@ -165,7 +164,7 @@ class TableFunctionTest { /** As {@link #testScannableTableFunction()} but with named parameters. */ @Test void testScannableTableFunctionWithNamedParameters() - throws SQLException, ClassNotFoundException { + throws SQLException { Connection connection = DriverManager.getConnection("jdbc:calcite:"); CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class); @@ -198,7 +197,7 @@ class TableFunctionTest { /** As {@link #testScannableTableFunction()} but with named parameters. */ @Test void testMultipleScannableTableFunctionWithNamedParameters() - throws SQLException, ClassNotFoundException { + throws SQLException { try (Connection connection = DriverManager.getConnection("jdbc:calcite:"); Statement statement = connection.createStatement()) { CalciteConnection calciteConnection = @@ -243,8 +242,7 @@ class TableFunctionTest { * Tests a table function that returns different row type based on * actual call arguments. */ - @Test void testTableFunctionDynamicStructure() - throws SQLException, ClassNotFoundException { + @Test void testTableFunctionDynamicStructure() throws SQLException { Connection connection = getConnectionWithMultiplyFunction(); final PreparedStatement ps = connection.prepareStatement("select *\n" + "from table(\"s\".\"multiplication\"(4, 3, ?))\n"); @@ -262,7 +260,7 @@ class TableFunctionTest { */ @Disabled("SQLException does not include message from nested exception") @Test void testTableFunctionNonNullableMustBeLiterals() - throws SQLException, ClassNotFoundException { + throws SQLException { Connection connection = getConnectionWithMultiplyFunction(); try { final PreparedStatement ps = connection.prepareStatement("select *\n" @@ -299,8 +297,7 @@ class TableFunctionTest { */ @Disabled("CannotPlanException: Node [rel#18:Subset#4.ENUMERABLE.[]] " + "could not be implemented") - @Test void testTableFunctionCursorInputs() - throws SQLException, ClassNotFoundException { + @Test void testTableFunctionCursorInputs() throws SQLException { try (Connection connection = DriverManager.getConnection("jdbc:calcite:")) { CalciteConnection calciteConnection = @@ -334,8 +331,7 @@ class TableFunctionTest { */ @Disabled("CannotPlanException: Node [rel#24:Subset#6.ENUMERABLE.[]] " + "could not be implemented") - @Test void testTableFunctionCursorsInputs() - throws SQLException, ClassNotFoundException { + @Test void testTableFunctionCursorsInputs() throws SQLException { try (Connection connection = getConnectionWithMultiplyFunction()) { CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class); @@ -369,6 +365,9 @@ class TableFunctionTest { } } + /** Tests a query with a table function in the FROM clause. + * + * @see Smalls#multiplicationTable */ @Test void testUserDefinedTableFunction() { final String q = "select *\n" + "from table(\"s\".\"multiplication\"(2, 3, 100))\n"; @@ -379,6 +378,11 @@ class TableFunctionTest { "row_name=row 2; c1=103; c2=106"); } + /** Tests a query with a table function in the FROM clause, + * attempting to reference a column from the table function in the WHERE + * clause but getting the case wrong. + * + * @see Smalls#multiplicationTable */ @Test void testUserDefinedTableFunction2() { final String q = "select c1\n" + "from table(\"s\".\"multiplication\"(2, 3, 100))\n" @@ -387,6 +391,10 @@ class TableFunctionTest { .throws_("Column 'C1' not found in any table; did you mean 'c1'?"); } + /** Tests a query with a table function in the FROM clause, + * referencing columns in the WHERE clause. + * + * @see Smalls#multiplicationTable */ @Test void testUserDefinedTableFunction3() { final String q = "select \"c1\"\n" + "from table(\"s\".\"multiplication\"(2, 3, 100))\n" @@ -394,6 +402,8 @@ class TableFunctionTest { with().query(q).returnsUnordered("c1=103"); } + /** As {@link #testUserDefinedTableFunction3()}, but provides a character + * literal argument for an integer parameter. */ @Test void testUserDefinedTableFunction4() { final String q = "select \"c1\"\n" + "from table(\"s\".\"multiplication\"('2', 3, 100))\n" diff --git a/example/function/src/test/java/org/apache/calcite/test/ExampleFunctionTest.java b/example/function/src/test/java/org/apache/calcite/test/ExampleFunctionTest.java index ac88123..f4cc0ea 100644 --- a/example/function/src/test/java/org/apache/calcite/test/ExampleFunctionTest.java +++ b/example/function/src/test/java/org/apache/calcite/test/ExampleFunctionTest.java @@ -75,7 +75,7 @@ class ExampleFunctionTest { } public void checkMazeTableFunction(Boolean solution, String maze) - throws SQLException, ClassNotFoundException { + throws SQLException { Connection connection = DriverManager.getConnection("jdbc:calcite:"); CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class);
