From 1937bed9035e953b4cb9099ddeb581d3bf38bca3 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Fri, 15 May 2015 19:34:32 +0200 Subject: [PATCH] Better support of null for UDF patch by Ronbert Stupp; reviewed by Benjamin Lerer for CASSANDRA-8374 --- CHANGES.txt | 3 +- doc/cql3/CQL.textile | 26 +- pylib/cqlshlib/cql3handling.py | 1 + src/java/org/apache/cassandra/cql3/Cql.g | 9 +- .../cql3/functions/JavaSourceUDFFactory.java | 14 +- .../cql3/functions/NativeScalarFunction.java | 5 + .../cql3/functions/ScalarFunction.java | 2 + .../cql3/functions/ScriptBasedUDF.java | 5 +- .../cassandra/cql3/functions/UDAggregate.java | 20 +- .../cassandra/cql3/functions/UDFunction.java | 68 ++- .../cassandra/cql3/functions/UDHelper.java | 39 +- .../statements/CreateAggregateStatement.java | 5 +- .../statements/CreateFunctionStatement.java | 8 +- .../cassandra/schema/LegacySchemaTables.java | 7 +- .../cql3/functions/JavaSourceUDF.txt | 6 +- .../cassandra/cql3/AggregationTest.java | 196 ++++++- .../org/apache/cassandra/cql3/JsonTest.java | 4 +- .../apache/cassandra/cql3/PgStringTest.java | 4 +- .../org/apache/cassandra/cql3/UFAuthTest.java | 6 + .../cassandra/cql3/UFIdentificationTest.java | 1 + .../org/apache/cassandra/cql3/UFTest.java | 527 +++++++++++++++--- 21 files changed, 821 insertions(+), 135 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 08e10057df..c5c505a75b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,6 @@ 2.2.0-beta1 - * Use ecj instead of javassist for UDFs + * Better support of null for UDF (CASSANDRA-8374) + * Use ecj instead of javassist for UDFs (CASSANDRA-8241) * faster async logback configuration for tests (CASSANDRA-9376) * Add `smallint` and `tinyint` data types (CASSANDRA-8951) * Avoid thrift schema creation when native driver is used in stress tool (CASSANDRA-9374) diff --git a/doc/cql3/CQL.textile b/doc/cql3/CQL.textile index 89bd1b9f3f..2e5861591a 100644 --- a/doc/cql3/CQL.textile +++ b/doc/cql3/CQL.textile @@ -591,8 +591,9 @@ __Syntax:__ bc(syntax).. ::= CREATE ( OR REPLACE )? FUNCTION ( IF NOT EXISTS )? - ( ( '.' )? )? + ( '.' )? '(' ( ',' )* ')' + ( CALLED | RETURNS NULL ) ON NULL INPUT RETURNS LANGUAGE AS @@ -602,6 +603,7 @@ __Sample:__ bc(sample). CREATE OR REPLACE FUNCTION somefunction ( somearg int, anotherarg text, complexarg frozen, listarg list ) + RETURNS NULL ON NULL INPUT RETURNS text LANGUAGE java AS $$ @@ -609,6 +611,7 @@ CREATE OR REPLACE FUNCTION somefunction $$; CREATE FUNCTION akeyspace.fname IF NOT EXISTS ( someArg int ) + CALLED ON NULL INPUT RETURNS text LANGUAGE java AS $$ @@ -628,6 +631,11 @@ Note that keyspace names, function names and argument types are subject to the d @CREATE FUNCTION@ with the optional @OR REPLACE@ keywords either creates a function or replaces an existing one with the same signature. A @CREATE FUNCTION@ without @OR REPLACE@ fails if a function with the same signature already exists. +Behavior on invocation with @null@ values must be defined for each function. There are two options: + +# @RETURNS NULL ON NULL INPUT@ declares that the function will always return @null@ if any of the input arguments is @null@. +# @CALLED ON NULL INPUT@ declares that the function will always be executed. + If the optional @IF NOT EXISTS@ keywords are used, the function will only be created if another function with the same signature does not exist. @OR REPLACE@ and @IF NOT EXIST@ cannot be used together. @@ -642,7 +650,7 @@ __Syntax:__ bc(syntax).. ::= DROP FUNCTION ( IF EXISTS )? - ( ( '.' )? )? + ( '.' )? ( '(' ( ',' )* ')' )? p. @@ -666,7 +674,7 @@ __Syntax:__ bc(syntax).. ::= CREATE ( OR REPLACE )? AGGREGATE ( IF NOT EXISTS )? - ( ( '.' )? )? + ( '.' )? '(' ( ',' )* ')' SFUNC ( '.' )? STYPE @@ -698,11 +706,11 @@ Signatures for user-defined aggregates follow the "same rules":#functionSignatur @STYPE@ defines the type of the state value and must be specified. -The optional @INITCOND@ defines the initial state value for the aggregate. It defaults to @null@. +The optional @INITCOND@ defines the initial state value for the aggregate. It defaults to @null@. A non-@null@ @INITCOND@ must be specified for state functions that are declared with @RETURNS NULL ON NULL INPUT@. -@SFUNC@ references an existing function to be used as the state modifying function. The type of first argument of the state function must match @STYPE@. The remaining argument types of the state function must match the argument types of the aggregate function. +@SFUNC@ references an existing function to be used as the state modifying function. The type of first argument of the state function must match @STYPE@. The remaining argument types of the state function must match the argument types of the aggregate function. State is not updated for state functions declared with @RETURNS NULL ON NULL INPUT@ and called with @null@. -The optional @FINALFUNC@ is called just before the aggregate result is returned. It must take only one argument with type @STYPE@. The return type of the @FINALFUNC@ may be a different type. +The optional @FINALFUNC@ is called just before the aggregate result is returned. It must take only one argument with type @STYPE@. The return type of the @FINALFUNC@ may be a different type. A final function declared with @RETURNS NULL ON NULL INPUT@ means that the aggregate's return value will be @null@, if the last state is @null@. If no @FINALFUNC@ is defined, the overall return type of the aggregate function is @STYPE@. If a @FINALFUNC@ is defined, it is the return type of that function. @@ -713,8 +721,8 @@ h3(#dropAggregateStmt). DROP AGGREGATE __Syntax:__ bc(syntax).. - ::= DROP AGGREGATE ( IF EXISTS )? - ( ( '.' )? )? + ::= DROP AGGREGATE ( IF EXISTS )? + ( '.' )? ( '(' ( ',' )* ')' )? p. @@ -1447,6 +1455,7 @@ A complete working example for user-defined aggregates (assuming that a keyspace bc(sample).. CREATE FUNCTION averageState ( state tuple, val int ) + CALLED ON NULL INPUT RETURNS tuple LANGUAGE java AS ' @@ -1458,6 +1467,7 @@ CREATE FUNCTION averageState ( state tuple, val int ) '; CREATE FUNCTION averageFinal ( state tuple ) + CALLED ON NULL INPUT RETURNS double LANGUAGE java AS ' diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py index add4a7fae3..3e155d0596 100644 --- a/pylib/cqlshlib/cql3handling.py +++ b/pylib/cqlshlib/cql3handling.py @@ -1027,6 +1027,7 @@ syntax_rules += r''' ( "(" ( newcol= ( "," [newcolname]= )* )? ")" )? + ("RETURNS" "NULL" | "CALLED") "ON" "NULL" "INPUT" "RETURNS" "LANGUAGE" "AS" ; diff --git a/src/java/org/apache/cassandra/cql3/Cql.g b/src/java/org/apache/cassandra/cql3/Cql.g index 831b0120c8..3600cd1e4b 100644 --- a/src/java/org/apache/cassandra/cql3/Cql.g +++ b/src/java/org/apache/cassandra/cql3/Cql.g @@ -594,6 +594,7 @@ createFunctionStatement returns [CreateFunctionStatement expr] List argsNames = new ArrayList<>(); List argsTypes = new ArrayList<>(); + boolean calledOnNullInput = false; } : K_CREATE (K_OR K_REPLACE { orReplace = true; })? K_FUNCTION @@ -605,10 +606,12 @@ createFunctionStatement returns [CreateFunctionStatement expr] ( ',' k=ident v=comparatorType { argsNames.add(k); argsTypes.add(v); } )* )? ')' + ( (K_RETURNS K_NULL) | (K_CALLED { calledOnNullInput=true; })) K_ON K_NULL K_INPUT K_RETURNS rt = comparatorType K_LANGUAGE language = IDENT K_AS body = STRING_LITERAL - { $expr = new CreateFunctionStatement(fn, $language.text.toLowerCase(), $body.text, argsNames, argsTypes, rt, orReplace, ifNotExists); } + { $expr = new CreateFunctionStatement(fn, $language.text.toLowerCase(), $body.text, + argsNames, argsTypes, rt, calledOnNullInput, orReplace, ifNotExists); } ; dropFunctionStatement returns [DropFunctionStatement expr] @@ -1550,6 +1553,8 @@ basic_unreserved_keyword returns [String str] | K_RETURNS | K_LANGUAGE | K_JSON + | K_CALLED + | K_INPUT ) { $str = $k.text; } ; @@ -1678,6 +1683,8 @@ K_STYPE: S T Y P E; K_FINALFUNC: F I N A L F U N C; K_INITCOND: I N I T C O N D; K_RETURNS: R E T U R N S; +K_CALLED: C A L L E D; +K_INPUT: I N P U T; K_LANGUAGE: L A N G U A G E; K_OR: O R; K_REPLACE: R E P L A C E; diff --git a/src/java/org/apache/cassandra/cql3/functions/JavaSourceUDFFactory.java b/src/java/org/apache/cassandra/cql3/functions/JavaSourceUDFFactory.java index 3809bf3025..97a08b1daa 100644 --- a/src/java/org/apache/cassandra/cql3/functions/JavaSourceUDFFactory.java +++ b/src/java/org/apache/cassandra/cql3/functions/JavaSourceUDFFactory.java @@ -118,6 +118,7 @@ public final class JavaSourceUDFFactory List argNames, List> argTypes, AbstractType returnType, + boolean calledOnNullInput, String body) throws InvalidRequestException { @@ -126,7 +127,7 @@ public final class JavaSourceUDFFactory // returnDataType is just the C* internal returnType converted to the Java Driver DataType DataType returnDataType = UDHelper.driverType(returnType); // javaParamTypes is just the Java representation for argTypes resp. argDataTypes - Class[] javaParamTypes = UDHelper.javaTypes(argDataTypes); + Class[] javaParamTypes = UDHelper.javaTypes(argDataTypes, calledOnNullInput); // javaReturnType is just the Java representation for returnType resp. returnDataType Class javaReturnType = returnDataType.asJavaClass(); @@ -226,11 +227,11 @@ public final class JavaSourceUDFFactory MethodType methodType = MethodType.methodType(void.class) .appendParameterTypes(FunctionName.class, List.class, List.class, DataType[].class, AbstractType.class, DataType.class, - String.class); + boolean.class, String.class); MethodHandle ctor = MethodHandles.lookup().findConstructor(cls, methodType); return (UDFunction) ctor.invokeWithArguments(name, argNames, argTypes, argDataTypes, returnType, returnDataType, - body); + calledOnNullInput, body); } catch (InvocationTargetException e) { @@ -309,11 +310,16 @@ public final class JavaSourceUDFFactory // cast to Java type .append(" (").append(javaSourceName(paramTypes[i])).append(") ") // generate object representation of input parameter (call UDFunction.compose) - .append("compose(protocolVersion, ").append(i).append(", params.get(").append(i).append("))"); + .append(composeMethod(paramTypes[i])).append("(protocolVersion, ").append(i).append(", params.get(").append(i).append("))"); } return code.toString(); } + private static String composeMethod(Class type) + { + return (type.isPrimitive()) ? ("compose_" + type.getName()) : "compose"; + } + // Java source UDFs are a very simple compilation task, which allows us to let one class implement // all interfaces required by ECJ. static final class EcjCompilationUnit implements ICompilationUnit, ICompilerRequestor, INameEnvironment diff --git a/src/java/org/apache/cassandra/cql3/functions/NativeScalarFunction.java b/src/java/org/apache/cassandra/cql3/functions/NativeScalarFunction.java index 8f7f2212f8..3ae0607f7a 100644 --- a/src/java/org/apache/cassandra/cql3/functions/NativeScalarFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/NativeScalarFunction.java @@ -29,6 +29,11 @@ public abstract class NativeScalarFunction extends NativeFunction implements Sca super(name, returnType, argsType); } + public boolean isCalledOnNullInput() + { + return true; + } + public final boolean isAggregate() { return false; diff --git a/src/java/org/apache/cassandra/cql3/functions/ScalarFunction.java b/src/java/org/apache/cassandra/cql3/functions/ScalarFunction.java index f00faf79d3..ba258dfbed 100644 --- a/src/java/org/apache/cassandra/cql3/functions/ScalarFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/ScalarFunction.java @@ -27,6 +27,8 @@ import org.apache.cassandra.exceptions.InvalidRequestException; */ public interface ScalarFunction extends Function { + public boolean isCalledOnNullInput(); + /** * Applies this function to the specified parameter. * diff --git a/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDF.java b/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDF.java index b38f483a51..319c9486cc 100644 --- a/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDF.java +++ b/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDF.java @@ -66,11 +66,12 @@ public class ScriptBasedUDF extends UDFunction List argNames, List> argTypes, AbstractType returnType, + boolean calledOnNullInput, String language, String body) throws InvalidRequestException { - super(name, argNames, argTypes, returnType, language, body); + super(name, argNames, argTypes, returnType, calledOnNullInput, language, body); Compilable scriptEngine = scriptEngines.get(language); if (scriptEngine == null) @@ -88,7 +89,7 @@ public class ScriptBasedUDF extends UDFunction } } - public ByteBuffer execute(int protocolVersion, List parameters) throws InvalidRequestException + public ByteBuffer executeUserDefined(int protocolVersion, List parameters) throws InvalidRequestException { Object[] params = new Object[argTypes.size()]; for (int i = 0; i < params.length; i++) diff --git a/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java b/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java index f5a1af00ed..f153aed95e 100644 --- a/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java +++ b/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java @@ -149,17 +149,27 @@ public class UDAggregate extends AbstractFunction implements AggregateFunction public void addInput(int protocolVersion, List values) throws InvalidRequestException { - List copy = new ArrayList<>(values.size() + 1); - copy.add(state); - copy.addAll(values); - state = stateFunction.execute(protocolVersion, copy); + List fArgs = new ArrayList<>(values.size() + 1); + fArgs.add(state); + fArgs.addAll(values); + if (stateFunction instanceof UDFunction) + { + UDFunction udf = (UDFunction)stateFunction; + if (udf.isCallableWrtNullable(fArgs)) + state = udf.executeUserDefined(protocolVersion, fArgs); + } + else + { + state = stateFunction.execute(protocolVersion, fArgs); + } } public ByteBuffer compute(int protocolVersion) throws InvalidRequestException { if (finalFunction == null) return state; - return finalFunction.execute(protocolVersion, Collections.singletonList(state)); + List fArgs = Collections.singletonList(state); + return finalFunction.execute(protocolVersion, fArgs); } public void reset() diff --git a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java index a56af6e74c..873a1f29b4 100644 --- a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java @@ -49,16 +49,18 @@ public abstract class UDFunction extends AbstractFunction implements ScalarFunct protected final DataType[] argDataTypes; protected final DataType returnDataType; + protected final boolean calledOnNullInput; protected UDFunction(FunctionName name, List argNames, List> argTypes, AbstractType returnType, + boolean calledOnNullInput, String language, String body) { this(name, argNames, argTypes, UDHelper.driverTypes(argTypes), returnType, - UDHelper.driverType(returnType), language, body); + UDHelper.driverType(returnType), calledOnNullInput, language, body); } protected UDFunction(FunctionName name, @@ -67,6 +69,7 @@ public abstract class UDFunction extends AbstractFunction implements ScalarFunct DataType[] argDataTypes, AbstractType returnType, DataType returnDataType, + boolean calledOnNullInput, String language, String body) { @@ -77,20 +80,22 @@ public abstract class UDFunction extends AbstractFunction implements ScalarFunct this.body = body; this.argDataTypes = argDataTypes; this.returnDataType = returnDataType; + this.calledOnNullInput = calledOnNullInput; } public static UDFunction create(FunctionName name, List argNames, List> argTypes, AbstractType returnType, + boolean calledOnNullInput, String language, String body) throws InvalidRequestException { switch (language) { - case "java": return JavaSourceUDFFactory.buildUDF(name, argNames, argTypes, returnType, body); - default: return new ScriptBasedUDF(name, argNames, argTypes, returnType, language, body); + case "java": return JavaSourceUDFFactory.buildUDF(name, argNames, argTypes, returnType, calledOnNullInput, body); + default: return new ScriptBasedUDF(name, argNames, argTypes, returnType, calledOnNullInput, language, body); } } @@ -107,13 +112,14 @@ public abstract class UDFunction extends AbstractFunction implements ScalarFunct List argNames, List> argTypes, AbstractType returnType, + boolean calledOnNullInput, String language, String body, final InvalidRequestException reason) { - return new UDFunction(name, argNames, argTypes, returnType, language, body) + return new UDFunction(name, argNames, argTypes, returnType, calledOnNullInput, language, body) { - public ByteBuffer execute(int protocolVersion, List parameters) throws InvalidRequestException + public ByteBuffer executeUserDefined(int protocolVersion, List parameters) throws InvalidRequestException { throw new InvalidRequestException(String.format("Function '%s' exists but hasn't been loaded successfully " + "for the following reason: %s. Please see the server log for details", @@ -123,6 +129,23 @@ public abstract class UDFunction extends AbstractFunction implements ScalarFunct }; } + public final ByteBuffer execute(int protocolVersion, List parameters) throws InvalidRequestException + { + if (!isCallableWrtNullable(parameters)) + return null; + return executeUserDefined(protocolVersion, parameters); + } + + public boolean isCallableWrtNullable(List parameters) + { + if (!calledOnNullInput) + for (ByteBuffer parameter : parameters) + if (parameter == null || parameter.remaining() == 0) + return false; + return true; + } + + protected abstract ByteBuffer executeUserDefined(int protocolVersion, List parameters) throws InvalidRequestException; public boolean isAggregate() { @@ -134,6 +157,11 @@ public abstract class UDFunction extends AbstractFunction implements ScalarFunct return false; } + public boolean isCalledOnNullInput() + { + return calledOnNullInput; + } + public List argNames() { return argNames; @@ -162,6 +190,36 @@ public abstract class UDFunction extends AbstractFunction implements ScalarFunct return value == null ? null : argDataTypes[argIndex].deserialize(value, ProtocolVersion.fromInt(protocolVersion)); } + // do not remove - used by generated Java UDFs + protected float compose_float(int protocolVersion, int argIndex, ByteBuffer value) + { + return value == null ? 0f : (float)DataType.cfloat().deserialize(value, ProtocolVersion.fromInt(protocolVersion)); + } + + // do not remove - used by generated Java UDFs + protected double compose_double(int protocolVersion, int argIndex, ByteBuffer value) + { + return value == null ? 0d : (double)DataType.cdouble().deserialize(value, ProtocolVersion.fromInt(protocolVersion)); + } + + // do not remove - used by generated Java UDFs + protected int compose_int(int protocolVersion, int argIndex, ByteBuffer value) + { + return value == null ? 0 : (int)DataType.cint().deserialize(value, ProtocolVersion.fromInt(protocolVersion)); + } + + // do not remove - used by generated Java UDFs + protected long compose_long(int protocolVersion, int argIndex, ByteBuffer value) + { + return value == null ? 0L : (long)DataType.bigint().deserialize(value, ProtocolVersion.fromInt(protocolVersion)); + } + + // do not remove - used by generated Java UDFs + protected boolean compose_boolean(int protocolVersion, int argIndex, ByteBuffer value) + { + return value != null && (boolean) DataType.cboolean().deserialize(value, ProtocolVersion.fromInt(protocolVersion)); + } + /** * Used by UDF implementations (both Java code generated by {@link org.apache.cassandra.cql3.functions.JavaSourceUDFFactory} * and script executor {@link org.apache.cassandra.cql3.functions.ScriptBasedUDF}) to convert the Java diff --git a/src/java/org/apache/cassandra/cql3/functions/UDHelper.java b/src/java/org/apache/cassandra/cql3/functions/UDHelper.java index af62c5a573..55a0888c18 100644 --- a/src/java/org/apache/cassandra/cql3/functions/UDHelper.java +++ b/src/java/org/apache/cassandra/cql3/functions/UDHelper.java @@ -20,22 +20,17 @@ package org.apache.cassandra.cql3.functions; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; -import java.util.*; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.List; import com.datastax.driver.core.DataType; -import org.apache.cassandra.cql3.*; -import org.apache.cassandra.db.marshal.*; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.db.marshal.AbstractType; /** * Helper class for User Defined Functions + Aggregates. */ public final class UDHelper { - protected static final Logger logger = LoggerFactory.getLogger(UDHelper.class); - // TODO make these c'tors and methods public in Java-Driver - see https://datastax-oss.atlassian.net/browse/JAVA-502 static final MethodHandle methodParseOne; static @@ -56,14 +51,36 @@ public final class UDHelper /** * Construct an array containing the Java classes for the given Java Driver {@link com.datastax.driver.core.DataType}s. * - * @param dataTypes array with UDF argument types + * @param dataTypes array with UDF argument types + * @param calledOnNullInput whether to allow {@code null} as an argument value * @return array of same size with UDF arguments */ - public static Class[] javaTypes(DataType[] dataTypes) + public static Class[] javaTypes(DataType[] dataTypes, boolean calledOnNullInput) { Class[] paramTypes = new Class[dataTypes.length]; for (int i = 0; i < paramTypes.length; i++) - paramTypes[i] = dataTypes[i].asJavaClass(); + { + Class clazz = dataTypes[i].asJavaClass(); + if (!calledOnNullInput) + { + // only care about classes that can be used in a data type + if (clazz == Integer.class) + clazz = int.class; + else if (clazz == Long.class) + clazz = long.class; + else if (clazz == Byte.class) + clazz = byte.class; + else if (clazz == Short.class) + clazz = short.class; + else if (clazz == Float.class) + clazz = float.class; + else if (clazz == Double.class) + clazz = double.class; + else if (clazz == Boolean.class) + clazz = boolean.class; + } + paramTypes[i] = clazz; + } return paramTypes; } diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateAggregateStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateAggregateStatement.java index 74cc521ce4..2524a7cfa2 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CreateAggregateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/CreateAggregateStatement.java @@ -199,6 +199,9 @@ public final class CreateAggregateStatement extends SchemaAlteringStatement functionName, returnType.asCQL3Type(), old.returnType().asCQL3Type())); } + if (!stateFunction.isCalledOnNullInput() && initcond == null) + throw new InvalidRequestException(String.format("Cannot create aggregate %s without INITCOND because state function %s does not accept 'null' arguments", functionName, stateFunc)); + udAggregate = new UDAggregate(functionName, argTypes, returnType, stateFunction, finalFunction, @@ -220,7 +223,7 @@ public final class CreateAggregateStatement extends SchemaAlteringStatement return sb.toString(); } - private List> stateArguments(AbstractType stateType, List> argTypes) + private static List> stateArguments(AbstractType stateType, List> argTypes) { List> r = new ArrayList<>(argTypes.size() + 1); r.add(stateType); diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateFunctionStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateFunctionStatement.java index faab0431a8..3cef6e4661 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CreateFunctionStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/CreateFunctionStatement.java @@ -49,6 +49,7 @@ public final class CreateFunctionStatement extends SchemaAlteringStatement private final List argNames; private final List argRawTypes; private final CQL3Type.Raw rawReturnType; + private final boolean calledOnNullInput; private List> argTypes; private AbstractType returnType; @@ -61,6 +62,7 @@ public final class CreateFunctionStatement extends SchemaAlteringStatement List argNames, List argRawTypes, CQL3Type.Raw rawReturnType, + boolean calledOnNullInput, boolean orReplace, boolean ifNotExists) { @@ -70,6 +72,7 @@ public final class CreateFunctionStatement extends SchemaAlteringStatement this.argNames = argNames; this.argRawTypes = argRawTypes; this.rawReturnType = rawReturnType; + this.calledOnNullInput = calledOnNullInput; this.orReplace = orReplace; this.ifNotExists = ifNotExists; } @@ -152,13 +155,16 @@ public final class CreateFunctionStatement extends SchemaAlteringStatement throw new InvalidRequestException(String.format("Function %s already exists", old)); if (!(old instanceof ScalarFunction)) throw new InvalidRequestException(String.format("Function %s can only replace a function", old)); + if (calledOnNullInput != ((ScalarFunction) old).isCalledOnNullInput()) + throw new InvalidRequestException(String.format("Function %s can only be replaced with %s", old, + calledOnNullInput ? "CALLED ON NULL INPUT" : "RETURNS NULL ON NULL INPUT")); if (!Functions.typeEquals(old.returnType(), returnType)) throw new InvalidRequestException(String.format("Cannot replace function %s, the new return type %s is not compatible with the return type %s of existing function", functionName, returnType.asCQL3Type(), old.returnType().asCQL3Type())); } - this.udFunction = UDFunction.create(functionName, argNames, argTypes, returnType, language, body); + this.udFunction = UDFunction.create(functionName, argNames, argTypes, returnType, calledOnNullInput, language, body); this.replaced = old != null; MigrationManager.announceNewFunction(udFunction, isLocalOnly); diff --git a/src/java/org/apache/cassandra/schema/LegacySchemaTables.java b/src/java/org/apache/cassandra/schema/LegacySchemaTables.java index 4eb800b85c..720f309158 100644 --- a/src/java/org/apache/cassandra/schema/LegacySchemaTables.java +++ b/src/java/org/apache/cassandra/schema/LegacySchemaTables.java @@ -162,6 +162,7 @@ public class LegacySchemaTables + "body text," + "language text," + "return_type text," + + "called_on_null_input boolean," + "PRIMARY KEY ((keyspace_name), function_name, signature))"); private static final CFMetaData Aggregates = @@ -1285,6 +1286,7 @@ public class LegacySchemaTables adder.add("body", function.body()); adder.add("language", function.language()); adder.add("return_type", function.returnType().toString()); + adder.add("called_on_null_input", function.isCalledOnNullInput()); } public static Mutation makeDropFunctionMutation(KSMetaData keyspace, UDFunction function, long timestamp) @@ -1333,15 +1335,16 @@ public class LegacySchemaTables String language = row.getString("language"); String body = row.getString("body"); + boolean calledOnNullInput = row.getBoolean("called_on_null_input"); try { - return UDFunction.create(name, argNames, argTypes, returnType, language, body); + return UDFunction.create(name, argNames, argTypes, returnType, calledOnNullInput, language, body); } catch (InvalidRequestException e) { logger.error(String.format("Cannot load function '%s' from schema: this function won't be available (on this node)", name), e); - return UDFunction.createBrokenFunction(name, argNames, argTypes, returnType, language, body, e); + return UDFunction.createBrokenFunction(name, argNames, argTypes, returnType, calledOnNullInput, language, body, e); } } diff --git a/src/resources/org/apache/cassandra/cql3/functions/JavaSourceUDF.txt b/src/resources/org/apache/cassandra/cql3/functions/JavaSourceUDF.txt index 985c8ff624..f57b01e54f 100644 --- a/src/resources/org/apache/cassandra/cql3/functions/JavaSourceUDF.txt +++ b/src/resources/org/apache/cassandra/cql3/functions/JavaSourceUDF.txt @@ -13,12 +13,12 @@ import org.apache.cassandra.exceptions.InvalidRequestException; public final class #class_name# extends org.apache.cassandra.cql3.functions.UDFunction { public #class_name#(FunctionName name, List argNames, List> argTypes, - DataType[] argDataTypes, AbstractType returnType, DataType returnDataType, String body) + DataType[] argDataTypes, AbstractType returnType, DataType returnDataType, boolean calledOnNullInput, String body) { - super(name, argNames, argTypes, argDataTypes, returnType, returnDataType, "java", body); + super(name, argNames, argTypes, argDataTypes, returnType, returnDataType, calledOnNullInput, "java", body); } - public ByteBuffer execute(int protocolVersion, List params) throws InvalidRequestException + protected ByteBuffer executeUserDefined(int protocolVersion, List params) throws InvalidRequestException { try { diff --git a/test/unit/org/apache/cassandra/cql3/AggregationTest.java b/test/unit/org/apache/cassandra/cql3/AggregationTest.java index 7fe665df22..7bec8a1497 100644 --- a/test/unit/org/apache/cassandra/cql3/AggregationTest.java +++ b/test/unit/org/apache/cassandra/cql3/AggregationTest.java @@ -106,9 +106,10 @@ public class AggregationTest extends CQLTester String copySign = createFunction(KEYSPACE, "double, double", "CREATE OR REPLACE FUNCTION %s(magnitude double, sign double) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS double " + "LANGUAGE JAVA " + - "AS 'return Double.valueOf(Math.copySign(magnitude.doubleValue(), sign.doubleValue()));';"); + "AS 'return Double.valueOf(Math.copySign(magnitude, sign));';"); assertColumnNames(execute("SELECT max(a), max(unixTimestampOf(b)) FROM %s"), "system.max(a)", "system.max(system.unixtimestampof(b))"); assertRows(execute("SELECT max(a), max(unixTimestampOf(b)) FROM %s"), row(null, null)); @@ -143,6 +144,7 @@ public class AggregationTest extends CQLTester String f = createFunction(KEYSPACE, "double, double", "CREATE OR REPLACE FUNCTION %s(state double, val double) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS double " + "LANGUAGE javascript " + "AS '\"string\";';"); @@ -150,6 +152,7 @@ public class AggregationTest extends CQLTester createFunctionOverload(f, "double, double", "CREATE OR REPLACE FUNCTION %s(state int, val int) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS int " + "LANGUAGE javascript " + "AS '\"string\";';"); @@ -158,7 +161,8 @@ public class AggregationTest extends CQLTester "double", "CREATE OR REPLACE AGGREGATE %s(double) " + "SFUNC " + shortFunctionName(f) + " " + - "STYPE double"); + "STYPE double " + + "INITCOND 0"); assertLastSchemaChange(Event.SchemaChange.Change.CREATED, Event.SchemaChange.Target.AGGREGATE, KEYSPACE, parseFunctionName(a).name, @@ -166,7 +170,8 @@ public class AggregationTest extends CQLTester schemaChange("CREATE OR REPLACE AGGREGATE " + a + "(double) " + "SFUNC " + shortFunctionName(f) + " " + - "STYPE double"); + "STYPE double " + + "INITCOND 0"); assertLastSchemaChange(Event.SchemaChange.Change.UPDATED, Event.SchemaChange.Target.AGGREGATE, KEYSPACE, parseFunctionName(a).name, @@ -176,7 +181,8 @@ public class AggregationTest extends CQLTester "int", "CREATE OR REPLACE AGGREGATE %s(int) " + "SFUNC " + shortFunctionName(f) + " " + - "STYPE int"); + "STYPE int " + + "INITCOND 0"); assertLastSchemaChange(Event.SchemaChange.Change.CREATED, Event.SchemaChange.Target.AGGREGATE, KEYSPACE, parseFunctionName(a).name, @@ -195,6 +201,7 @@ public class AggregationTest extends CQLTester String f = createFunction(KEYSPACE, "double, double", "CREATE OR REPLACE FUNCTION %s(state double, val double) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS double " + "LANGUAGE javascript " + "AS '\"string\";';"); @@ -202,6 +209,7 @@ public class AggregationTest extends CQLTester createFunctionOverload(f, "double, double", "CREATE OR REPLACE FUNCTION %s(state int, val int) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS int " + "LANGUAGE javascript " + "AS '\"string\";';"); @@ -214,12 +222,14 @@ public class AggregationTest extends CQLTester "double", "CREATE OR REPLACE AGGREGATE %s(double) " + "SFUNC " + shortFunctionName(f) + " " + - "STYPE double"); + "STYPE double " + + "INITCOND 0"); createAggregateOverload(a, "int", "CREATE OR REPLACE AGGREGATE %s(int) " + "SFUNC " + shortFunctionName(f) + " " + - "STYPE int"); + "STYPE int " + + "INITCOND 0"); // DROP FUNCTION must not succeed against an aggregate assertInvalidMessage("matches multiple function definitions", "DROP FUNCTION " + a); @@ -243,6 +253,7 @@ public class AggregationTest extends CQLTester String f = createFunction(KEYSPACE, "double, double", "CREATE OR REPLACE FUNCTION %s(state double, val double) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS double " + "LANGUAGE javascript " + "AS '\"string\";';"); @@ -251,7 +262,8 @@ public class AggregationTest extends CQLTester "double", "CREATE OR REPLACE AGGREGATE %s(double) " + "SFUNC " + shortFunctionName(f) + " " + - "STYPE double"); + "STYPE double " + + "INITCOND 0"); // DROP FUNCTION must not succeed because the function is still referenced by the aggregate assertInvalidMessage("still referenced by", "DROP FUNCTION " + f); @@ -270,6 +282,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return Integer.valueOf((a!=null?a.intValue():0) + b.intValue());'"); @@ -277,6 +290,7 @@ public class AggregationTest extends CQLTester String fFinal = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s(a int) " + + "CALLED ON NULL INPUT " + "RETURNS text " + "LANGUAGE java " + "AS 'return a.toString();'"); @@ -307,6 +321,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return Integer.valueOf((a!=null?a.intValue():0) + b.intValue());'"); @@ -314,6 +329,7 @@ public class AggregationTest extends CQLTester String fFinal = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s(a int) " + + "CALLED ON NULL INPUT " + "RETURNS text " + "LANGUAGE java " + "AS 'return a.toString();'"); @@ -340,6 +356,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return Integer.valueOf((a!=null?a.intValue():0) + b.intValue());'"); @@ -347,6 +364,7 @@ public class AggregationTest extends CQLTester String fFinal = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s(a int) " + + "CALLED ON NULL INPUT " + "RETURNS text " + "LANGUAGE java " + "AS 'return a.toString();'"); @@ -365,6 +383,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return Integer.valueOf((a!=null?a.intValue():0) + b.intValue());'"); @@ -372,6 +391,7 @@ public class AggregationTest extends CQLTester String fFinal = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s(a int) " + + "CALLED ON NULL INPUT " + "RETURNS text " + "LANGUAGE java " + "AS 'return a.toString();'"); @@ -379,6 +399,7 @@ public class AggregationTest extends CQLTester String fState2 = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a double, b double) " + + "CALLED ON NULL INPUT " + "RETURNS double " + "LANGUAGE java " + "AS 'return Double.valueOf((a!=null?a.doubleValue():0d) + b.doubleValue());'"); @@ -386,6 +407,7 @@ public class AggregationTest extends CQLTester String fFinal2 = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s(a double) " + + "CALLED ON NULL INPUT " + "RETURNS text " + "LANGUAGE java " + "AS 'return a.toString();'"); @@ -433,6 +455,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return Integer.valueOf((a!=null?a.intValue():0) + b.intValue());'"); @@ -440,6 +463,7 @@ public class AggregationTest extends CQLTester String fFinal = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s(a int) " + + "CALLED ON NULL INPUT " + "RETURNS text " + "LANGUAGE java " + "AS 'return a.toString();'"); @@ -474,13 +498,15 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + - "AS 'throw new RuntimeException();'"); + "AS 'throw new RuntimeException(\"thrown to unit test - not a bug\");'"); String fStateOK = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return Integer.valueOf(42);'"); @@ -488,13 +514,15 @@ public class AggregationTest extends CQLTester String fFinal = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s(a int) " + + "CALLED ON NULL INPUT " + "RETURNS text " + "LANGUAGE java " + - "AS 'throw new RuntimeException();'"); + "AS 'throw new RuntimeException(\"thrown to unit test - not a bug\");'"); String fFinalOK = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s(a int) " + + "CALLED ON NULL INPUT " + "RETURNS text " + "LANGUAGE java " + "AS 'return \"foobar\";'"); @@ -537,9 +565,10 @@ public class AggregationTest extends CQLTester String f = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + - "AS 'return Integer.valueOf((a!=null?a.intValue():0) + b.intValue());'"); + "AS 'return Integer.valueOf(a + b);'"); assertInvalidMessage("does not exist or is not a scalar function", "CREATE AGGREGATE " + KEYSPACE + ".jSumFooNE2(int) " + @@ -561,6 +590,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return Integer.valueOf((a!=null?a.intValue():0) + b.intValue());'"); @@ -568,6 +598,7 @@ public class AggregationTest extends CQLTester String fFinal = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s(a int) " + + "CALLED ON NULL INPUT " + "RETURNS text " + "LANGUAGE java " + "AS 'return a.toString();'"); @@ -602,6 +633,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return Integer.valueOf((a!=null?a.intValue():0) + b.intValue());'"); @@ -637,6 +669,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "frozen>, int", "CREATE FUNCTION %s(a frozen>, b int) " + + "CALLED ON NULL INPUT " + "RETURNS frozen> " + "LANGUAGE java " + "AS '" + @@ -648,6 +681,7 @@ public class AggregationTest extends CQLTester String fFinal = createFunction(KEYSPACE, "frozen>", "CREATE FUNCTION %s(a frozen>) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS double " + "LANGUAGE java " + "AS '" + @@ -680,6 +714,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS int " + "LANGUAGE javascript " + "AS 'a + b;'"); @@ -687,6 +722,7 @@ public class AggregationTest extends CQLTester String fFinal = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s(a int) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS text " + "LANGUAGE javascript " + "AS '\"\"+a'"); @@ -721,6 +757,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE javascript " + "AS 'a + b;'"); @@ -754,6 +791,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(otherKS, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE javascript " + "AS 'a + b;'"); @@ -796,6 +834,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE javascript " + "AS 'a + b;'"); @@ -818,6 +857,140 @@ public class AggregationTest extends CQLTester "FINALFUNC " + shortFunctionName(a)); } + @Test + public void testCalledOnNullInput() throws Throwable + { + String fStateNonNull = createFunction(KEYSPACE, + "int, int", + "CREATE OR REPLACE FUNCTION %s(state int, val int) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS int " + + "LANGUAGE java\n" + + "AS 'return Integer.valueOf(state + val);';"); + String fStateNull = createFunction(KEYSPACE, + "int, int", + "CREATE OR REPLACE FUNCTION %s(state int, val int) " + + "CALLED ON NULL INPUT " + + "RETURNS int " + + "LANGUAGE java\n" + + "AS 'return Integer.valueOf(" + + " (state != null ? state.intValue() : 0) " + + " + (val != null ? val.intValue() : 0));';"); + String fStateAlwaysNull = createFunction(KEYSPACE, + "int, int", + "CREATE OR REPLACE FUNCTION %s(state int, val int) " + + "CALLED ON NULL INPUT " + + "RETURNS int " + + "LANGUAGE java\n" + + "AS 'return null;';"); + String fFinalNonNull = createFunction(KEYSPACE, + "int", + "CREATE OR REPLACE FUNCTION %s(state int) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS int " + + "LANGUAGE java\n" + + "AS 'return Integer.valueOf(state);';"); + String fFinalNull = createFunction(KEYSPACE, + "int", + "CREATE OR REPLACE FUNCTION %s(state int) " + + "CALLED ON NULL INPUT " + + "RETURNS int " + + "LANGUAGE java\n" + + "AS 'return state;';"); + + assertInvalid("CREATE AGGREGATE " + KEYSPACE + ".invAggr(int) " + + "SFUNC " + shortFunctionName(fStateNonNull) + " " + + "STYPE int"); + assertInvalid("CREATE AGGREGATE " + KEYSPACE + ".invAggr(int) " + + "SFUNC " + shortFunctionName(fStateNonNull) + " " + + "STYPE int " + + "FINALFUNC " + shortFunctionName(fFinalNonNull)); + + String aStateNull = createAggregate(KEYSPACE, + "int", + "CREATE AGGREGATE %s(int) " + + "SFUNC " + shortFunctionName(fStateNull) + " " + + "STYPE int"); + String aStateNullFinalNull = createAggregate(KEYSPACE, + "int", + "CREATE AGGREGATE %s(int) " + + "SFUNC " + shortFunctionName(fStateNull) + " " + + "STYPE int " + + "FINALFUNC " + shortFunctionName(fFinalNull)); + String aStateNullFinalNonNull = createAggregate(KEYSPACE, + "int", + "CREATE AGGREGATE %s(int) " + + "SFUNC " + shortFunctionName(fStateNull) + " " + + "STYPE int " + + "FINALFUNC " + shortFunctionName(fFinalNonNull)); + String aStateNonNull = createAggregate(KEYSPACE, + "int", + "CREATE AGGREGATE %s(int) " + + "SFUNC " + shortFunctionName(fStateNonNull) + " " + + "STYPE int " + + "INITCOND 0"); + String aStateNonNullFinalNull = createAggregate(KEYSPACE, + "int", + "CREATE AGGREGATE %s(int) " + + "SFUNC " + shortFunctionName(fStateNonNull) + " " + + "STYPE int " + + "FINALFUNC " + shortFunctionName(fFinalNull) + " " + + "INITCOND 0"); + String aStateNonNullFinalNonNull = createAggregate(KEYSPACE, + "int", + "CREATE AGGREGATE %s(int) " + + "SFUNC " + shortFunctionName(fStateNonNull) + " " + + "STYPE int " + + "FINALFUNC " + shortFunctionName(fFinalNonNull) + " " + + "INITCOND 0"); + String aStateAlwaysNullFinalNull = createAggregate(KEYSPACE, + "int", + "CREATE AGGREGATE %s(int) " + + "SFUNC " + shortFunctionName(fStateAlwaysNull) + " " + + "STYPE int " + + "FINALFUNC " + shortFunctionName(fFinalNull)); + String aStateAlwaysNullFinalNonNull = createAggregate(KEYSPACE, + "int", + "CREATE AGGREGATE %s(int) " + + "SFUNC " + shortFunctionName(fStateAlwaysNull) + " " + + "STYPE int " + + "FINALFUNC " + shortFunctionName(fFinalNonNull)); + + createTable("CREATE TABLE %s (key int PRIMARY KEY, i int)"); + + execute("INSERT INTO %s (key, i) VALUES (0, null)"); + execute("INSERT INTO %s (key, i) VALUES (1, 1)"); + execute("INSERT INTO %s (key, i) VALUES (2, 2)"); + execute("INSERT INTO %s (key, i) VALUES (3, 3)"); + + assertRows(execute("SELECT " + aStateNull + "(i) FROM %s WHERE key = 0"), row(0)); + assertRows(execute("SELECT " + aStateNullFinalNull + "(i) FROM %s WHERE key = 0"), row(0)); + assertRows(execute("SELECT " + aStateNullFinalNonNull + "(i) FROM %s WHERE key = 0"), row(0)); + assertRows(execute("SELECT " + aStateNonNull + "(i) FROM %s WHERE key = 0"), row(0)); + assertRows(execute("SELECT " + aStateNonNullFinalNull + "(i) FROM %s WHERE key = 0"), row(0)); + assertRows(execute("SELECT " + aStateNonNullFinalNonNull + "(i) FROM %s WHERE key = 0"), row(0)); + assertRows(execute("SELECT " + aStateAlwaysNullFinalNull + "(i) FROM %s WHERE key = 0"), row(new Object[]{null})); + assertRows(execute("SELECT " + aStateAlwaysNullFinalNonNull + "(i) FROM %s WHERE key = 0"), row(new Object[]{null})); + + assertRows(execute("SELECT " + aStateNull + "(i) FROM %s WHERE key = 1"), row(1)); + assertRows(execute("SELECT " + aStateNullFinalNull + "(i) FROM %s WHERE key = 1"), row(1)); + assertRows(execute("SELECT " + aStateNullFinalNonNull + "(i) FROM %s WHERE key = 1"), row(1)); + assertRows(execute("SELECT " + aStateNonNull + "(i) FROM %s WHERE key = 1"), row(1)); + assertRows(execute("SELECT " + aStateNonNullFinalNull + "(i) FROM %s WHERE key = 1"), row(1)); + assertRows(execute("SELECT " + aStateNonNullFinalNonNull + "(i) FROM %s WHERE key = 1"), row(1)); + assertRows(execute("SELECT " + aStateAlwaysNullFinalNull + "(i) FROM %s WHERE key = 1"), row(new Object[]{null})); + assertRows(execute("SELECT " + aStateAlwaysNullFinalNonNull + "(i) FROM %s WHERE key = 1"), row(new Object[]{null})); + + assertRows(execute("SELECT " + aStateNull + "(i) FROM %s WHERE key IN (1, 2, 3)"), row(6)); + assertRows(execute("SELECT " + aStateNullFinalNull + "(i) FROM %s WHERE key IN (1, 2, 3)"), row(6)); + assertRows(execute("SELECT " + aStateNullFinalNonNull + "(i) FROM %s WHERE key IN (1, 2, 3)"), row(6)); + assertRows(execute("SELECT " + aStateNonNull + "(i) FROM %s WHERE key IN (1, 2, 3)"), row(6)); + assertRows(execute("SELECT " + aStateNonNullFinalNull + "(i) FROM %s WHERE key IN (1, 2, 3)"), row(6)); + assertRows(execute("SELECT " + aStateNonNullFinalNonNull + "(i) FROM %s WHERE key IN (1, 2, 3)"), row(6)); + assertRows(execute("SELECT " + aStateAlwaysNullFinalNull + "(i) FROM %s WHERE key IN (1, 2, 3)"), row(new Object[]{null})); + assertRows(execute("SELECT " + aStateAlwaysNullFinalNonNull + "(i) FROM %s WHERE key IN (1, 2, 3)"), row(new Object[]{null})); + } + @Test public void testBrokenAggregate() throws Throwable { @@ -827,6 +1000,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE javascript " + "AS 'a + b;'"); @@ -855,6 +1029,7 @@ public class AggregationTest extends CQLTester String fState = createFunction(KEYSPACE, "int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS double " + "LANGUAGE java " + "AS 'return Double.valueOf(1.0);'"); @@ -862,6 +1037,7 @@ public class AggregationTest extends CQLTester String fFinal = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s(a int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return Integer.valueOf(1);';"); diff --git a/test/unit/org/apache/cassandra/cql3/JsonTest.java b/test/unit/org/apache/cassandra/cql3/JsonTest.java index 305502e267..0380dddedd 100644 --- a/test/unit/org/apache/cassandra/cql3/JsonTest.java +++ b/test/unit/org/apache/cassandra/cql3/JsonTest.java @@ -72,8 +72,8 @@ public class JsonTest extends CQLTester // fromJson() can only be used when the receiver type is known assertInvalidMessage("fromJson() cannot be used in the selection clause", "SELECT fromJson(asciival) FROM %s", 0, 0); - String func1 = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s (a int) RETURNS text LANGUAGE java AS $$ return a.toString(); $$"); - createFunctionOverload(func1, "int", "CREATE FUNCTION %s (a text) RETURNS text LANGUAGE java AS $$ return new String(a); $$"); + String func1 = createFunction(KEYSPACE, "int", "CREATE FUNCTION %s (a int) CALLED ON NULL INPUT RETURNS text LANGUAGE java AS $$ return a.toString(); $$"); + createFunctionOverload(func1, "int", "CREATE FUNCTION %s (a text) CALLED ON NULL INPUT RETURNS text LANGUAGE java AS $$ return new String(a); $$"); assertInvalidMessage("Ambiguous call to function", "INSERT INTO %s (k, textval) VALUES (?, " + func1 + "(fromJson(?)))", 0, "123"); diff --git a/test/unit/org/apache/cassandra/cql3/PgStringTest.java b/test/unit/org/apache/cassandra/cql3/PgStringTest.java index 1870a9ab14..0a9d70272d 100644 --- a/test/unit/org/apache/cassandra/cql3/PgStringTest.java +++ b/test/unit/org/apache/cassandra/cql3/PgStringTest.java @@ -26,7 +26,7 @@ public class PgStringTest extends CQLTester @Test public void testPgSyleFunction() throws Throwable { - execute("create or replace function "+KEYSPACE+".pgfun1 ( input double ) returns text language java\n" + + execute("create or replace function "+KEYSPACE+".pgfun1 ( input double ) called on null input returns text language java\n" + "AS $$return \"foobar\";$$"); } @@ -70,7 +70,7 @@ public class PgStringTest extends CQLTester public void testMarkerPgFail() throws Throwable { // must throw SyntaxException - not StringIndexOutOfBoundsException or similar - execute("create function "+KEYSPACE+".pgfun1 ( input double ) returns text language java\n" + + execute("create function "+KEYSPACE+".pgfun1 ( input double ) called on null input returns bigint language java\n" + "AS $javasrc$return 0L;$javasrc$;"); } } diff --git a/test/unit/org/apache/cassandra/cql3/UFAuthTest.java b/test/unit/org/apache/cassandra/cql3/UFAuthTest.java index 1d63d29ce3..2c36bd1b9d 100644 --- a/test/unit/org/apache/cassandra/cql3/UFAuthTest.java +++ b/test/unit/org/apache/cassandra/cql3/UFAuthTest.java @@ -249,6 +249,7 @@ public class UFAuthTest extends CQLTester String innerFunctionName = createSimpleFunction(); String outerFunctionName = createFunction("int", "CREATE FUNCTION %s(input int) " + + " CALLED ON NULL INPUT" + " RETURNS int" + " LANGUAGE java" + " AS 'return Integer.valueOf(0);'"); @@ -389,6 +390,7 @@ public class UFAuthTest extends CQLTester { String outerFunc = createFunction("int", "CREATE FUNCTION %s(input int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return input;'"); @@ -420,6 +422,7 @@ public class UFAuthTest extends CQLTester { String innerFunc = createFunction("int", "CREATE FUNCTION %s(input int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return input;'"); @@ -568,6 +571,7 @@ public class UFAuthTest extends CQLTester { return createFunction("int, int", "CREATE FUNCTION %s(a int, b int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return Integer.valueOf( (a != null ? a.intValue() : 0 ) + b.intValue());'"); @@ -577,6 +581,7 @@ public class UFAuthTest extends CQLTester { return createFunction("int", "CREATE FUNCTION %s(a int) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS 'return a;'"); @@ -586,6 +591,7 @@ public class UFAuthTest extends CQLTester { return createFunction("", "CREATE FUNCTION %s() " + + " CALLED ON NULL INPUT " + " RETURNS int " + " LANGUAGE java " + " AS 'return Integer.valueOf(0);'"); diff --git a/test/unit/org/apache/cassandra/cql3/UFIdentificationTest.java b/test/unit/org/apache/cassandra/cql3/UFIdentificationTest.java index 044c98b87d..7cac25204e 100644 --- a/test/unit/org/apache/cassandra/cql3/UFIdentificationTest.java +++ b/test/unit/org/apache/cassandra/cql3/UFIdentificationTest.java @@ -368,6 +368,7 @@ public class UFIdentificationTest extends CQLTester { return createFunction(KEYSPACE, type, "CREATE FUNCTION %s(input " + type + ")" + + " CALLED ON NULL INPUT" + " RETURNS " + type + " LANGUAGE java" + " AS ' return input;'"); diff --git a/test/unit/org/apache/cassandra/cql3/UFTest.java b/test/unit/org/apache/cassandra/cql3/UFTest.java index c9779b9d58..78c6dd92c1 100644 --- a/test/unit/org/apache/cassandra/cql3/UFTest.java +++ b/test/unit/org/apache/cassandra/cql3/UFTest.java @@ -46,6 +46,7 @@ public class UFTest extends CQLTester String f = createFunction(KEYSPACE, "double, double", "CREATE OR REPLACE FUNCTION %s(state double, val double) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS double " + "LANGUAGE javascript " + "AS '\"string\";';"); @@ -57,6 +58,7 @@ public class UFTest extends CQLTester createFunctionOverload(f, "double, double", "CREATE OR REPLACE FUNCTION %s(state int, val int) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS int " + "LANGUAGE javascript " + "AS '\"string\";';"); @@ -66,6 +68,7 @@ public class UFTest extends CQLTester "int", "int"); schemaChange("CREATE OR REPLACE FUNCTION " + f + "(state int, val int) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS int " + "LANGUAGE javascript " + "AS '\"string\";';"); @@ -86,6 +89,7 @@ public class UFTest extends CQLTester { String fSin = createFunction(KEYSPACE_PER_TEST, "double", "CREATE FUNCTION %s ( input double ) " + + "CALLED ON NULL INPUT " + "RETURNS double " + "LANGUAGE java " + "AS 'return Double.valueOf(Math.sin(input.doubleValue()));'"); @@ -111,6 +115,7 @@ public class UFTest extends CQLTester String fSin = createFunction(KEYSPACE_PER_TEST, "double", "CREATE FUNCTION %s ( input double ) " + + "CALLED ON NULL INPUT " + "RETURNS double " + "LANGUAGE java " + "AS 'return Double.valueOf(Math.sin(input.doubleValue()));'"); @@ -150,9 +155,10 @@ public class UFTest extends CQLTester Assert.assertNotNull(QueryProcessor.instance.getPrepared(preparedInsert2.statementId)); execute("CREATE FUNCTION " + fSin + " ( input double ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS double " + "LANGUAGE java " + - "AS 'return Double.valueOf(Math.sin(input.doubleValue()));'"); + "AS 'return Double.valueOf(Math.sin(input));'"); Assert.assertEquals(1, Functions.find(fSinName).size()); @@ -220,7 +226,7 @@ public class UFTest extends CQLTester createTable("CREATE TABLE %s (" + " key int PRIMARY KEY," + - " val " + collectionType + ")"); + " val " + collectionType + ')'); ResultMessage.Prepared prepared = QueryProcessor.prepare( String.format("INSERT INTO %s.%s (key, val) VALUES (?, %s)", @@ -265,6 +271,7 @@ public class UFTest extends CQLTester // provide a DelayedValue are removed from the cache in QueryProcessor String function = createFunction(KEYSPACE_PER_TEST, "double", "CREATE FUNCTION %s ( input double ) " + + "CALLED ON NULL INPUT " + "RETURNS double " + "LANGUAGE javascript " + "AS 'input'"); @@ -304,13 +311,22 @@ public class UFTest extends CQLTester // simple creation String fSin = createFunction(KEYSPACE_PER_TEST, "double", "CREATE FUNCTION %s ( input double ) " + + "CALLED ON NULL INPUT " + "RETURNS double " + "LANGUAGE java " + "AS 'return Math.sin(input);'"); // check we can't recreate the same function - assertInvalidMessage("already exists", "CREATE FUNCTION " + fSin + " ( input double ) RETURNS double LANGUAGE java AS 'return Math.sin(input);'"); + assertInvalidMessage("already exists", + "CREATE FUNCTION " + fSin + " ( input double ) " + + "CALLED ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE java AS 'return Double.valueOf(Math.sin(input.doubleValue()));'"); + // but that it doesn't comply with "IF NOT EXISTS" - execute("CREATE FUNCTION IF NOT EXISTS " + fSin + " ( input double ) RETURNS double LANGUAGE java AS 'return Math.sin(input);'"); + execute("CREATE FUNCTION IF NOT EXISTS " + fSin + " ( input double ) " + + "CALLED ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE java AS 'return Double.valueOf(Math.sin(input.doubleValue()));'"); // Validate that it works as expected assertRows(execute("SELECT key, " + fSin + "(d) FROM %s"), @@ -321,9 +337,16 @@ public class UFTest extends CQLTester // Replace the method with incompatible return type assertInvalidMessage("the new return type text is not compatible with the return type double of existing function", - "CREATE OR REPLACE FUNCTION " + fSin + " ( input double ) RETURNS text LANGUAGE java AS 'return Double.valueOf(42d);'"); + "CREATE OR REPLACE FUNCTION " + fSin + " ( input double ) " + + "CALLED ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java AS 'return Double.valueOf(42d);'"); + // proper replacement - execute("CREATE OR REPLACE FUNCTION " + fSin + " ( input double ) RETURNS double LANGUAGE java AS 'return Double.valueOf(42d);'"); + execute("CREATE OR REPLACE FUNCTION " + fSin + " ( input double ) " + + "CALLED ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE java AS 'return Double.valueOf(42d);'"); // Validate the method as been replaced assertRows(execute("SELECT key, " + fSin + "(d) FROM %s"), @@ -335,6 +358,7 @@ public class UFTest extends CQLTester // same function but other keyspace String fSin2 = createFunction(KEYSPACE, "double", "CREATE FUNCTION %s ( input double ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS double " + "LANGUAGE java " + "AS 'return Math.sin(input);'"); @@ -370,6 +394,7 @@ public class UFTest extends CQLTester String fRepeat = createFunction(KEYSPACE_PER_TEST, "text,int", "CREATE FUNCTION %s(v text, n int) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS text " + "LANGUAGE java " + "AS 'StringBuilder sb = new StringBuilder();\n" + @@ -390,22 +415,38 @@ public class UFTest extends CQLTester String fOverload = createFunction(KEYSPACE_PER_TEST, "varchar", "CREATE FUNCTION %s ( input varchar ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS text " + "LANGUAGE java " + "AS 'return \"f1\";'"); createFunctionOverload(fOverload, "int", - "CREATE OR REPLACE FUNCTION %s(i int) RETURNS text LANGUAGE java AS 'return \"f2\";'"); + "CREATE OR REPLACE FUNCTION %s(i int) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java " + + "AS 'return \"f2\";'"); createFunctionOverload(fOverload, "text,text", - "CREATE OR REPLACE FUNCTION %s(v1 text, v2 text) RETURNS text LANGUAGE java AS 'return \"f3\";'"); + "CREATE OR REPLACE FUNCTION %s(v1 text, v2 text) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java " + + "AS 'return \"f3\";'"); createFunctionOverload(fOverload, "ascii", - "CREATE OR REPLACE FUNCTION %s(v ascii) RETURNS text LANGUAGE java AS 'return \"f1\";'"); + "CREATE OR REPLACE FUNCTION %s(v ascii) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java " + + "AS 'return \"f1\";'"); // text == varchar, so this should be considered as a duplicate assertInvalidMessage("already exists", - "CREATE FUNCTION " + fOverload + "(v varchar) RETURNS text LANGUAGE java AS 'return \"f1\";'"); + "CREATE FUNCTION " + fOverload + "(v varchar) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java AS 'return \"f1\";'"); assertRows(execute("SELECT " + fOverload + "(k), " + fOverload + "(v), " + fOverload + "(k, k) FROM %s"), row("f1", "f2", "f3") @@ -451,7 +492,9 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE_PER_TEST, "double", "CREATE FUNCTION %s( input double ) " + - "RETURNS double LANGUAGE java\n" + + "CALLED ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE java " + "AS '\n" + " // parameter val is of type java.lang.Double\n" + " /* return type is of type java.lang.Double */\n" + @@ -469,7 +512,9 @@ public class UFTest extends CQLTester ); execute("CREATE OR REPLACE FUNCTION " + fName + "( input double ) " + - "RETURNS double LANGUAGE java\n" + + "CALLED ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE java\n" + "AS '\n" + " return input;\n" + "';"); @@ -490,7 +535,10 @@ public class UFTest extends CQLTester String functionBody = "\n return 1L;\n"; String fName = createFunction(KEYSPACE, "", - "CREATE OR REPLACE FUNCTION %s() RETURNS bigint LANGUAGE JAVA\n" + + "CREATE OR REPLACE FUNCTION %s() " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS bigint " + + "LANGUAGE JAVA\n" + "AS '" +functionBody + "';"); assertRows(execute("SELECT language, body FROM system.schema_functions WHERE keyspace_name=? AND function_name=?", @@ -512,7 +560,10 @@ public class UFTest extends CQLTester { try { - execute("CREATE OR REPLACE FUNCTION " + KEYSPACE + ".jfinv() RETURNS bigint LANGUAGE JAVA\n" + + execute("CREATE OR REPLACE FUNCTION " + KEYSPACE + ".jfinv() " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS bigint " + + "LANGUAGE JAVA\n" + "AS '\n" + "foobarbaz" + "\n';"); @@ -526,7 +577,10 @@ public class UFTest extends CQLTester try { - execute("CREATE OR REPLACE FUNCTION " + KEYSPACE + ".jfinv() RETURNS bigint LANGUAGE JAVA\n" + + execute("CREATE OR REPLACE FUNCTION " + KEYSPACE + ".jfinv() " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS bigint " + + "LANGUAGE JAVA\n" + "AS '\n" + "foobarbaz;" + "\n';"); @@ -543,7 +597,10 @@ public class UFTest extends CQLTester public void testJavaFunctionInvalidReturn() throws Throwable { assertInvalidMessage("system keyspace is not user-modifiable", - "CREATE OR REPLACE FUNCTION jfir(val double) RETURNS double LANGUAGE JAVA\n" + + "CREATE OR REPLACE FUNCTION jfir(val double) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE JAVA\n" + "AS 'return 1L;';"); } @@ -554,8 +611,10 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE, "double", "CREATE OR REPLACE FUNCTION %s(val double)" + - "RETURNS double LANGUAGE JAVA " + - "AS 'return val;';"); + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE JAVA " + + "AS 'return Double.valueOf(val);';"); execute("INSERT INTO %s (key, val) VALUES (?, ?)", 1, 1L); execute("INSERT INTO %s (key, val) VALUES (?, ?)", 2, 2L); @@ -579,6 +638,7 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE, "double", "CREATE OR REPLACE FUNCTION %s(val double) " + + "CALLED ON NULL INPUT " + "RETURNS double " + "LANGUAGE JAVA " + "AS '" + functionBody + "';"); @@ -607,8 +667,11 @@ public class UFTest extends CQLTester execute("CREATE TABLE " + KEYSPACE_PER_TEST + ".second_tab (key int primary key, val double)"); String fName = createFunction(KEYSPACE_PER_TEST, "double", - "CREATE OR REPLACE FUNCTION %s(val double) RETURNS double LANGUAGE JAVA " + - "AS 'return val;';"); + "CREATE OR REPLACE FUNCTION %s(val double) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE JAVA " + + "AS 'return Double.valueOf(val);';"); execute("INSERT INTO %s (key, val) VALUES (?, ?)", 1, 1d); execute("INSERT INTO %s (key, val) VALUES (?, ?)", 2, 2d); @@ -632,7 +695,10 @@ public class UFTest extends CQLTester execute("CREATE TABLE " + KEYSPACE_PER_TEST + ".second_tab (key int primary key, val double)"); String fName = createFunction(KEYSPACE_PER_TEST, "", - "CREATE OR REPLACE FUNCTION %s() RETURNS timestamp LANGUAGE JAVA " + + "CREATE OR REPLACE FUNCTION %s() " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS timestamp " + + "LANGUAGE JAVA " + "AS 'return null;';"); execute("INSERT INTO " + KEYSPACE_PER_TEST + ".second_tab (key, val) VALUES (?, ?)", 1, 1d); @@ -656,24 +722,39 @@ public class UFTest extends CQLTester @Test public void testFunctionInSystemKS() throws Throwable { - execute("CREATE OR REPLACE FUNCTION " + KEYSPACE + ".dateof(val timeuuid) RETURNS timestamp LANGUAGE JAVA\n" + + execute("CREATE OR REPLACE FUNCTION " + KEYSPACE + ".dateof(val timeuuid) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS timestamp " + + "LANGUAGE JAVA\n" + "AS 'return null;';"); assertInvalidMessage("system keyspace is not user-modifiable", - "CREATE OR REPLACE FUNCTION system.jnft(val double) RETURNS double LANGUAGE JAVA\n" + + "CREATE OR REPLACE FUNCTION system.jnft(val double) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE JAVA\n" + "AS 'return null;';"); assertInvalidMessage("system keyspace is not user-modifiable", - "CREATE OR REPLACE FUNCTION system.dateof(val timeuuid) RETURNS timestamp LANGUAGE JAVA\n" + + "CREATE OR REPLACE FUNCTION system.dateof(val timeuuid) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS timestamp " + + "LANGUAGE JAVA\n" + "AS 'return null;';"); assertInvalidMessage("system keyspace is not user-modifiable", "DROP FUNCTION system.now"); // KS for executeInternal() is system assertInvalidMessage("system keyspace is not user-modifiable", - "CREATE OR REPLACE FUNCTION jnft(val double) RETURNS double LANGUAGE JAVA\n" + + "CREATE OR REPLACE FUNCTION jnft(val double) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE JAVA\n" + "AS 'return null;';"); assertInvalidMessage("system keyspace is not user-modifiable", - "CREATE OR REPLACE FUNCTION dateof(val timeuuid) RETURNS timestamp LANGUAGE JAVA\n" + + "CREATE OR REPLACE FUNCTION dateof(val timeuuid) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS timestamp " + + "LANGUAGE JAVA\n" + "AS 'return null;';"); assertInvalidMessage("system keyspace is not user-modifiable", "DROP FUNCTION now"); @@ -683,7 +764,10 @@ public class UFTest extends CQLTester public void testFunctionNonExistingKeyspace() throws Throwable { assertInvalidMessage("to non existing keyspace", - "CREATE OR REPLACE FUNCTION this_ks_does_not_exist.jnft(val double) RETURNS double LANGUAGE JAVA\n" + + "CREATE OR REPLACE FUNCTION this_ks_does_not_exist.jnft(val double) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE JAVA\n" + "AS 'return null;';"); } @@ -693,7 +777,10 @@ public class UFTest extends CQLTester dropPerTestKeyspace(); assertInvalidMessage("to non existing keyspace", - "CREATE OR REPLACE FUNCTION " + KEYSPACE_PER_TEST + ".jnft(val double) RETURNS double LANGUAGE JAVA\n" + + "CREATE OR REPLACE FUNCTION " + KEYSPACE_PER_TEST + ".jnft(val double) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE JAVA\n" + "AS 'return null;';"); } @@ -711,7 +798,10 @@ public class UFTest extends CQLTester " return Math.sin( val );\n"; String fName = createFunction(KEYSPACE_PER_TEST, "double", - "CREATE OR REPLACE FUNCTION %s(val double) RETURNS double LANGUAGE JAVA " + + "CREATE OR REPLACE FUNCTION %s(val double) " + + "CALLED ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE JAVA " + "AS '" + functionBody + "';"); FunctionName fNameName = parseFunctionName(fName); @@ -739,7 +829,10 @@ public class UFTest extends CQLTester " throw new RuntimeException(\"oh no!\");\n"; String fName = createFunction(KEYSPACE_PER_TEST, "double", - "CREATE OR REPLACE FUNCTION %s(val double) RETURNS double LANGUAGE JAVA\n" + + "CREATE OR REPLACE FUNCTION %s(val double) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE JAVA\n" + "AS '" + functionBody + "';"); FunctionName fNameName = parseFunctionName(fName); @@ -770,6 +863,7 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE_PER_TEST, "double", "CREATE FUNCTION %s( input double ) " + + "CALLED ON NULL INPUT " + "RETURNS text " + "LANGUAGE java\n" + "AS $$" + functionBody + "$$;"); @@ -788,15 +882,21 @@ public class UFTest extends CQLTester String fList = createFunction(KEYSPACE_PER_TEST, "list", "CREATE FUNCTION %s( lst list ) " + - "RETURNS list LANGUAGE java\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS list " + + "LANGUAGE java\n" + "AS $$return lst;$$;"); String fSet = createFunction(KEYSPACE_PER_TEST, "set", "CREATE FUNCTION %s( st set ) " + - "RETURNS set LANGUAGE java\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS set " + + "LANGUAGE java\n" + "AS $$return st;$$;"); String fMap = createFunction(KEYSPACE_PER_TEST, "map", "CREATE FUNCTION %s( mp map ) " + - "RETURNS map LANGUAGE java\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS map " + + "LANGUAGE java\n" + "AS $$return mp;$$;"); List list = Arrays.asList(1d, 2d, 3d); @@ -828,26 +928,31 @@ public class UFTest extends CQLTester String fList = createFunction(KEYSPACE, "list", "CREATE FUNCTION %s( coll list ) " + + "CALLED ON NULL INPUT " + "RETURNS list " + "LANGUAGE java\n" + "AS $$return coll;$$;"); String fSet = createFunction(KEYSPACE, "set", "CREATE FUNCTION %s( coll set ) " + + "CALLED ON NULL INPUT " + "RETURNS set " + "LANGUAGE java\n" + "AS $$return coll;$$;"); String fMap = createFunction(KEYSPACE, "map", "CREATE FUNCTION %s( coll map ) " + + "CALLED ON NULL INPUT " + "RETURNS map " + "LANGUAGE java\n" + "AS $$return coll;$$;"); String fTup = createFunction(KEYSPACE, "frozen>", "CREATE FUNCTION %s( val frozen> ) " + + "CALLED ON NULL INPUT " + "RETURNS frozen> " + "LANGUAGE java\n" + "AS $$return val;$$;"); String fUdt = createFunction(KEYSPACE, "frozen<" + type+'>', "CREATE FUNCTION %s( val frozen<" + type + "> ) " + + "CALLED ON NULL INPUT " + "RETURNS frozen<" + type + "> " + "LANGUAGE java\n" + "AS $$return val;$$;"); @@ -929,6 +1034,7 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE, "frozen>", "CREATE FUNCTION %s( tup frozen> ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS frozen> " + "LANGUAGE java\n" + "AS $$return tup;$$;"); @@ -953,30 +1059,35 @@ public class UFTest extends CQLTester String fTup0 = createFunction(KEYSPACE_PER_TEST, tupleTypeDef, "CREATE FUNCTION %s( tup " + tupleTypeDef + " ) " + + "CALLED ON NULL INPUT " + "RETURNS " + tupleTypeDef + ' ' + "LANGUAGE java\n" + "AS $$return " + " tup;$$;"); String fTup1 = createFunction(KEYSPACE_PER_TEST, tupleTypeDef, "CREATE FUNCTION %s( tup " + tupleTypeDef + " ) " + + "CALLED ON NULL INPUT " + "RETURNS double " + "LANGUAGE java\n" + "AS $$return " + " Double.valueOf(tup.getDouble(0));$$;"); String fTup2 = createFunction(KEYSPACE_PER_TEST, tupleTypeDef, - "CREATE FUNCTION %s( tup " + tupleTypeDef + " ) " + - "RETURNS list " + - "LANGUAGE java\n" + - "AS $$return " + - " tup.getList(1, Double.class);$$;"); + "CREATE FUNCTION %s( tup " + tupleTypeDef + " ) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS list " + + "LANGUAGE java\n" + + "AS $$return " + + " tup.getList(1, Double.class);$$;"); String fTup3 = createFunction(KEYSPACE_PER_TEST, tupleTypeDef, "CREATE FUNCTION %s( tup " + tupleTypeDef + " ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS set " + "LANGUAGE java\n" + "AS $$return " + " tup.getSet(2, String.class);$$;"); String fTup4 = createFunction(KEYSPACE_PER_TEST, tupleTypeDef, "CREATE FUNCTION %s( tup " + tupleTypeDef + " ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS map " + "LANGUAGE java\n" + "AS $$return " + @@ -1042,6 +1153,7 @@ public class UFTest extends CQLTester executeNet(version, "CREATE FUNCTION f_use1( udt frozen<" + type + "> ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS frozen<" + type + "> " + "LANGUAGE java " + "AS $$return " + @@ -1068,6 +1180,7 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE_PER_TEST, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS frozen<" + type + "> " + "LANGUAGE java " + "AS $$return " + @@ -1085,18 +1198,21 @@ public class UFTest extends CQLTester String fUdt0 = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS frozen<" + type + "> " + "LANGUAGE java " + "AS $$return " + " udt;$$;"); String fUdt1 = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS text " + "LANGUAGE java " + "AS $$return " + " udt.getString(\"txt\");$$;"); String fUdt2 = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS $$return " + @@ -1136,6 +1252,7 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + + "CALLED ON NULL INPUT " + "RETURNS int " + "LANGUAGE java " + "AS $$return " + @@ -1172,7 +1289,9 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + - "RETURNS text LANGUAGE java\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + "AS $$return udt.getString(\"txt\");$$;"); execute("INSERT INTO %s (key, udt) VALUES (1, {txt: 'one', i:1})"); @@ -1200,36 +1319,52 @@ public class UFTest extends CQLTester String fName1replace = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + - "RETURNS text LANGUAGE java\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + "AS $$return udt.getString(\"txt\");$$;"); String fName2replace = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + - "RETURNS int LANGUAGE java\n" + + "CALLED ON NULL INPUT " + + "RETURNS int " + + "LANGUAGE java\n" + "AS $$return Integer.valueOf(udt.getInt(\"i\"));$$;"); String fName3replace = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + - "RETURNS double LANGUAGE java\n" + + "CALLED ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE java\n" + "AS $$return Double.valueOf(udt.getDouble(\"added\"));$$;"); String fName4replace = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + - "RETURNS frozen<" + type + "> LANGUAGE java\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS frozen<" + type + "> " + + "LANGUAGE java\n" + "AS $$return udt;$$;"); String fName1noReplace = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + - "RETURNS text LANGUAGE java\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + "AS $$return udt.getString(\"txt\");$$;"); String fName2noReplace = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + - "RETURNS int LANGUAGE java\n" + + "CALLED ON NULL INPUT " + + "RETURNS int " + + "LANGUAGE java\n" + "AS $$return Integer.valueOf(udt.getInt(\"i\"));$$;"); String fName3noReplace = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + - "RETURNS double LANGUAGE java\n" + + "CALLED ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE java\n" + "AS $$return Double.valueOf(udt.getDouble(\"added\"));$$;"); String fName4noReplace = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + - "RETURNS frozen<" + type + "> LANGUAGE java\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS frozen<" + type + "> " + + "LANGUAGE java\n" + "AS $$return udt;$$;"); execute("INSERT INTO %s (key, udt) VALUES (1, {txt: 'one', i:1})"); @@ -1249,25 +1384,33 @@ public class UFTest extends CQLTester // means we need to recreate the functions execute(String.format("CREATE OR REPLACE FUNCTION %s( udt frozen<%s> ) " + - "RETURNS text LANGUAGE java\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + "AS $$return " + " udt.getString(\"txt\");$$;", fName1replace, type)); Assert.assertEquals(1, Functions.find(parseFunctionName(fName1replace)).size()); execute(String.format("CREATE OR REPLACE FUNCTION %s( udt frozen<%s> ) " + - "RETURNS int LANGUAGE java\n" + + "CALLED ON NULL INPUT " + + "RETURNS int " + + "LANGUAGE java\n" + "AS $$return " + " Integer.valueOf(udt.getInt(\"i\"));$$;", fName2replace, type)); Assert.assertEquals(1, Functions.find(parseFunctionName(fName2replace)).size()); execute(String.format("CREATE OR REPLACE FUNCTION %s( udt frozen<%s> ) " + - "RETURNS double LANGUAGE java\n" + + "CALLED ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE java\n" + "AS $$return " + " Double.valueOf(udt.getDouble(\"added\"));$$;", fName3replace, type)); Assert.assertEquals(1, Functions.find(parseFunctionName(fName3replace)).size()); execute(String.format("CREATE OR REPLACE FUNCTION %s( udt frozen<%s> ) " + - "RETURNS frozen<%s> LANGUAGE java\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS frozen<%s> " + + "LANGUAGE java\n" + "AS $$return " + " udt;$$;", fName4replace, type, type)); @@ -1315,19 +1458,25 @@ public class UFTest extends CQLTester String fName1 = createFunction(KEYSPACE, "list>", "CREATE FUNCTION %s( lst list> ) " + - "RETURNS text LANGUAGE java\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + "AS $$" + " com.datastax.driver.core.UDTValue udtVal = (com.datastax.driver.core.UDTValue)lst.get(1);" + " return udtVal.getString(\"txt\");$$;"); String fName2 = createFunction(KEYSPACE, "set>", - "CREATE FUNCTION %s( st set> ) " + - "RETURNS text LANGUAGE java\n" + - "AS $$" + - " com.datastax.driver.core.UDTValue udtVal = (com.datastax.driver.core.UDTValue)st.iterator().next();" + - " return udtVal.getString(\"txt\");$$;"); + "CREATE FUNCTION %s( st set> ) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS $$" + + " com.datastax.driver.core.UDTValue udtVal = (com.datastax.driver.core.UDTValue)st.iterator().next();" + + " return udtVal.getString(\"txt\");$$;"); String fName3 = createFunction(KEYSPACE, "map>", "CREATE FUNCTION %s( mp map> ) " + - "RETURNS text LANGUAGE java\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + "AS $$" + " com.datastax.driver.core.UDTValue udtVal = (com.datastax.driver.core.UDTValue)mp.get(Integer.valueOf(3));" + " return udtVal.getString(\"txt\");$$;"); @@ -1353,16 +1502,19 @@ public class UFTest extends CQLTester String fName1 = createFunction(KEYSPACE_PER_TEST, "list", "CREATE FUNCTION %s( lst list ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS list " + "LANGUAGE javascript\n" + "AS 'lst;';"); String fName2 = createFunction(KEYSPACE_PER_TEST, "set", "CREATE FUNCTION %s( st set ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS set " + "LANGUAGE javascript\n" + "AS 'st;';"); String fName3 = createFunction(KEYSPACE_PER_TEST, "map", "CREATE FUNCTION %s( mp map ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS map " + "LANGUAGE javascript\n" + "AS 'mp;';"); @@ -1395,6 +1547,7 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE_PER_TEST, "frozen>", "CREATE FUNCTION %s( tup frozen> ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS frozen> " + "LANGUAGE javascript\n" + "AS $$tup;$$;"); @@ -1418,30 +1571,35 @@ public class UFTest extends CQLTester String fTup1 = createFunction(KEYSPACE_PER_TEST, tupleTypeDef, "CREATE FUNCTION %s( tup " + tupleTypeDef + " ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS frozen, set, map>> " + "LANGUAGE javascript\n" + "AS $$" + " tup;$$;"); String fTup2 = createFunction(KEYSPACE_PER_TEST, tupleTypeDef, "CREATE FUNCTION %s( tup " + tupleTypeDef + " ) " + - "RETURNS double LANGUAGE " + - "javascript\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE javascript\n" + "AS $$" + " tup.getDouble(0);$$;"); String fTup3 = createFunction(KEYSPACE_PER_TEST, tupleTypeDef, "CREATE FUNCTION %s( tup " + tupleTypeDef + " ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS list " + "LANGUAGE javascript\n" + "AS $$" + " tup.getList(1, java.lang.Class.forName(\"java.lang.Double\"));$$;"); String fTup4 = createFunction(KEYSPACE_PER_TEST, tupleTypeDef, "CREATE FUNCTION %s( tup " + tupleTypeDef + " ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS set " + "LANGUAGE javascript\n" + "AS $$" + " tup.getSet(2, java.lang.Class.forName(\"java.lang.String\"));$$;"); String fTup5 = createFunction(KEYSPACE_PER_TEST, tupleTypeDef, "CREATE FUNCTION %s( tup " + tupleTypeDef + " ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS map " + "LANGUAGE javascript\n" + "AS $$" + @@ -1504,22 +1662,25 @@ public class UFTest extends CQLTester String fUdt1 = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS frozen<" + type + "> " + "LANGUAGE javascript\n" + "AS $$" + " udt;$$;"); String fUdt2 = createFunction(KEYSPACE, "frozen<" + type + '>', "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS text " + "LANGUAGE javascript\n" + "AS $$" + " udt.getString(\"txt\");$$;"); String fUdt3 = createFunction(KEYSPACE, "frozen<" + type + '>', - "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + - "RETURNS int " + - "LANGUAGE javascript\n" + - "AS $$" + - " udt.getInt(\"i\");$$;"); + "CREATE FUNCTION %s( udt frozen<" + type + "> ) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS int " + + "LANGUAGE javascript\n" + + "AS $$" + + " udt.getInt(\"i\");$$;"); execute("INSERT INTO %s (key, udt) VALUES (1, {txt: 'one', i:1})"); @@ -1541,19 +1702,22 @@ public class UFTest extends CQLTester type, type, type)); String fName = createFunction(KEYSPACE, "list>", - "CREATE FUNCTION %s( lst list> ) " + - "RETURNS text " + - "LANGUAGE javascript\n" + - "AS $$" + - " lst.get(1).getString(\"txt\");$$;"); + "CREATE FUNCTION %s( lst list> ) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE javascript\n" + + "AS $$" + + " lst.get(1).getString(\"txt\");$$;"); createFunctionOverload(fName, "set>", "CREATE FUNCTION %s( st set> ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS text " + "LANGUAGE javascript\n" + "AS $$" + " st.iterator().next().getString(\"txt\");$$;"); createFunctionOverload(fName, "map>", "CREATE FUNCTION %s( mp map> ) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS text " + "LANGUAGE javascript\n" + "AS $$" + @@ -1595,7 +1759,9 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE, "double", "CREATE OR REPLACE FUNCTION %s(val double) " + - "RETURNS double LANGUAGE javascript\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE javascript\n" + "AS '" + functionBody + "';"); FunctionName fNameName = parseFunctionName(fName); @@ -1621,6 +1787,7 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE, "double", "CREATE OR REPLACE FUNCTION %s(val double) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS double " + "LANGUAGE javascript\n" + "AS '\"string\";';"); @@ -1637,6 +1804,7 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE, "double", "CREATE OR REPLACE FUNCTION %s(val double) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS double " + "LANGUAGE javascript\n" + "AS 'throw \"fool\";';"); @@ -1652,7 +1820,9 @@ public class UFTest extends CQLTester { assertInvalidMessage("duplicate argument names for given function", "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".scrinv(val double, val text) " + - "RETURNS text LANGUAGE javascript\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE javascript\n" + "AS '\"foo bar\";';"); } @@ -1661,7 +1831,9 @@ public class UFTest extends CQLTester { assertInvalidMessage("Failed to compile function 'cql_test_keyspace.scrinv'", "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".scrinv(val double) " + - "RETURNS double LANGUAGE javascript\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE javascript\n" + "AS 'foo bar';"); } @@ -1670,7 +1842,9 @@ public class UFTest extends CQLTester { assertInvalidMessage("Invalid language 'artificial_intelligence' for function 'cql_test_keyspace.scrinv'", "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".scrinv(val double) " + - "RETURNS double LANGUAGE artificial_intelligence\n" + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE artificial_intelligence\n" + "AS 'question for 42?';"); } @@ -1703,6 +1877,7 @@ public class UFTest extends CQLTester String fName = createFunction(KEYSPACE, "double", "CREATE OR REPLACE FUNCTION %s(val double) " + + "RETURNS NULL ON NULL INPUT " + "RETURNS " +returnType + ' ' + "LANGUAGE javascript " + "AS '" + functionBody + ";';"); @@ -1735,6 +1910,7 @@ public class UFTest extends CQLTester Object expected2 = variation[3]; String fName = createFunction(KEYSPACE, type.toString(), "CREATE OR REPLACE FUNCTION %s(val " + type + ") " + + "RETURNS NULL ON NULL INPUT " + "RETURNS " + type + ' ' + "LANGUAGE javascript " + "AS 'val+1;';"); @@ -1743,6 +1919,197 @@ public class UFTest extends CQLTester } } + @Test + public void testNullOnReturnsNullOnNullInput() throws Throwable + { + String type = createType("CREATE TYPE %s (txt text, i int)"); + createTable("CREATE TABLE %s (key int PRIMARY KEY, i int, b bigint, f float, d double, x boolean, t text, u frozen<"+type+">, tup frozen>)"); + + execute("INSERT INTO %s (key, i, b, f, d, x, t, u, tup) VALUES (1, null, null, null, null, null, null, null, null)"); + + String fI = createFunction(KEYSPACE, + "int", + "CREATE OR REPLACE FUNCTION %s(val int) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fB = createFunction(KEYSPACE, + "bigint", + "CREATE OR REPLACE FUNCTION %s(val bigint) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fF = createFunction(KEYSPACE, + "float", + "CREATE OR REPLACE FUNCTION %s(val float) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fD = createFunction(KEYSPACE, + "double", + "CREATE OR REPLACE FUNCTION %s(val double) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fX = createFunction(KEYSPACE, + "boolean", + "CREATE OR REPLACE FUNCTION %s(val boolean) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fT = createFunction(KEYSPACE, + "text", + "CREATE OR REPLACE FUNCTION %s(val text) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fU = createFunction(KEYSPACE, + "frozen<" + type + '>', + "CREATE OR REPLACE FUNCTION %s(val frozen<" + type + ">) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fTup = createFunction(KEYSPACE, + "frozen>", + "CREATE OR REPLACE FUNCTION %s(val frozen>) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + + assertRows(execute("SELECT " + fI + "(i) FROM %s WHERE key=1"), row(new Object[]{null})); + assertRows(execute("SELECT " + fB + "(b) FROM %s WHERE key=1"), row(new Object[]{null})); + assertRows(execute("SELECT " + fF + "(f) FROM %s WHERE key=1"), row(new Object[]{null})); + assertRows(execute("SELECT " + fD + "(d) FROM %s WHERE key=1"), row(new Object[]{null})); + assertRows(execute("SELECT " + fX + "(x) FROM %s WHERE key=1"), row(new Object[]{null})); + assertRows(execute("SELECT " + fT + "(t) FROM %s WHERE key=1"), row(new Object[]{null})); + assertRows(execute("SELECT " + fU + "(u) FROM %s WHERE key=1"), row(new Object[]{null})); + assertRows(execute("SELECT " + fTup + "(tup) FROM %s WHERE key=1"), row(new Object[]{null})); + } + + @Test + public void testReplaceAllowNulls() throws Throwable + { + String fNulls = createFunction(KEYSPACE, + "int", + "CREATE OR REPLACE FUNCTION %s(val int) " + + "CALLED ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fNoNulls = createFunction(KEYSPACE, + "int", + "CREATE OR REPLACE FUNCTION %s(val int) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + + assertInvalid("CREATE OR REPLACE FUNCTION " + fNulls + "(val int) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + assertInvalid("CREATE OR REPLACE FUNCTION " + fNoNulls + "(val int) " + + "CALLED ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + + execute("CREATE OR REPLACE FUNCTION " + fNulls + "(val int) " + + "CALLED ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + execute("CREATE OR REPLACE FUNCTION " + fNoNulls + "(val int) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + } + + @Test + public void testNullOnCalledOnNullInput() throws Throwable + { + String type = createType("CREATE TYPE %s (txt text, i int)"); + createTable("CREATE TABLE %s (key int PRIMARY KEY, i int, b bigint, f float, d double, x boolean, t text, u frozen<"+type+">, tup frozen>)"); + + execute("INSERT INTO %s (key, i, b, f, d, x, t, u, tup) VALUES (1, null, null, null, null, null, null, null, null)"); + + String fI = createFunction(KEYSPACE, + "int", + "CREATE OR REPLACE FUNCTION %s(val int) " + + "CALLED ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fB = createFunction(KEYSPACE, + "bigint", + "CREATE OR REPLACE FUNCTION %s(val bigint) " + + "CALLED ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fF = createFunction(KEYSPACE, + "float", + "CREATE OR REPLACE FUNCTION %s(val float) " + + "CALLED ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fD = createFunction(KEYSPACE, + "double", + "CREATE OR REPLACE FUNCTION %s(val double) " + + "CALLED ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fX = createFunction(KEYSPACE, + "boolean", + "CREATE OR REPLACE FUNCTION %s(val boolean) " + + "CALLED ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fT = createFunction(KEYSPACE, + "text", + "CREATE OR REPLACE FUNCTION %s(val text) " + + "CALLED ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fU = createFunction(KEYSPACE, + "frozen<" + type + '>', + "CREATE OR REPLACE FUNCTION %s(val frozen<" + type + ">) " + + "CALLED ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + String fTup = createFunction(KEYSPACE, + "frozen>", + "CREATE OR REPLACE FUNCTION %s(val frozen>) " + + "CALLED ON NULL INPUT " + + "RETURNS text " + + "LANGUAGE java\n" + + "AS 'return \"foo bar\";';"); + + assertRows(execute("SELECT " + fI + "(i) FROM %s WHERE key=1"), row("foo bar")); + assertRows(execute("SELECT " + fB + "(b) FROM %s WHERE key=1"), row("foo bar")); + assertRows(execute("SELECT " + fF + "(f) FROM %s WHERE key=1"), row("foo bar")); + assertRows(execute("SELECT " + fD + "(d) FROM %s WHERE key=1"), row("foo bar")); + assertRows(execute("SELECT " + fX + "(x) FROM %s WHERE key=1"), row("foo bar")); + assertRows(execute("SELECT " + fT + "(t) FROM %s WHERE key=1"), row("foo bar")); + assertRows(execute("SELECT " + fU + "(u) FROM %s WHERE key=1"), row("foo bar")); + assertRows(execute("SELECT " + fTup + "(tup) FROM %s WHERE key=1"), row("foo bar")); + } + @Test public void testBrokenFunction() throws Throwable { @@ -1750,12 +2117,15 @@ public class UFTest extends CQLTester execute("INSERT INTO %s (key, dval) VALUES (?, ?)", 1, 1d); String fName = createFunction(KEYSPACE_PER_TEST, "double", - "CREATE OR REPLACE FUNCTION %s(val double) RETURNS double LANGUAGE JAVA\n" + + "CREATE OR REPLACE FUNCTION %s(val double) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE JAVA\n" + "AS 'throw new RuntimeException();';"); UDFunction f = (UDFunction) Functions.find(parseFunctionName(fName)).get(0); - Functions.replaceFunction(UDFunction.createBrokenFunction(f.name(), f.argNames(), f.argTypes(), f.returnType(), + Functions.replaceFunction(UDFunction.createBrokenFunction(f.name(), f.argNames(), f.argTypes(), f.returnType(), true, "java", f.body(), new InvalidRequestException("foo bar is broken"))); assertInvalidThrowMessage("foo bar is broken", InvalidRequestException.class, @@ -1770,7 +2140,10 @@ public class UFTest extends CQLTester execute("INSERT INTO %s (key, dval) VALUES (?, ?)", 1, 1d); String fName = createFunction(KEYSPACE_PER_TEST, "double", - "CREATE OR REPLACE FUNCTION %s(val double) RETURNS double LANGUAGE JAVA\n" + + "CREATE OR REPLACE FUNCTION %s(val double) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE JAVA\n" + "AS 'throw new RuntimeException()';"); for (int version = Server.VERSION_2; version <= maxProtocolVersion; version++)