/* * Licensed 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 com.facebook.presto.operator.scalar; import com.facebook.presto.metadata.FunctionListBuilder; import com.facebook.presto.spi.ConnectorSession; import com.facebook.presto.spi.function.IsNull; import com.facebook.presto.spi.function.ScalarFunction; import com.facebook.presto.spi.function.SqlNullable; import com.facebook.presto.spi.function.SqlType; import com.facebook.presto.spi.function.TypeParameter; import com.facebook.presto.spi.type.StandardTypes; import com.facebook.presto.spi.type.Type; import org.testng.annotations.Test; import javax.annotation.Nullable; @SuppressWarnings("UtilityClassWithoutPrivateConstructor") public class TestScalarValidation { @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Parametric class method .* is annotated with @ScalarFunction") public void testBogusParametricMethodAnnotation() { extractParametricScalar(BogusParametricMethodAnnotation.class); } @ScalarFunction public static final class BogusParametricMethodAnnotation { @ScalarFunction public static void bad() {} } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Parametric class .* does not have any annotated methods") public void testNoParametricMethods() { extractParametricScalar(NoParametricMethods.class); } @SuppressWarnings("EmptyClass") @ScalarFunction public static final class NoParametricMethods {} @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* is missing @SqlType annotation") public void testMethodMissingReturnAnnotation() { extractScalars(MethodMissingReturnAnnotation.class); } public static final class MethodMissingReturnAnnotation { @ScalarFunction public static void bad() {} } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* annotated with @SqlType is missing @ScalarFunction or @ScalarOperator") public void testMethodMissingScalarAnnotation() { extractScalars(MethodMissingScalarAnnotation.class); } public static final class MethodMissingScalarAnnotation { @SuppressWarnings("unused") @SqlType public static void bad() {} } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has wrapper return type Long but is missing @SqlNullable") public void testPrimitiveWrapperReturnWithoutNullable() { extractScalars(PrimitiveWrapperReturnWithoutNullable.class); } public static final class PrimitiveWrapperReturnWithoutNullable { @ScalarFunction @SqlType(StandardTypes.BIGINT) public static Long bad() { return 0L; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* annotated with @SqlNullable has primitive return type long") public void testPrimitiveReturnWithNullable() { extractScalars(PrimitiveReturnWithNullable.class); } public static final class PrimitiveReturnWithNullable { @ScalarFunction @SqlNullable @SqlType(StandardTypes.BIGINT) public static long bad() { return 0L; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has parameter with wrapper type Boolean that is missing @SqlNullable") public void testPrimitiveWrapperParameterWithoutNullable() { extractScalars(PrimitiveWrapperParameterWithoutNullable.class); } public static final class PrimitiveWrapperParameterWithoutNullable { @ScalarFunction @SqlType(StandardTypes.BIGINT) public static long bad(@SqlType(StandardTypes.BOOLEAN) Boolean boxed) { return 0; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has parameter with primitive type double annotated with @SqlNullable") public void testPrimitiveParameterWithNullable() { extractScalars(PrimitiveParameterWithNullable.class); } public static final class PrimitiveParameterWithNullable { @ScalarFunction @SqlType(StandardTypes.BIGINT) public static long bad(@SqlNullable @SqlType(StandardTypes.DOUBLE) double primitive) { return 0; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* is missing @SqlType annotation for parameter") public void testParameterWithoutType() { extractScalars(ParameterWithoutType.class); } public static final class ParameterWithoutType { @ScalarFunction @SqlType(StandardTypes.BIGINT) public static long bad(long missing) { return 0; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* annotated with @ScalarFunction must be public") public void testNonPublicAnnnotatedMethod() { extractScalars(NonPublicAnnnotatedMethod.class); } public static final class NonPublicAnnnotatedMethod { @ScalarFunction @SqlType(StandardTypes.BIGINT) private static long bad() { return 0; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* is annotated with @Nullable but not @SqlNullable") public void testMethodWithLegacyNullable() { extractScalars(MethodWithLegacyNullable.class); } public static final class MethodWithLegacyNullable { @ScalarFunction @Nullable @SqlType(StandardTypes.BIGINT) public static Long bad() { return 0L; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has parameter annotated with @Nullable but not @SqlNullable") public void testParameterWithLegacyNullable() { extractScalars(ParameterWithLegacyNullable.class); } public static final class ParameterWithLegacyNullable { @ScalarFunction @SqlType(StandardTypes.BIGINT) public static long bad(@Nullable @SqlType(StandardTypes.DOUBLE) Double value) { return 0; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has @IsNull parameter that does not follow a @SqlType parameter") public void testParameterWithConnectorAndIsNull() { extractScalars(ParameterWithConnectorAndIsNull.class); } public static final class ParameterWithConnectorAndIsNull { @ScalarFunction @SqlType(StandardTypes.BIGINT) public static long bad(ConnectorSession session, @IsNull boolean isNull) { return 0; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has @IsNull parameter that does not follow a @SqlType parameter") public void testParameterWithOnlyIsNull() { extractScalars(ParameterWithOnlyIsNull.class); } public static final class ParameterWithOnlyIsNull { @ScalarFunction @SqlType(StandardTypes.BIGINT) public static long bad(@IsNull boolean isNull) { return 0; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has non-boolean parameter with @IsNull") public void testParameterWithNonBooleanIsNull() { extractScalars(ParameterWithNonBooleanIsNull.class); } public static final class ParameterWithNonBooleanIsNull { @ScalarFunction @SqlType(StandardTypes.BIGINT) public static long bad(@SqlType(StandardTypes.BIGINT) long value, @IsNull int isNull) { return 0; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* uses @IsNull following a parameter with boxed primitive type: Long") public void testParameterWithBoxedPrimitiveIsNull() { extractScalars(ParameterWithBoxedPrimitiveIsNull.class); } public static final class ParameterWithBoxedPrimitiveIsNull { @ScalarFunction @SqlType(StandardTypes.BIGINT) public static long bad(@SqlNullable @SqlType(StandardTypes.BIGINT) Long value, @IsNull boolean isNull) { return 0; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Method .* has @IsNull parameter that has other annotations") public void testParameterWithOtherAnnotationsWithIsNull() { extractScalars(ParameterWithOtherAnnotationsWithIsNull.class); } public static final class ParameterWithOtherAnnotationsWithIsNull { @ScalarFunction @SqlType(StandardTypes.BIGINT) public static long bad(@SqlType(StandardTypes.BIGINT) long value, @IsNull @SqlNullable boolean isNull) { return 0; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Expected type parameter to only contain A-Z and 0-9 \\(starting with A-Z\\), but got bad on method .*") public void testNonUpperCaseTypeParameters() { extractScalars(TypeParameterWithNonUpperCaseAnnotation.class); } public static final class TypeParameterWithNonUpperCaseAnnotation { @ScalarFunction @SqlType(StandardTypes.BIGINT) @TypeParameter("bad") public static long bad(@TypeParameter("array(bad)") Type type, @SqlType(StandardTypes.BIGINT) long value) { return value; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Expected type parameter to only contain A-Z and 0-9 \\(starting with A-Z\\), but got 1E on method .*") public void testLeadingNumericTypeParameters() { extractScalars(TypeParameterWithLeadingNumbers.class); } public static final class TypeParameterWithLeadingNumbers { @ScalarFunction @SqlType(StandardTypes.BIGINT) @TypeParameter("1E") public static long bad(@TypeParameter("array(1E)") Type type, @SqlType(StandardTypes.BIGINT) long value) { return value; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Expected type parameter not to take parameters, but got E on method .*") public void testNonPrimitiveTypeParameters() { extractScalars(TypeParameterWithNonPrimitiveAnnotation.class); } public static final class TypeParameterWithNonPrimitiveAnnotation { @ScalarFunction @SqlType(StandardTypes.BIGINT) @TypeParameter("E") public static long bad(@TypeParameter("E(VARCHAR)") Type type, @SqlType(StandardTypes.BIGINT) long value) { return value; } } @Test public void testValidTypeParameters() { extractScalars(ValidTypeParameter.class); } public static final class ValidTypeParameter { @ScalarFunction @SqlType(StandardTypes.BIGINT) public static long good1( @TypeParameter("ROW(ARRAY(BIGINT),MAP(INTEGER,DECIMAL),SMALLINT,CHAR,BOOLEAN,DATE,TIMESTAMP,VARCHAR)") Type type, @SqlType(StandardTypes.BIGINT) long value) { return value; } @ScalarFunction @SqlType(StandardTypes.BIGINT) @TypeParameter("E12") @TypeParameter("F34") public static long good2( @TypeParameter("ROW(ARRAY(E12),JSON,TIME,VARBINARY,ROW(ROW(F34)))") Type type, @SqlType(StandardTypes.BIGINT) long value) { return value; } } @Test public void testValidTypeParametersForConstructors() { extractScalars(ConstructorWithValidTypeParameters.class); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Expected type parameter not to take parameters, but got K on method .*") public void testInvalidTypeParametersForConstructors() { extractScalars(ConstructorWithInvalidTypeParameters.class); } private static void extractParametricScalar(Class<?> clazz) { new FunctionListBuilder().scalar(clazz); } private static void extractScalars(Class<?> clazz) { new FunctionListBuilder().scalars(clazz); } }