/*
 *
 * 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.selection;

import java.util.ArrayList;
import java.util.List;

import org.junit.BeforeClass;
import org.junit.Test;

import org.apache.cassandra.ServerTestUtils;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.db.marshal.*;
import org.apache.cassandra.dht.ByteOrderedPartitioner;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.utils.ByteBufferUtil;

import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class SelectionColumnMappingTest extends CQLTester
{
    private static final ColumnMetadata NULL_DEF = null;
    String tableName;
    String typeName;
    UserType userType;
    String functionName;

    @BeforeClass
    public static void setUpClass()     // overrides CQLTester.setUpClass()
    {
        ServerTestUtils.daemonInitialization();

        DatabaseDescriptor.setPartitionerUnsafe(ByteOrderedPartitioner.instance);

        prepareServer();
    }

    @Test
    public void testSelectionColumnMapping() throws Throwable
    {
        // Organised as a single test to avoid the overhead of
        // table creation for each variant

        typeName = createType("CREATE TYPE %s (f1 int, f2 text)");
        tableName = createTable("CREATE TABLE %s (" +
                                " k int PRIMARY KEY," +
                                " v1 int," +
                                " v2 ascii," +
                                " v3 frozen<" + typeName + ">)");
        userType = Schema.instance.getKeyspaceMetadata(KEYSPACE).types.get(ByteBufferUtil.bytes(typeName)).get().freeze();
        functionName = createFunction(KEYSPACE, "int, ascii",
                                      "CREATE FUNCTION %s (i int, a ascii) " +
                                      "CALLED ON NULL INPUT " +
                                      "RETURNS int " +
                                      "LANGUAGE java " +
                                      "AS 'return Integer.valueOf(i);'");
        execute("INSERT INTO %s (k, v1 ,v2, v3) VALUES (1, 1, 'foo', {f1:1, f2:'bar'})");

        testSimpleTypes();
        testWildcard();
        testSimpleTypesWithAliases();
        testUserTypes();
        testUserTypesWithAliases();
        testWritetimeAndTTL();
        testWritetimeAndTTLWithAliases();
        testFunction();
        testNoArgFunction();
        testUserDefinedFunction();
        testOverloadedFunction();
        testFunctionWithAlias();
        testNoArgumentFunction();
        testNestedFunctions();
        testNestedFunctionsWithArguments();
        testCount();
        testDuplicateFunctionsWithoutAliases();
        testDuplicateFunctionsWithAliases();
        testSelectDistinct();
        testMultipleAliasesOnSameColumn();
        testMixedColumnTypes();
        testMultipleUnaliasedSelectionOfSameColumn();
        testUserDefinedAggregate();
        testListLitteral();
        testEmptyListLitteral();
        testSetLitteral();
        testEmptySetLitteral();
        testMapLitteral();
        testEmptyMapLitteral();
        testUDTLitteral();
        testTupleLitteral();
    }

    @Test
    public void testMultipleArgumentFunction() throws Throwable
    {
        // demonstrate behaviour of token() with composite partition key
        tableName = createTable("CREATE TABLE %s (a int, b text, PRIMARY KEY ((a, b)))");
        ColumnSpecification tokenSpec = columnSpecification("system.token(a, b)", BytesType.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(tokenSpec, columnDefinitions("a", "b"));
        // we don't use verify like with the other tests because this query will produce no results
        SelectStatement statement = getSelect("SELECT token(a,b) FROM %s");
        verifyColumnMapping(expected, statement);
        statement.executeLocally(QueryState.forInternalCalls(), QueryOptions.DEFAULT);
    }

    private void testSimpleTypes() throws Throwable
    {
        // simple column identifiers without aliases are represented in
        // ResultSet.Metadata by the underlying ColumnMetadata
        ColumnSpecification kSpec = columnSpecification("k", Int32Type.instance);
        ColumnSpecification v1Spec = columnSpecification("v1", Int32Type.instance);
        ColumnSpecification v2Spec = columnSpecification("v2", AsciiType.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(kSpec, columnDefinition("k"))
                                                                .addMapping(v1Spec, columnDefinition("v1"))
                                                                .addMapping(v2Spec, columnDefinition("v2"));

        verify(expected, "SELECT k, v1, v2 FROM %s");
    }

    private void testWildcard() throws Throwable
    {
        // Wildcard select represents each column in the table with a ColumnMetadata
        // in the ResultSet metadata
        ColumnMetadata kSpec = columnDefinition("k");
        ColumnMetadata v1Spec = columnDefinition("v1");
        ColumnMetadata v2Spec = columnDefinition("v2");
        ColumnMetadata v3Spec = columnDefinition("v3");
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(kSpec, columnDefinition("k"))
                                                                .addMapping(v1Spec, columnDefinition("v1"))
                                                                .addMapping(v2Spec, columnDefinition("v2"))
                                                                .addMapping(v3Spec, columnDefinition("v3"));

        verify(expected, "SELECT * FROM %s");
    }

    private void testSimpleTypesWithAliases() throws Throwable
    {
        // simple column identifiers with aliases are represented in ResultSet.Metadata
        // by a ColumnSpecification based on the underlying ColumnMetadata
        ColumnSpecification kSpec = columnSpecification("k_alias", Int32Type.instance);
        ColumnSpecification v1Spec = columnSpecification("v1_alias", Int32Type.instance);
        ColumnSpecification v2Spec = columnSpecification("v2_alias", AsciiType.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(kSpec, columnDefinition("k"))
                                                                .addMapping(v1Spec, columnDefinition("v1"))
                                                                .addMapping(v2Spec, columnDefinition("v2"));
        verify(expected, "SELECT k AS k_alias, v1 AS v1_alias, v2 AS v2_alias FROM %s");
    }

    private void testUserTypes() throws Throwable
    {
        // User type fields are represented in ResultSet.Metadata by a
        // ColumnSpecification denoting the name and type of the particular field
        ColumnSpecification f1Spec = columnSpecification("v3.f1", Int32Type.instance);
        ColumnSpecification f2Spec = columnSpecification("v3.f2", UTF8Type.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(f1Spec, columnDefinition("v3"))
                                                                .addMapping(f2Spec, columnDefinition("v3"));

        verify(expected, "SELECT v3.f1, v3.f2 FROM %s");
    }

    private void testUserTypesWithAliases() throws Throwable
    {
        // User type fields with aliases are represented in ResultSet.Metadata
        // by a ColumnSpecification with the alias name and the type of the actual field
        ColumnSpecification f1Spec = columnSpecification("f1_alias", Int32Type.instance);
        ColumnSpecification f2Spec = columnSpecification("f2_alias", UTF8Type.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(f1Spec, columnDefinition("v3"))
                                                                .addMapping(f2Spec, columnDefinition("v3"));

        verify(expected, "SELECT v3.f1 AS f1_alias, v3.f2 AS f2_alias FROM %s");
    }

    private void testWritetimeAndTTL() throws Throwable
    {
        // writetime and ttl are represented in ResultSet.Metadata by a ColumnSpecification
        // with the function name plus argument and a long or int type respectively
        ColumnSpecification wtSpec = columnSpecification("writetime(v1)", LongType.instance);
        ColumnSpecification ttlSpec = columnSpecification("ttl(v2)", Int32Type.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(wtSpec, columnDefinition("v1"))
                                                                .addMapping(ttlSpec, columnDefinition("v2"));

        verify(expected, "SELECT writetime(v1), ttl(v2) FROM %s");
    }

    private void testWritetimeAndTTLWithAliases() throws Throwable
    {
        // writetime and ttl with aliases are represented in ResultSet.Metadata
        // by a ColumnSpecification with the alias name and the appropriate numeric type
        ColumnSpecification wtSpec = columnSpecification("wt_alias", LongType.instance);
        ColumnSpecification ttlSpec = columnSpecification("ttl_alias", Int32Type.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(wtSpec, columnDefinition("v1"))
                                                                .addMapping(ttlSpec, columnDefinition("v2"));

        verify(expected, "SELECT writetime(v1) AS wt_alias, ttl(v2) AS ttl_alias FROM %s");
    }

    private void testFunction() throws Throwable
    {
        // a function such as int_as_blob(<col>) is represented in ResultSet.Metadata
        // by a ColumnSpecification with the function name plus args and the type set
        // to the function's return type
        ColumnSpecification fnSpec = columnSpecification("system.int_as_blob(v1)", BytesType.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(fnSpec, columnDefinition("v1"));

        verify(expected, "SELECT int_as_blob(v1) FROM %s");
    }

    private void testNoArgFunction() throws Throwable
    {
        // a no-arg function such as now() is represented in ResultSet.Metadata
        // but has no mapping to any underlying column
        ColumnSpecification fnSpec = columnSpecification("system.now()", TimeUUIDType.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping().addMapping(fnSpec, NULL_DEF);

        verify(expected, "SELECT now() FROM %s");
    }

    private void testOverloadedFunction() throws Throwable
    {
        String fnName = createFunction(KEYSPACE, "int",
                                       "CREATE FUNCTION %s (input int) " +
                                       "RETURNS NULL ON NULL INPUT " +
                                       "RETURNS text " +
                                       "LANGUAGE java " +
                                       "AS 'return \"Hello World\";'");
        createFunctionOverload(fnName, "text",
                               "CREATE FUNCTION %s (input text) " +
                               "RETURNS NULL ON NULL INPUT " +
                               "RETURNS text " +
                               "LANGUAGE java " +
                               "AS 'return \"Hello World\";'");

        createFunctionOverload(fnName, "int, text",
                               "CREATE FUNCTION %s (input1 int, input2 text) " +
                               "RETURNS NULL ON NULL INPUT " +
                               "RETURNS text " +
                               "LANGUAGE java " +
                               "AS 'return \"Hello World\";'");
        ColumnSpecification fnSpec1 = columnSpecification(fnName + "(v1)", UTF8Type.instance);
        ColumnSpecification fnSpec2 = columnSpecification(fnName + "(v2)", UTF8Type.instance);
        ColumnSpecification fnSpec3 = columnSpecification(fnName + "(v1, v2)", UTF8Type.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(fnSpec1, columnDefinition("v1"))
                                                                .addMapping(fnSpec2, columnDefinition("v2"))
                                                                .addMapping(fnSpec3, columnDefinitions("v1", "v2"));

        verify(expected, String.format("SELECT %1$s(v1), %1$s(v2), %1$s(v1, v2) FROM %%s", fnName));
    }

    private void testCount() throws Throwable
    {
        // SELECT COUNT does not necessarily include any mappings, but it must always return
        // a singleton list from getColumnSpecifications() in order for the ResultSet.Metadata
        // to be constructed correctly:
        // * COUNT(*) / COUNT(1) do not generate any mappings, as no specific columns are referenced
        // * COUNT(foo) does generate a mapping from the 'system.count' column spec to foo
        ColumnSpecification count = columnSpecification("count", LongType.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping().addMapping(count, NULL_DEF);

        verify(expected, "SELECT COUNT(*) FROM %s");
        verify(expected, "SELECT COUNT(1) FROM %s");

        ColumnSpecification aliased = columnSpecification("count_alias", LongType.instance);
        expected = SelectionColumnMapping.newMapping().addMapping(aliased, NULL_DEF);

        verify(expected, "SELECT COUNT(*) AS count_alias FROM %s");
        verify(expected, "SELECT COUNT(1) AS count_alias FROM %s");

        ColumnSpecification countV1 = columnSpecification("system.count(v1)", LongType.instance);
        expected = SelectionColumnMapping.newMapping().addMapping(countV1, columnDefinition("v1"));
        verify(expected, "SELECT COUNT(v1) FROM %s");

        ColumnSpecification countV1Alias = columnSpecification("count_v1", LongType.instance);
        expected = SelectionColumnMapping.newMapping().addMapping(countV1Alias, columnDefinition("v1"));
        verify(expected, "SELECT COUNT(v1) AS count_v1 FROM %s");
    }

    private void testUserDefinedFunction() throws Throwable
    {
        // UDFs are basically represented in the same way as system functions
        String functionCall = String.format("%s(v1, v2)", functionName);
        ColumnSpecification fnSpec = columnSpecification(functionCall, Int32Type.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(fnSpec, columnDefinitions("v1", "v2"));
        verify(expected, "SELECT " + functionCall + " FROM %s");
    }

    private void testFunctionWithAlias() throws Throwable
    {
        // a function with an alias is represented in ResultSet.Metadata by a
        // ColumnSpecification with the alias and the type set to the function's
        // return type
        ColumnSpecification fnSpec = columnSpecification("fn_alias", BytesType.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(fnSpec, columnDefinition("v1"));

        verify(expected, "SELECT int_as_blob(v1) AS fn_alias FROM %s");
    }

    public void testNoArgumentFunction() throws Throwable
    {
        SelectionColumns expected = SelectionColumnMapping.newMapping()
                                                          .addMapping(columnSpecification("system.now()",
                                                                                          TimeUUIDType.instance),
                                                                      NULL_DEF);
        verify(expected, "SELECT now() FROM %s");
    }

    public void testNestedFunctionsWithArguments() throws Throwable
    {
        SelectionColumns expected = SelectionColumnMapping.newMapping()
                                                          .addMapping(columnSpecification("system.blob_as_int(system.int_as_blob(v1))",
                                                                                          Int32Type.instance),
                                                                      columnDefinition("v1"));
        verify(expected, "SELECT blob_as_int(int_as_blob(v1)) FROM %s");
    }

    public void testNestedFunctions() throws Throwable
    {
        SelectionColumns expected = SelectionColumnMapping.newMapping()
                                                          .addMapping(columnSpecification("system.to_unix_timestamp(system.now())",
                                                                                          LongType.instance),
                                                                      NULL_DEF);
        verify(expected, "SELECT to_unix_timestamp(now()) FROM %s");
    }

    public void testDuplicateFunctionsWithoutAliases() throws Throwable
    {
        // where duplicate functions are present, the ColumnSpecification list will
        // contain an entry per-duplicate but the mappings will be deduplicated (i.e.
        // a single mapping k/v pair regardless of the number of duplicates)
        ColumnSpecification spec = columnSpecification("system.int_as_blob(v1)", BytesType.instance);
        SelectionColumns expected = SelectionColumnMapping.newMapping()
                                                          .addMapping(spec, columnDefinition("v1"))
                                                          .addMapping(spec, columnDefinition("v1"));
        verify(expected, "SELECT int_as_blob(v1), int_as_blob(v1) FROM %s");
    }

    public void testDuplicateFunctionsWithAliases() throws Throwable
    {
        // where duplicate functions are present with distinct aliases, they are
        // represented as any other set of distinct columns would be - an entry
        // in theColumnSpecification list and a separate k/v mapping for each
        SelectionColumns expected = SelectionColumnMapping.newMapping()
                                                          .addMapping(columnSpecification("blob_1", BytesType.instance),
                                                                      columnDefinition("v1"))
                                                          .addMapping(columnSpecification("blob_2", BytesType.instance),
                                                                      columnDefinition("v1"));
        verify(expected, "SELECT int_as_blob(v1) AS blob_1, int_as_blob(v1) AS blob_2 FROM %s");
    }

    public void testSelectDistinct() throws Throwable
    {
        SelectionColumns expected = SelectionColumnMapping.newMapping().addMapping(columnSpecification("k",
                                                                                                       Int32Type.instance),
                                                                                   columnDefinition("k"));
        verify(expected, "SELECT DISTINCT k FROM %s");

    }

    private void testMultipleAliasesOnSameColumn() throws Throwable
    {
        // Multiple result columns derived from the same underlying column are
        // represented by ColumnSpecifications
        ColumnSpecification alias1 = columnSpecification("alias_1", Int32Type.instance);
        ColumnSpecification alias2 = columnSpecification("alias_2", Int32Type.instance);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(alias1, columnDefinition("v1"))
                                                                .addMapping(alias2, columnDefinition("v1"));

        verify(expected, "SELECT v1 AS alias_1, v1 AS alias_2 FROM %s");
    }

    private void testMultipleUnaliasedSelectionOfSameColumn() throws Throwable
    {
        // simple column identifiers without aliases are represented in
        // ResultSet.Metadata by the underlying ColumnMetadata
        SelectionColumns expected = SelectionColumnMapping.newMapping()
                                                          .addMapping(columnSpecification("v1", Int32Type.instance),
                                                                      columnDefinition("v1"))
                                                          .addMapping(columnSpecification("v1", Int32Type.instance),
                                                                      columnDefinition("v1"));

        verify(expected, "SELECT v1, v1 FROM %s");
    }

    private void testListLitteral() throws Throwable
    {
        ColumnSpecification listSpec = columnSpecification("[k, v1]", ListType.getInstance(Int32Type.instance, false));
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(listSpec, asList(columnDefinition("k"),
                                                                                             columnDefinition("v1")));

        verify(expected, "SELECT [k, v1] FROM %s");
    }

    private void testEmptyListLitteral() throws Throwable
    {
        ColumnSpecification listSpec = columnSpecification("(list<int>)[]", ListType.getInstance(Int32Type.instance, false));
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(listSpec, (ColumnMetadata) null);

        verify(expected, "SELECT (list<int>)[] FROM %s");
    }

    private void testSetLitteral() throws Throwable
    {
        ColumnSpecification setSpec = columnSpecification("{k, v1}", SetType.getInstance(Int32Type.instance, false));
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(setSpec, asList(columnDefinition("k"),
                                                                                             columnDefinition("v1")));

        verify(expected, "SELECT {k, v1} FROM %s");
    }

    private void testEmptySetLitteral() throws Throwable
    {
        ColumnSpecification setSpec = columnSpecification("(set<int>){}", SetType.getInstance(Int32Type.instance, false));
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(setSpec, (ColumnMetadata) null);

        verify(expected, "SELECT (set<int>){} FROM %s");
    }

    private void testMapLitteral() throws Throwable
    {
        ColumnSpecification mapSpec = columnSpecification("(map<text, int>){'min': system.min(v1), 'max': system.max(v1)}", MapType.getInstance(UTF8Type.instance, Int32Type.instance, false));
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(mapSpec, asList(columnDefinition("v1")));

        verify(expected, "SELECT (map<text, int>){'min': min(v1), 'max': max(v1)} FROM %s");
    }

    private void testEmptyMapLitteral() throws Throwable
    {
        ColumnSpecification mapSpec = columnSpecification("(map<text, int>){}", MapType.getInstance(UTF8Type.instance, Int32Type.instance, false));
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(mapSpec, (ColumnMetadata) null);

        verify(expected, "SELECT (map<text, int>){} FROM %s");
    }

    private void testUDTLitteral() throws Throwable
    {
        UserType type = new UserType(KEYSPACE, ByteBufferUtil.bytes(typeName),
                                      asList(FieldIdentifier.forUnquoted("f1"),
                                             FieldIdentifier.forUnquoted("f2")),
                                      asList(Int32Type.instance,
                                             UTF8Type.instance),
                                      false);

        ColumnSpecification spec = columnSpecification("(" + KEYSPACE + "." + typeName + "){f1: v1, f2: v2}", type);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(spec, asList(columnDefinition("v1"),
                                                                                         columnDefinition("v2")));

        verify(expected, "SELECT ("+ typeName + "){f1: v1, f2: v2} FROM %s");
    }

    private void testTupleLitteral() throws Throwable
    {
        TupleType type = new TupleType(asList(Int32Type.instance, UTF8Type.instance));

        ColumnSpecification setSpec = columnSpecification("(tuple<int, text>)(v1, v2)", type);
        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(setSpec, asList(columnDefinition("v1"),
                                                                                            columnDefinition("v2")));

        verify(expected, "SELECT (tuple<int, text>)(v1, v2) FROM %s");
    }

    private void testMixedColumnTypes() throws Throwable
    {
        ColumnSpecification kSpec = columnSpecification("k_alias", Int32Type.instance);
        ColumnSpecification v1Spec = columnSpecification("writetime(v1)", LongType.instance);
        ColumnSpecification v2Spec = columnSpecification("ttl_alias", Int32Type.instance);
        ColumnSpecification f1Spec = columnSpecification("v3.f1", Int32Type.instance);
        ColumnSpecification f2Spec = columnSpecification("f2_alias", UTF8Type.instance);
        ColumnSpecification f3Spec = columnSpecification("v3", userType);

        SelectionColumnMapping expected = SelectionColumnMapping.newMapping()
                                                                .addMapping(kSpec, columnDefinition("k"))
                                                                .addMapping(v1Spec, columnDefinition("v1"))
                                                                .addMapping(v2Spec, columnDefinition("v2"))
                                                                .addMapping(f1Spec, columnDefinition("v3"))
                                                                .addMapping(f2Spec, columnDefinition("v3"))
                                                                .addMapping(f3Spec, columnDefinition("v3"));


        verify(expected, "SELECT k AS k_alias," +
                         "       writetime(v1)," +
                         "       ttl(v2) as ttl_alias," +
                         "       v3.f1," +
                         "       v3.f2 AS f2_alias," +
                         "       v3" +
                         " FROM %s");
    }

    private void testUserDefinedAggregate() throws Throwable
    {
        String sFunc = parseFunctionName(createFunction(KEYSPACE, "int",
                                                        " CREATE FUNCTION %s (a int, b int)" +
                                                        " RETURNS NULL ON NULL INPUT" +
                                                        " RETURNS int" +
                                                        " LANGUAGE java" +
                                                        " AS ' return a + b;'")).name;

        String aFunc = createAggregate(KEYSPACE, "int, int",
                                       " CREATE AGGREGATE %s (int)" +
                                       " SFUNC " + sFunc +
                                       " STYPE int" +
                                       " INITCOND 0");

        String plusOne = createFunction(KEYSPACE, "int",
                                        " CREATE FUNCTION %s (a int)" +
                                        " RETURNS NULL ON NULL INPUT" +
                                        " RETURNS int" +
                                        " LANGUAGE java" +
                                        " AS ' return a + 1;'");

        String sqFunc = createFunction(KEYSPACE, "int",
                                       " CREATE FUNCTION %s (a int)" +
                                       " RETURNS NULL ON NULL INPUT" +
                                       " RETURNS int" +
                                       " LANGUAGE java" +
                                       " AS ' return a * a;'");

        ColumnMetadata v1 = columnDefinition("v1");
        SelectionColumns expected = SelectionColumnMapping.newMapping()
                                                          .addMapping(columnSpecification(aFunc + "(v1)",
                                                                                          Int32Type.instance),
                                                                      v1);
        verify(expected, String.format("SELECT %s(v1) FROM %%s", aFunc));

        // aggregate with nested udfs as input
        String specName = String.format("%s(%s(%s(v1)))", aFunc, sqFunc, plusOne);
        expected = SelectionColumnMapping.newMapping().addMapping(columnSpecification(specName, Int32Type.instance),
                                                                  v1);
        verify(expected, String.format("SELECT %s FROM %%s", specName));
    }

    private void verify(SelectionColumns expected, String query) throws Throwable
    {
        SelectStatement statement = getSelect(query);
        verifyColumnMapping(expected, statement);
        checkExecution(statement, expected.getColumnSpecifications());
    }

    private void checkExecution(SelectStatement statement, List<ColumnSpecification> expectedResultColumns)
    throws RequestExecutionException, RequestValidationException
    {
        UntypedResultSet rs = UntypedResultSet.create(statement.executeLocally(QueryState.forInternalCalls(),
                                                                               QueryOptions.DEFAULT).result);

        assertEquals(expectedResultColumns, rs.one().getColumns());
    }

    private SelectStatement getSelect(String query) throws RequestValidationException
    {
        CQLStatement statement = QueryProcessor.getStatement(String.format(query, KEYSPACE + "." + tableName),
                                                             ClientState.forInternalCalls());
        assertTrue(statement instanceof SelectStatement);
        return (SelectStatement)statement;
    }

    private void verifyColumnMapping(SelectionColumns expected, SelectStatement select)
    {
        assertEquals(expected, select.getSelection().getColumnMapping());
    }

    private Iterable<ColumnMetadata> columnDefinitions(String...names)
    {
        List<ColumnMetadata> defs = new ArrayList<>();
        for (String n : names)
            defs.add(columnDefinition(n));
        return defs;
    }

    private ColumnMetadata columnDefinition(String name)
    {
        return Schema.instance.getTableMetadata(KEYSPACE, tableName).getColumn(new ColumnIdentifier(name, true));

    }

    private ColumnSpecification columnSpecification(String name, AbstractType<?> type)
    {
        return new ColumnSpecification(KEYSPACE,
                                       tableName,
                                       new ColumnIdentifier(name, true),
                                       type);
    }
}
