/*
 * Licensed 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 com.facebook.presto.functionNamespace.mysql;

import com.facebook.airlift.bootstrap.Bootstrap;
import com.facebook.airlift.bootstrap.LifeCycleManager;
import com.facebook.drift.transport.netty.client.DriftNettyClientModule;
import com.facebook.presto.common.QualifiedObjectName;
import com.facebook.presto.common.type.TypeSignature;
import com.facebook.presto.functionNamespace.execution.SimpleAddressSqlFunctionExecutorsModule;
import com.facebook.presto.spi.ErrorCodeSupplier;
import com.facebook.presto.spi.PrestoException;
import com.facebook.presto.spi.function.AlterRoutineCharacteristics;
import com.facebook.presto.spi.function.FunctionHandle;
import com.facebook.presto.spi.function.FunctionMetadata;
import com.facebook.presto.spi.function.FunctionNamespaceTransactionHandle;
import com.facebook.presto.spi.function.Parameter;
import com.facebook.presto.spi.function.RoutineCharacteristics;
import com.facebook.presto.spi.function.SqlFunctionHandle;
import com.facebook.presto.spi.function.SqlFunctionId;
import com.facebook.presto.spi.function.SqlInvokedFunction;
import com.facebook.presto.testing.mysql.TestingMySqlServer;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Injector;
import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.Jdbi;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static com.facebook.airlift.testing.Closeables.closeQuietly;
import static com.facebook.presto.common.type.StandardTypes.DOUBLE;
import static com.facebook.presto.common.type.TypeSignature.parseTypeSignature;
import static com.facebook.presto.functionNamespace.testing.SqlInvokedFunctionTestUtils.FUNCTION_POWER_TOWER_DOUBLE;
import static com.facebook.presto.functionNamespace.testing.SqlInvokedFunctionTestUtils.FUNCTION_POWER_TOWER_DOUBLE_UPDATED;
import static com.facebook.presto.functionNamespace.testing.SqlInvokedFunctionTestUtils.FUNCTION_POWER_TOWER_INT;
import static com.facebook.presto.functionNamespace.testing.SqlInvokedFunctionTestUtils.FUNCTION_TANGENT;
import static com.facebook.presto.functionNamespace.testing.SqlInvokedFunctionTestUtils.POWER_TOWER;
import static com.facebook.presto.functionNamespace.testing.SqlInvokedFunctionTestUtils.TANGENT;
import static com.facebook.presto.functionNamespace.testing.SqlInvokedFunctionTestUtils.TEST_CATALOG;
import static com.facebook.presto.functionNamespace.testing.SqlInvokedFunctionTestUtils.TEST_SCHEMA;
import static com.facebook.presto.spi.StandardErrorCode.ALREADY_EXISTS;
import static com.facebook.presto.spi.function.FunctionKind.SCALAR;
import static com.facebook.presto.spi.function.FunctionVersion.notVersioned;
import static com.facebook.presto.spi.function.FunctionVersion.withVersion;
import static com.facebook.presto.spi.function.RoutineCharacteristics.Determinism.DETERMINISTIC;
import static com.facebook.presto.spi.function.RoutineCharacteristics.NullCallClause.CALLED_ON_NULL_INPUT;
import static com.facebook.presto.spi.function.RoutineCharacteristics.NullCallClause.RETURNS_NULL_ON_NULL_INPUT;
import static com.google.common.base.Throwables.throwIfUnchecked;
import static java.lang.String.format;
import static java.util.Collections.nCopies;
import static java.util.Comparator.comparing;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

@Test(singleThreaded = true)
public class TestMySqlFunctionNamespaceManager
{
    private static final String DB = "presto";

    private TestingMySqlServer mySqlServer;
    private Jdbi jdbi;
    private Injector injector;
    private MySqlFunctionNamespaceManager functionNamespaceManager;

    public Jdbi getJdbi()
    {
        return jdbi;
    }

    @BeforeClass
    public void setup()
            throws Exception
    {
        this.mySqlServer = new TestingMySqlServer("testuser", "testpass", DB);
        Bootstrap app = new Bootstrap(
                new MySqlFunctionNamespaceManagerModule(TEST_CATALOG),
                new SimpleAddressSqlFunctionExecutorsModule(),
                new DriftNettyClientModule(),
                new MySqlConnectionModule());

        Map<String, String> config = ImmutableMap.<String, String>builder()
                .put("function-cache-expiration", "0s")
                .put("function-instance-cache-expiration", "0s")
                .put("database-url", mySqlServer.getJdbcUrl(DB))
                .build();

        try {
            this.injector = app
                    .doNotInitializeLogging()
                    .setRequiredConfigurationProperties(config)
                    .initialize();
            this.functionNamespaceManager = injector.getInstance(MySqlFunctionNamespaceManager.class);
            this.jdbi = injector.getInstance(Jdbi.class);
        }
        catch (Exception e) {
            throwIfUnchecked(e);
            throw new RuntimeException(e);
        }
    }

    @BeforeMethod
    public void setupFunctionNamespace()
    {
        createFunctionNamespace(TEST_CATALOG, TEST_SCHEMA);
    }

    @AfterMethod
    public void cleanup()
    {
        try (Handle handle = jdbi.open()) {
            handle.execute("DELETE FROM sql_functions");
            handle.execute("DELETE FROM function_namespaces");
        }
    }

    @AfterClass(alwaysRun = true)
    public void tearDown()
    {
        closeQuietly(mySqlServer);
        if (injector != null) {
            injector.getInstance(LifeCycleManager.class).stop();
        }
    }

    @Test
    public void testListFunction()
    {
        createFunctionNamespace(TEST_CATALOG, "schema1");
        createFunctionNamespace(TEST_CATALOG, "schema2");
        SqlInvokedFunction function1 = constructTestFunction(QualifiedObjectName.valueOf(TEST_CATALOG, "schema1", "power_tower"));
        SqlInvokedFunction function2 = constructTestFunction(QualifiedObjectName.valueOf(TEST_CATALOG, "schema2", "power_tower"));

        createFunction(function1, false);
        createFunction(function2, false);
        assertListFunctions(function1.withVersion("1"), function2.withVersion("1"));
        assertListFunctions(Optional.of(format("%s.%%", TEST_CATALOG)), Optional.empty(), function1.withVersion("1"), function2.withVersion("1"));
        assertListFunctions(Optional.of(format("%s.%s.%%", TEST_CATALOG, "schema1")), Optional.empty(), function1.withVersion("1"));
        assertListFunctions(Optional.of("%schema%"), Optional.empty(), function1.withVersion("1"), function2.withVersion("1"));
        assertListFunctions(Optional.of("%power$_tower"), Optional.of("$"), function1.withVersion("1"), function2.withVersion("1"));
    }

    @Test
    public void testCreateFunction()
    {
        assertListFunctions();

        createFunction(FUNCTION_POWER_TOWER_DOUBLE, false);
        assertListFunctions(FUNCTION_POWER_TOWER_DOUBLE.withVersion("1"));

        createFunction(FUNCTION_POWER_TOWER_DOUBLE_UPDATED, true);
        assertListFunctions(FUNCTION_POWER_TOWER_DOUBLE_UPDATED.withVersion("2"));
        assertGetFunctions(POWER_TOWER, FUNCTION_POWER_TOWER_DOUBLE_UPDATED.withVersion("2"));

        createFunction(FUNCTION_POWER_TOWER_INT, true);
        assertListFunctions(FUNCTION_POWER_TOWER_DOUBLE_UPDATED.withVersion("2"), FUNCTION_POWER_TOWER_INT.withVersion("1"));
        assertGetFunctions(POWER_TOWER, FUNCTION_POWER_TOWER_DOUBLE_UPDATED.withVersion("2"), FUNCTION_POWER_TOWER_INT.withVersion("1"));

        createFunction(FUNCTION_TANGENT, true);
        assertListFunctions(FUNCTION_POWER_TOWER_DOUBLE_UPDATED.withVersion("2"), FUNCTION_POWER_TOWER_INT.withVersion("1"), FUNCTION_TANGENT.withVersion("1"));
        assertGetFunctions(POWER_TOWER, FUNCTION_POWER_TOWER_DOUBLE_UPDATED.withVersion("2"), FUNCTION_POWER_TOWER_INT.withVersion("1"));
        assertGetFunctions(TANGENT, FUNCTION_TANGENT.withVersion("1"));
    }

    @Test
    public void testCreateFunctionFailedDuplicate()
    {
        createFunction(FUNCTION_POWER_TOWER_DOUBLE, true);
        assertPrestoException(() -> createFunction(FUNCTION_POWER_TOWER_DOUBLE, false), ALREADY_EXISTS, ".*Function already exists: unittest\\.memory\\.power_tower\\(double\\)");
        assertPrestoException(() -> createFunction(FUNCTION_POWER_TOWER_DOUBLE_UPDATED, false), ALREADY_EXISTS, ".*Function already exists: unittest\\.memory\\.power_tower\\(double\\)");
    }

    public void testCreateFunctionRepeatedly()
    {
        createFunction(FUNCTION_POWER_TOWER_DOUBLE, false);
        assertListFunctions(FUNCTION_POWER_TOWER_DOUBLE.withVersion("1"));

        createFunction(FUNCTION_POWER_TOWER_DOUBLE, true);
        assertListFunctions(FUNCTION_POWER_TOWER_DOUBLE.withVersion("1"));
    }

    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = ".*function 'unittest\\.memory\\.power_tower\\(x double\\):double:1 \\[SCALAR] \\{RETURN pow\\(x, x\\)\\} \\(SQL, DETERMINISTIC, CALLED_ON_NULL_INPUT\\)' is already versioned")
    public void testCreateFunctionFailedVersioned()
    {
        createFunction(FUNCTION_POWER_TOWER_DOUBLE.withVersion("1"), true);
    }

    @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = "Schema name exceeds max length of 128.*")
    public void testSchemaNameTooLong()
    {
        QualifiedObjectName functionName = QualifiedObjectName.valueOf(TEST_CATALOG, dummyString(129), "tangent");
        createFunction(createFunctionTangent(functionName), false);
    }

    @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = "Function name exceeds max length of 256.*")
    public void testFunctionNameTooLong()
    {
        QualifiedObjectName functionName = QualifiedObjectName.valueOf(TEST_CATALOG, TEST_SCHEMA, dummyString(257));
        createFunction(createFunctionTangent(functionName), false);
    }

    @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = "Function has more than 100 parameters: 101")
    public void testTooManyParameters()
    {
        List<Parameter> parameters = nCopies(101, new Parameter("x", parseTypeSignature(DOUBLE)));
        createFunction(createFunctionTangent(parameters), false);
    }

    public void testLargeParameterCount()
    {
        List<Parameter> parameters = nCopies(100, new Parameter("x", parseTypeSignature(DOUBLE)));
        createFunction(createFunctionTangent(parameters), false);
    }

    @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = "Parameter name exceeds max length of 100.*")
    public void testParameterNameTooLong()
    {
        List<Parameter> parameters = ImmutableList.of(new Parameter(dummyString(101), parseTypeSignature(DOUBLE)));
        createFunction(createFunctionTangent(parameters), false);
    }

    public void testLongParameterName()
    {
        List<Parameter> parameters = ImmutableList.of(new Parameter(dummyString(100), parseTypeSignature(DOUBLE)));
        createFunction(createFunctionTangent(parameters), false);
    }

    @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = "Parameter type list exceeds max length of 30000.*")
    public void testParameterTypeListTooLong()
    {
        List<Parameter> parameters = ImmutableList.of(new Parameter("x", createLargeRowType(4286)));
        createFunction(createFunctionTangent(parameters), false);
    }

    public void testLongParameterTypeList()
    {
        List<Parameter> parameters = ImmutableList.of(new Parameter("x", createLargeRowType(4285)));
        createFunction(createFunctionTangent(parameters), false);
    }

    @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = "Return type exceeds max length of 30000.*")
    public void testReturnTypeTooLong()
    {
        TypeSignature returnType = parseTypeSignature(dummyString(30001));
        createFunction(createFunctionTangent(returnType), false);
    }

    public void testLongReturnType()
    {
        TypeSignature returnType = parseTypeSignature(dummyString(30000));
        createFunction(createFunctionTangent(returnType), false);
    }

    @Test
    public void testAlterFunction()
    {
        createFunction(FUNCTION_POWER_TOWER_DOUBLE, false);
        createFunction(FUNCTION_POWER_TOWER_INT, false);
        createFunction(FUNCTION_TANGENT, false);

        // Alter a specific function by name and parameter types
        alterFunction(POWER_TOWER, Optional.of(ImmutableList.of(parseTypeSignature(DOUBLE))), new AlterRoutineCharacteristics(Optional.of(RETURNS_NULL_ON_NULL_INPUT)));
        assertGetFunctions(
                POWER_TOWER,
                FUNCTION_POWER_TOWER_INT.withVersion("1"),
                new SqlInvokedFunction(
                        POWER_TOWER,
                        ImmutableList.of(new Parameter("x", parseTypeSignature(DOUBLE))),
                        parseTypeSignature(DOUBLE),
                        "power tower",
                        RoutineCharacteristics.builder().setDeterminism(DETERMINISTIC).setNullCallClause(RETURNS_NULL_ON_NULL_INPUT).build(),
                        "RETURN pow(x, x)",
                        withVersion("2")));

        // Drop function and alter function by name
        dropFunction(POWER_TOWER, Optional.of(ImmutableList.of(parseTypeSignature(DOUBLE))), false);
        alterFunction(POWER_TOWER, Optional.empty(), new AlterRoutineCharacteristics(Optional.of(CALLED_ON_NULL_INPUT)));

        // Alter function by name
        alterFunction(TANGENT, Optional.empty(), new AlterRoutineCharacteristics(Optional.of(CALLED_ON_NULL_INPUT)));
        SqlInvokedFunction tangentV2 = new SqlInvokedFunction(
                TANGENT,
                FUNCTION_TANGENT.getParameters(),
                FUNCTION_TANGENT.getSignature().getReturnType(),
                FUNCTION_TANGENT.getDescription(),
                RoutineCharacteristics.builder().setDeterminism(DETERMINISTIC).build(),
                FUNCTION_TANGENT.getBody(),
                withVersion("2"));
        assertGetFunctions(TANGENT, tangentV2);

        // Alter function with no change
        alterFunction(TANGENT, Optional.empty(), new AlterRoutineCharacteristics(Optional.of(CALLED_ON_NULL_INPUT)));
        assertGetFunctions(TANGENT, tangentV2);
    }

    @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = "Function 'unittest\\.memory\\.power_tower' has multiple signatures: unittest\\.memory\\.power_tower(\\(integer\\):integer|\\(double\\):double); unittest\\.memory\\.power_tower(\\(double\\):double|\\(integer\\):integer)\\. Please specify parameter types\\.")
    public void testAlterFunctionAmbiguous()
    {
        createFunction(FUNCTION_POWER_TOWER_DOUBLE, false);
        createFunction(FUNCTION_POWER_TOWER_INT, false);
        alterFunction(POWER_TOWER, Optional.empty(), new AlterRoutineCharacteristics(Optional.of(RETURNS_NULL_ON_NULL_INPUT)));
    }

    @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = "Function not found: unittest\\.memory\\.power_tower\\(\\)")
    public void testAlterFunctionNotFound()
    {
        createFunction(FUNCTION_POWER_TOWER_DOUBLE, false);
        createFunction(FUNCTION_POWER_TOWER_INT, false);
        alterFunction(POWER_TOWER, Optional.of(ImmutableList.of()), new AlterRoutineCharacteristics(Optional.of(RETURNS_NULL_ON_NULL_INPUT)));
    }

    @Test
    public void testDropFunction()
    {
        // Create functions
        createFunction(FUNCTION_POWER_TOWER_DOUBLE, false);
        createFunction(FUNCTION_POWER_TOWER_INT, false);
        createFunction(FUNCTION_TANGENT, false);

        // Drop function by name
        dropFunction(TANGENT, Optional.empty(), false);
        assertGetFunctions(TANGENT);

        // Recreate functions
        createFunction(FUNCTION_TANGENT, false);

        // Drop a specific function by name and parameter types
        dropFunction(POWER_TOWER, Optional.of(ImmutableList.of(parseTypeSignature(DOUBLE))), true);
        assertGetFunctions(POWER_TOWER, FUNCTION_POWER_TOWER_INT.withVersion("1"));

        dropFunction(TANGENT, Optional.of(ImmutableList.of(parseTypeSignature(DOUBLE))), true);
        assertGetFunctions(POWER_TOWER, FUNCTION_POWER_TOWER_INT.withVersion("1"));
        assertGetFunctions(TANGENT);

        // Recreate functions, power_double(double) is created with a different definition
        createFunction(FUNCTION_POWER_TOWER_DOUBLE_UPDATED, false);
        createFunction(FUNCTION_TANGENT, false);

        // Drop functions consecutively
        dropFunction(POWER_TOWER, Optional.of(ImmutableList.of(parseTypeSignature(DOUBLE))), false);
        dropFunction(POWER_TOWER, Optional.empty(), false);
    }

    public void testDropFunctionMultiple()
    {
        createFunction(FUNCTION_POWER_TOWER_DOUBLE, false);
        createFunction(FUNCTION_POWER_TOWER_INT, false);
        dropFunction(POWER_TOWER, Optional.empty(), false);
        assertGetFunctions(POWER_TOWER);
    }

    @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = "Function not found: unittest\\.memory\\.power_tower\\(\\)")
    public void testDropFunctionNotFound()
    {
        createFunction(FUNCTION_POWER_TOWER_DOUBLE, false);
        createFunction(FUNCTION_POWER_TOWER_INT, false);
        dropFunction(POWER_TOWER, Optional.of(ImmutableList.of()), true);
    }

    @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = "Function not found: unittest\\.memory\\.tangent\\(double\\)")
    public void testDropFunctionFailed()
    {
        dropFunction(TANGENT, Optional.of(ImmutableList.of(parseTypeSignature(DOUBLE))), false);
    }

    @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = "Function not found: unittest\\.memory\\.invalid")
    public void testDropFunctionsFailed()
    {
        dropFunction(QualifiedObjectName.valueOf(TEST_CATALOG, TEST_SCHEMA, "invalid"), Optional.empty(), false);
    }

    @Test
    public void testGetFunctionMetadata()
    {
        createFunction(FUNCTION_POWER_TOWER_DOUBLE, true);
        FunctionHandle handle1 = getLatestFunctionHandle(FUNCTION_POWER_TOWER_DOUBLE.getFunctionId());
        assertGetFunctionMetadata(handle1, FUNCTION_POWER_TOWER_DOUBLE);

        createFunction(FUNCTION_POWER_TOWER_DOUBLE_UPDATED, true);
        FunctionHandle handle2 = getLatestFunctionHandle(FUNCTION_POWER_TOWER_DOUBLE_UPDATED.getFunctionId());
        assertGetFunctionMetadata(handle1, FUNCTION_POWER_TOWER_DOUBLE);
        assertGetFunctionMetadata(handle2, FUNCTION_POWER_TOWER_DOUBLE_UPDATED);
    }

    @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = "Error getting FunctionMetadata for handle: unittest\\.memory\\.power_tower\\(double\\):2")
    public void testInvalidFunctionHandle()
    {
        createFunction(FUNCTION_POWER_TOWER_DOUBLE, true);
        SqlFunctionHandle functionHandle = new SqlFunctionHandle(FUNCTION_POWER_TOWER_DOUBLE.getFunctionId(), "2");
        functionNamespaceManager.getFunctionMetadata(functionHandle);
    }

    private void createFunctionNamespace(String catalog, String schema)
    {
        try (Handle handle = jdbi.open()) {
            handle.execute("INSERT INTO function_namespaces VALUES (?, ?)", catalog, schema);
        }
    }

    private void createFunction(SqlInvokedFunction function, boolean replace)
    {
        functionNamespaceManager.createFunction(function, replace);
    }

    private void alterFunction(QualifiedObjectName functionName, Optional<List<TypeSignature>> parameterTypes, AlterRoutineCharacteristics characteristics)
    {
        functionNamespaceManager.alterFunction(functionName, parameterTypes, characteristics);
    }

    private void dropFunction(QualifiedObjectName functionName, Optional<List<TypeSignature>> parameterTypes, boolean exists)
    {
        functionNamespaceManager.dropFunction(functionName, parameterTypes, exists);
    }

    private FunctionHandle getLatestFunctionHandle(SqlFunctionId functionId)
    {
        FunctionNamespaceTransactionHandle transactionHandle = functionNamespaceManager.beginTransaction();
        Optional<SqlInvokedFunction> function = functionNamespaceManager.getFunctions(Optional.of(transactionHandle), functionId.getFunctionName()).stream()
                .filter(candidate -> candidate.getFunctionId().equals(functionId))
                .max(comparing(SqlInvokedFunction::getRequiredVersion));
        assertTrue(function.isPresent());
        functionNamespaceManager.commit(transactionHandle);
        return function.get().getRequiredFunctionHandle();
    }

    private void assertListFunctions(Optional<String> likePattern, Optional<String> escape, SqlInvokedFunction... functions)
    {
        assertEquals(ImmutableSet.copyOf(functionNamespaceManager.listFunctions(likePattern, escape)), ImmutableSet.copyOf(Arrays.asList(functions)));
    }

    private void assertListFunctions(SqlInvokedFunction... functions)
    {
        assertListFunctions(Optional.empty(), Optional.empty(), functions);
    }

    private void assertGetFunctions(QualifiedObjectName functionName, SqlInvokedFunction... functions)
    {
        FunctionNamespaceTransactionHandle transactionHandle = functionNamespaceManager.beginTransaction();
        assertEquals(ImmutableSet.copyOf(functionNamespaceManager.getFunctions(Optional.of(transactionHandle), functionName)), ImmutableSet.copyOf(Arrays.asList(functions)));
        functionNamespaceManager.commit(transactionHandle);
    }

    private void assertGetFunctionMetadata(FunctionHandle functionHandle, SqlInvokedFunction expectFunction)
    {
        FunctionMetadata functionMetadata = functionNamespaceManager.getFunctionMetadata(functionHandle);

        assertEquals(functionMetadata.getName(), expectFunction.getSignature().getName());
        assertFalse(functionMetadata.getOperatorType().isPresent());
        assertEquals(functionMetadata.getArgumentTypes(), expectFunction.getSignature().getArgumentTypes());
        assertEquals(functionMetadata.getReturnType(), expectFunction.getSignature().getReturnType());
        assertEquals(functionMetadata.getFunctionKind(), SCALAR);
        assertEquals(functionMetadata.isDeterministic(), expectFunction.isDeterministic());
        assertEquals(functionMetadata.isCalledOnNullInput(), expectFunction.isCalledOnNullInput());
    }

    private static void assertPrestoException(Runnable runnable, ErrorCodeSupplier errorCode, String expectedMessageRegex)
    {
        try {
            runnable.run();
            fail("No Exception is thrown, expect PrestoException");
        }
        catch (PrestoException e) {
            assertEquals(e.getErrorCode(), errorCode.toErrorCode());
            assertTrue(e.getMessage().matches(expectedMessageRegex), format("Error message '%s' does not match '%s'", e.getMessage(), expectedMessageRegex));
        }
    }

    private static String dummyString(int length)
    {
        return Joiner.on("").join(nCopies(length, "x"));
    }

    private static SqlInvokedFunction constructTestFunction(QualifiedObjectName functionName)
    {
        return new SqlInvokedFunction(
                functionName,
                ImmutableList.of(new Parameter("x", parseTypeSignature(DOUBLE))),
                parseTypeSignature(DOUBLE),
                "power tower",
                RoutineCharacteristics.builder().setDeterminism(DETERMINISTIC).build(),
                "pow(x, x)",
                notVersioned());
    }

    private static SqlInvokedFunction createFunctionTangent(QualifiedObjectName functionName)
    {
        return new SqlInvokedFunction(
                functionName,
                FUNCTION_TANGENT.getParameters(),
                FUNCTION_TANGENT.getSignature().getReturnType(),
                FUNCTION_TANGENT.getDescription(),
                FUNCTION_TANGENT.getRoutineCharacteristics(),
                FUNCTION_TANGENT.getBody(),
                notVersioned());
    }

    private static SqlInvokedFunction createFunctionTangent(List<Parameter> parameters)
    {
        return new SqlInvokedFunction(
                FUNCTION_TANGENT.getFunctionId().getFunctionName(),
                parameters,
                FUNCTION_TANGENT.getSignature().getReturnType(),
                FUNCTION_TANGENT.getDescription(),
                FUNCTION_TANGENT.getRoutineCharacteristics(),
                FUNCTION_TANGENT.getBody(),
                notVersioned());
    }

    private static SqlInvokedFunction createFunctionTangent(TypeSignature returnType)
    {
        return new SqlInvokedFunction(
                TANGENT,
                FUNCTION_TANGENT.getParameters(),
                returnType,
                FUNCTION_TANGENT.getDescription(),
                FUNCTION_TANGENT.getRoutineCharacteristics(),
                FUNCTION_TANGENT.getBody(),
                notVersioned());
    }

    private static TypeSignature createLargeRowType(int fieldCount)
    {
        String format = format("ROW(%s)", Joiner.on(",").join(nCopies(fieldCount, DOUBLE)));
        return parseTypeSignature(format);
    }
}
