All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.cassandra.cql3.functions.Functions Maven / Gradle / Ivy

The newest version!
/*
 * 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> 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 functions = declared.get(fun.name());
            if (functions == null)
            {
                functions = new CopyOnWriteArrayList<>();
                List 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 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 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 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 find(FunctionName name)
    {
        List functions = declared.get(name);
        return functions != null ? functions : Collections.emptyList();
    }

    public static Function find(FunctionName name, List> 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 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 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 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> argTypes)
    {
        assert name.hasKeyspace() : "function name " + name + " not fully qualified";
        synchronized (declared)
        {
            List 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 getReferencesTo(Function old)
    {
        List references = new ArrayList<>();
        for (List functions : declared.values())
            for (Function function : functions)
                if (function.hasReferenceTo(old))
                    references.add(function);
        return references;
    }

    public static Collection all()
    {
        List all = new ArrayList<>();
        for (List 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> t1, List> 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> 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);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy