/* * 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.List; import com.google.common.collect.ArrayListMultimap; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.ColumnSpecification; import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.AssignementTestable; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.exceptions.InvalidRequestException; public abstract class Functions { private Functions() {} // If we ever allow this to be populated at runtime, this will need to be thread safe. private static final ArrayListMultimap<String, Function.Factory> declared = ArrayListMultimap.create(); static { // All method sharing the same name must have the same returnType. We could find a way to make that clear. declared.put("token", TokenFct.factory); declared.put("now", AbstractFunction.factory(TimeuuidFcts.nowFct)); declared.put("mintimeuuid", AbstractFunction.factory(TimeuuidFcts.minTimeuuidFct)); declared.put("maxtimeuuid", AbstractFunction.factory(TimeuuidFcts.maxTimeuuidFct)); declared.put("dateof", AbstractFunction.factory(TimeuuidFcts.dateOfFct)); declared.put("unixtimestampof", AbstractFunction.factory(TimeuuidFcts.unixTimestampOfFct)); declared.put("uuid", AbstractFunction.factory(UuidFcts.uuidFct)); for (CQL3Type type : CQL3Type.Native.values()) { // Note: because text and varchar ends up being synonimous, 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) continue; Function toBlob = BytesConversionFcts.makeToBlobFunction(type.getType()); Function fromBlob = BytesConversionFcts.makeFromBlobFunction(type.getType()); declared.put(toBlob.name(), AbstractFunction.factory(toBlob)); declared.put(fromBlob.name(), AbstractFunction.factory(fromBlob)); } declared.put("varcharasblob", AbstractFunction.factory(BytesConversionFcts.VarcharAsBlobFct)); declared.put("blobasvarchar", AbstractFunction.factory(BytesConversionFcts.BlobAsVarcharFact)); } public static AbstractType<?> getReturnType(String functionName, String ksName, String cfName) { List<Function.Factory> factories = declared.get(functionName.toLowerCase()); return factories.isEmpty() ? null // That's ok, we'll complain later : factories.get(0).create(ksName, cfName).returnType(); } public static ColumnSpecification makeArgSpec(ColumnSpecification receiver, Function fun, int i) { return new ColumnSpecification(receiver.ksName, receiver.cfName, new ColumnIdentifier("arg" + i + "(" + fun.name() + ")", true), fun.argsType().get(i)); } public static Function get(String keyspace, String name, List<? extends AssignementTestable> providedArgs, ColumnSpecification receiver) throws InvalidRequestException { List<Function.Factory> factories = declared.get(name.toLowerCase()); if (factories.isEmpty()) throw new InvalidRequestException(String.format("Unknown CQL3 function %s called", name)); // Fast path if there is not choice if (factories.size() == 1) { Function fun = factories.get(0).create(receiver.ksName, receiver.cfName); validateTypes(keyspace, fun, providedArgs, receiver); return fun; } Function candidate = null; for (Function.Factory factory : factories) { Function toTest = factory.create(receiver.ksName, receiver.cfName); if (!isValidType(keyspace, toTest, providedArgs, receiver)) continue; if (candidate == null) candidate = toTest; else throw new InvalidRequestException(String.format("Ambiguous call to function %s (can match both type signature %s and %s): use type casts to disambiguate", name, signature(candidate), signature(toTest))); } if (candidate == null) throw new InvalidRequestException(String.format("Invalid call to function %s, none of its type signature matches (known type signatures: %s)", name, signatures(factories, receiver))); return candidate; } private static void validateTypes(String keyspace, Function fun, List<? extends AssignementTestable> providedArgs, ColumnSpecification receiver) throws InvalidRequestException { if (!receiver.type.isValueCompatibleWith(fun.returnType())) throw new InvalidRequestException(String.format("Type error: cannot assign result of function %s (type %s) to %s (type %s)", fun.name(), fun.returnType().asCQL3Type(), receiver, receiver.type.asCQL3Type())); if (providedArgs.size() != fun.argsType().size()) throw new InvalidRequestException(String.format("Invalid number of arguments in call to function %s: %d required but %d provided", fun.name(), fun.argsType().size(), providedArgs.size())); for (int i = 0; i < providedArgs.size(); i++) { AssignementTestable 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(receiver, fun, i); if (!provided.isAssignableTo(keyspace, expected)) 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 boolean isValidType(String keyspace, Function fun, List<? extends AssignementTestable> providedArgs, ColumnSpecification receiver) throws InvalidRequestException { if (!receiver.type.isValueCompatibleWith(fun.returnType())) return false; if (providedArgs.size() != fun.argsType().size()) return false; for (int i = 0; i < providedArgs.size(); i++) { AssignementTestable 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(receiver, fun, i); if (!provided.isAssignableTo(keyspace, expected)) return false; } return true; } private static String signature(Function fun) { List<AbstractType<?>> args = fun.argsType(); StringBuilder sb = new StringBuilder(); sb.append("("); for (int i = 0; i < args.size(); i++) { if (i > 0) sb.append(", "); sb.append(args.get(i).asCQL3Type()); } sb.append(") -> "); sb.append(fun.returnType().asCQL3Type()); return sb.toString(); } private static String signatures(List<Function.Factory> factories, ColumnSpecification receiver) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < factories.size(); i++) { if (i > 0) sb.append(", "); sb.append(signature(factories.get(i).create(receiver.ksName, receiver.cfName))); } return sb.toString(); } }