blob: 0f1af19eab2142b33c559babf51d18d5636a54e3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.cql3.functions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.service.MigrationListener;
import org.apache.cassandra.service.MigrationManager;
public abstract class Functions
{
// We special case the token function because that's the only function whose argument types actually
// depend on the table on which the function is called. Because it's the sole exception, it's easier
// to handle it as a special case.
private static final FunctionName TOKEN_FUNCTION_NAME = FunctionName.nativeFunction("token");
private Functions() {}
private static final ConcurrentMap<FunctionName, List<Function>> declared = new ConcurrentHashMap<>();
static
{
declare(AggregateFcts.countRowsFunction);
declare(TimeFcts.nowFct);
declare(TimeFcts.minTimeuuidFct);
declare(TimeFcts.maxTimeuuidFct);
declare(TimeFcts.dateOfFct);
declare(TimeFcts.unixTimestampOfFct);
declare(TimeFcts.timeUuidtoDate);
declare(TimeFcts.timeUuidToTimestamp);
declare(TimeFcts.timeUuidToUnixTimestamp);
declare(TimeFcts.timestampToDate);
declare(TimeFcts.timestampToUnixTimestamp);
declare(TimeFcts.dateToTimestamp);
declare(TimeFcts.dateToUnixTimestamp);
declare(UuidFcts.uuidFct);
for (CQL3Type type : CQL3Type.Native.values())
{
// Note: because text and varchar ends up being synonymous, our automatic makeToBlobFunction doesn't work
// for varchar, so we special case it below. We also skip blob for obvious reasons.
if (type != CQL3Type.Native.VARCHAR && type != CQL3Type.Native.BLOB)
{
declare(BytesConversionFcts.makeToBlobFunction(type.getType()));
declare(BytesConversionFcts.makeFromBlobFunction(type.getType()));
}
}
declare(BytesConversionFcts.VarcharAsBlobFct);
declare(BytesConversionFcts.BlobAsVarcharFact);
for (CQL3Type type : CQL3Type.Native.values())
{
// special case varchar to avoid duplicating functions for UTF8Type
if (type != CQL3Type.Native.VARCHAR)
{
declare(AggregateFcts.makeCountFunction(type.getType()));
declare(AggregateFcts.makeMaxFunction(type.getType()));
declare(AggregateFcts.makeMinFunction(type.getType()));
}
}
declare(AggregateFcts.sumFunctionForByte);
declare(AggregateFcts.sumFunctionForShort);
declare(AggregateFcts.sumFunctionForInt32);
declare(AggregateFcts.sumFunctionForLong);
declare(AggregateFcts.sumFunctionForFloat);
declare(AggregateFcts.sumFunctionForDouble);
declare(AggregateFcts.sumFunctionForDecimal);
declare(AggregateFcts.sumFunctionForVarint);
declare(AggregateFcts.sumFunctionForCounter);
declare(AggregateFcts.avgFunctionForByte);
declare(AggregateFcts.avgFunctionForShort);
declare(AggregateFcts.avgFunctionForInt32);
declare(AggregateFcts.avgFunctionForLong);
declare(AggregateFcts.avgFunctionForFloat);
declare(AggregateFcts.avgFunctionForDouble);
declare(AggregateFcts.avgFunctionForVarint);
declare(AggregateFcts.avgFunctionForDecimal);
declare(AggregateFcts.avgFunctionForCounter);
MigrationManager.instance.register(new FunctionsMigrationListener());
}
private static void declare(Function fun)
{
synchronized (declared)
{
List<Function> functions = declared.get(fun.name());
if (functions == null)
{
functions = new CopyOnWriteArrayList<>();
List<Function> existing = declared.putIfAbsent(fun.name(), functions);
if (existing != null)
functions = existing;
}
functions.add(fun);
}
}
public static ColumnSpecification makeArgSpec(String receiverKs, String receiverCf, Function fun, int i)
{
return new ColumnSpecification(receiverKs,
receiverCf,
new ColumnIdentifier("arg" + i + '(' + fun.name().toString().toLowerCase() + ')', true),
fun.argTypes().get(i));
}
public static int getOverloadCount(FunctionName name)
{
return find(name).size();
}
/**
* @param keyspace the current keyspace
* @param name the name of the function
* @param providedArgs the arguments provided for the function call
* @param receiverKs the receiver's keyspace
* @param receiverCf the receiver's table
* @param receiverType if the receiver type is known (during inserts, for example), this should be the type of
* the receiver
* @throws InvalidRequestException
*/
public static Function get(String keyspace,
FunctionName name,
List<? extends AssignmentTestable> providedArgs,
String receiverKs,
String receiverCf,
AbstractType<?> receiverType)
throws InvalidRequestException
{
if (name.equalsNativeFunction(TOKEN_FUNCTION_NAME))
return new TokenFct(Schema.instance.getCFMetaData(receiverKs, receiverCf));
// The toJson() function can accept any type of argument, so instances of it are not pre-declared. Instead,
// we create new instances as needed while handling selectors (which is the only place that toJson() is supported,
// due to needing to know the argument types in advance).
if (name.equalsNativeFunction(ToJsonFct.NAME))
throw new InvalidRequestException("toJson() may only be used within the selection clause of SELECT statements");
// Similarly, we can only use fromJson when we know the receiver type (such as inserts)
if (name.equalsNativeFunction(FromJsonFct.NAME))
{
if (receiverType == null)
throw new InvalidRequestException("fromJson() cannot be used in the selection clause of a SELECT statement");
return FromJsonFct.getInstance(receiverType);
}
List<Function> candidates;
if (!name.hasKeyspace())
{
// function name not fully qualified
candidates = new ArrayList<>();
// add 'SYSTEM' (native) candidates
candidates.addAll(find(name.asNativeFunction()));
// add 'current keyspace' candidates
candidates.addAll(find(new FunctionName(keyspace, name.name)));
}
else
// function name is fully qualified (keyspace + name)
candidates = find(name);
if (candidates.isEmpty())
return null;
// Fast path if there is only one choice
if (candidates.size() == 1)
{
Function fun = candidates.get(0);
validateTypes(keyspace, fun, providedArgs, receiverKs, receiverCf);
return fun;
}
List<Function> compatibles = null;
for (Function toTest : candidates)
{
AssignmentTestable.TestResult r = matchAguments(keyspace, toTest, providedArgs, receiverKs, receiverCf);
switch (r)
{
case EXACT_MATCH:
// We always favor exact matches
return toTest;
case WEAKLY_ASSIGNABLE:
if (compatibles == null)
compatibles = new ArrayList<>();
compatibles.add(toTest);
break;
}
}
if (compatibles == null || compatibles.isEmpty())
throw new InvalidRequestException(String.format("Invalid call to function %s, none of its type signatures match (known type signatures: %s)",
name, toString(candidates)));
if (compatibles.size() > 1)
throw new InvalidRequestException(String.format("Ambiguous call to function %s (can be matched by following signatures: %s): use type casts to disambiguate",
name, toString(compatibles)));
return compatibles.get(0);
}
public static List<Function> find(FunctionName name)
{
List<Function> functions = declared.get(name);
return functions != null ? functions : Collections.<Function>emptyList();
}
public static Function find(FunctionName name, List<AbstractType<?>> argTypes)
{
assert name.hasKeyspace() : "function name not fully qualified";
for (Function f : find(name))
{
if (typeEquals(f.argTypes(), argTypes))
return f;
}
return null;
}
// This method and matchArguments are somewhat duplicate, but this method allows us to provide more precise errors in the common
// case where there is no override for a given function. This is thus probably worth the minor code duplication.
private static void validateTypes(String keyspace,
Function fun,
List<? extends AssignmentTestable> providedArgs,
String receiverKs,
String receiverCf)
throws InvalidRequestException
{
if (providedArgs.size() != fun.argTypes().size())
throw new InvalidRequestException(String.format("Invalid number of arguments in call to function %s: %d required but %d provided", fun.name(), fun.argTypes().size(), providedArgs.size()));
for (int i = 0; i < providedArgs.size(); i++)
{
AssignmentTestable provided = providedArgs.get(i);
// If the concrete argument is a bind variables, it can have any type.
// We'll validate the actually provided value at execution time.
if (provided == null)
continue;
ColumnSpecification expected = makeArgSpec(receiverKs, receiverCf, fun, i);
if (!provided.testAssignment(keyspace, expected).isAssignable())
throw new InvalidRequestException(String.format("Type error: %s cannot be passed as argument %d of function %s of type %s", provided, i, fun.name(), expected.type.asCQL3Type()));
}
}
private static AssignmentTestable.TestResult matchAguments(String keyspace,
Function fun,
List<? extends AssignmentTestable> providedArgs,
String receiverKs,
String receiverCf)
{
if (providedArgs.size() != fun.argTypes().size())
return AssignmentTestable.TestResult.NOT_ASSIGNABLE;
// It's an exact match if all are exact match, but is not assignable as soon as any is non assignable.
AssignmentTestable.TestResult res = AssignmentTestable.TestResult.EXACT_MATCH;
for (int i = 0; i < providedArgs.size(); i++)
{
AssignmentTestable provided = providedArgs.get(i);
if (provided == null)
{
res = AssignmentTestable.TestResult.WEAKLY_ASSIGNABLE;
continue;
}
ColumnSpecification expected = makeArgSpec(receiverKs, receiverCf, fun, i);
AssignmentTestable.TestResult argRes = provided.testAssignment(keyspace, expected);
if (argRes == AssignmentTestable.TestResult.NOT_ASSIGNABLE)
return AssignmentTestable.TestResult.NOT_ASSIGNABLE;
if (argRes == AssignmentTestable.TestResult.WEAKLY_ASSIGNABLE)
res = AssignmentTestable.TestResult.WEAKLY_ASSIGNABLE;
}
return res;
}
private static String toString(List<Function> funs)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < funs.size(); i++)
{
if (i > 0) sb.append(", ");
sb.append(funs.get(i));
}
return sb.toString();
}
public static void addOrReplaceFunction(AbstractFunction fun)
{
// We shouldn't get there unless that function don't exist
removeFunction(fun.name(), fun.argTypes());
declare(fun);
}
// Same remarks than for addFunction
public static void removeFunction(FunctionName name, List<AbstractType<?>> argTypes)
{
assert name.hasKeyspace() : "function name " + name + " not fully qualified";
synchronized (declared)
{
List<Function> functions = find(name);
for (int i = 0; i < functions.size(); i++)
{
Function f = functions.get(i);
if (!typeEquals(f.argTypes(), argTypes))
continue;
assert !f.isNative();
functions.remove(i);
if (functions.isEmpty())
declared.remove(name);
return;
}
}
}
public static List<Function> getReferencesTo(Function old)
{
List<Function> references = new ArrayList<>();
for (List<Function> functions : declared.values())
for (Function function : functions)
if (function.hasReferenceTo(old))
references.add(function);
return references;
}
public static Collection<Function> all()
{
List<Function> all = new ArrayList<>();
for (List<Function> functions : declared.values())
all.addAll(functions);
return all;
}
/*
* We need to compare the CQL3 representation of the type because comparing
* the AbstractType will fail for example if a UDT has been changed.
* Reason is that UserType.equals() takes the field names and types into account.
* Example CQL sequence that would fail when comparing AbstractType:
* CREATE TYPE foo ...
* CREATE FUNCTION bar ( par foo ) RETURNS foo ...
* ALTER TYPE foo ADD ...
* or
* ALTER TYPE foo ALTER ...
* or
* ALTER TYPE foo RENAME ...
*/
public static boolean typeEquals(AbstractType<?> t1, AbstractType<?> t2)
{
return t1.asCQL3Type().toString().equals(t2.asCQL3Type().toString());
}
public static boolean typeEquals(List<AbstractType<?>> t1, List<AbstractType<?>> t2)
{
if (t1.size() != t2.size())
return false;
for (int i = 0; i < t1.size(); i ++)
if (!typeEquals(t1.get(i), t2.get(i)))
return false;
return true;
}
public static int typeHashCode(AbstractType<?> t)
{
return t.asCQL3Type().toString().hashCode();
}
public static int typeHashCode(List<AbstractType<?>> types)
{
int h = 0;
for (AbstractType<?> type : types)
h = h * 31 + typeHashCode(type);
return h;
}
private static class FunctionsMigrationListener extends MigrationListener
{
public void onUpdateUserType(String ksName, String typeName) {
for (Function function : all())
if (function instanceof UDFunction)
((UDFunction)function).userTypeUpdated(ksName, typeName);
}
}
}