Функции#

Реализация плагина#

Фреймворк функций используется для реализации функций SQL. Трино включает в себя количество встроенных функций. Для реализации новых функций вы можете написать плагин, который возвращает одну или несколько функций изgetFunctions():

public class ExampleFunctionsPlugin
        implements Plugin
{
    @Override
    public Set<Class<?>> getFunctions()
    {
        return ImmutableSet.<Class<?>>builder()
                .add(ExampleNullFunction.class)
                .add(IsNullFunction.class)
                .add(IsEqualOrNullFunction.class)
                .add(ExampleStringFunction.class)
                .add(ExampleAverageFunction.class)
                .build();
    }
}

Обратите внимание, что класс ImmutableSet — это служебный класс из Guava. Метод getFunctions() содержит все классы функций, которые мы реализуем ниже в этом руководстве.

Полный пример в кодовой базе см. в модуле trino-ml для функций машинного обучения или в модуле trino-teradata-functions для функций, совместимых с Teradata, оба находятся в каталоге plugin исходного кода Trino.

Реализация скалярной функции#

Функциональная структура использует аннотации для указания соответствующей информации. о функциях, включая имя, описание, тип возвращаемого значения и параметр типы. Ниже приведен пример функции, которая реализуетis_null:

public class ExampleNullFunction
{
    @ScalarFunction("is_null", deterministic = true)
    @Description("Returns TRUE if the argument is NULL")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean isNull(
            @SqlNullable @SqlType(StandardTypes.VARCHAR) Slice string)
    {
        return (string == null);
    }
}

Функция is_null принимает один аргумент VARCHAR и возвращает BOOLEAN, указывающий, был ли аргумент NULL. Обратите внимание, что аргумент функции имеет тип Slice. VARCHAR использует Slice, что по сути является оберткой над byte[], а не String, как собственным контейнерным типом.

Аргумент deterministic указывает, что функция не имеет побочных эффектов и при последующих вызовах с теми же аргументами возвращает точно те же значения.

В Trino детерминированные функции не зависят от какого-либо изменяющегося состояния. и не изменяют никакого состояния. Флаг deterministic является необязательным и по умолчанию равен true.

Например, функция shuffle() является недетерминированной, поскольку использует случайные значения. С другой стороны, now() является детерминированной, поскольку последующие вызовы в рамках одного запроса возвращают одну и ту же метку времени.

Для любой функции с недетерминированным поведением требуется установить deterministic = false, чтобы избежать неожиданных результатов.

  • @SqlType:

    Аннотация @SqlType используется для объявления типа возвращаемого значения и типов аргументов. Обратите внимание, что тип возвращаемого значения и аргументы Java-кода должны соответствовать собственным контейнерным типам соответствующих аннотаций.

  • @SqlNullable:

    Аннотация @SqlNullable указывает, что аргумент может быть NULL. Без этой аннотации framework предполагает, что все функции возвращают NULL, если любой из их аргументов равен NULL. При работе с Type, у которого собственный контейнерный тип примитивный (например, BigintType), при использовании @SqlNullable применяйте объектную обертку контейнерного типа. Метод должен быть помечен @SqlNullable, если он может вернуть NULL, когда аргументы не равны null.

Параметрические скалярные функции#

Скалярные функции, имеющие параметры типа, имеют некоторую дополнительную сложность. Чтобы наш предыдущий пример работал с любым типом, нам нужно следующее:

@ScalarFunction(name = "is_null")
@Description("Returns TRUE if the argument is NULL")
public final class IsNullFunction
{
    @TypeParameter("T")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean isNullSlice(@SqlNullable @SqlType("T") Slice value)
    {
        return (value == null);
    }

    @TypeParameter("T")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean isNullLong(@SqlNullable @SqlType("T") Long value)
    {
        return (value == null);
    }

    @TypeParameter("T")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean isNullDouble(@SqlNullable @SqlType("T") Double value)
    {
        return (value == null);
    }

    // ...and so on for each native container type
}
  • @TypeParameter:

    Аннотация @TypeParameter используется для объявления параметра типа, который можно использовать в аргументах аннотации @SqlType или в типе возвращаемого значения функции. Ее также можно использовать для аннотирования параметра типа Type. Во время выполнения движок привяжет конкретный тип к этому параметру. @OperatorDependency может использоваться, чтобы объявить необходимость дополнительной функции для работы с данным параметром типа. Например, следующая функция привяжется только к типам, для которых определена функция равенства:

@ScalarFunction(name = "is_equal_or_null")
@Description("Returns TRUE if arguments are equal or both NULL")
public final class IsEqualOrNullFunction
{
    @TypeParameter("T")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean isEqualOrNullSlice(
            @OperatorDependency(
                    operator = OperatorType.EQUAL,
                    returnType = StandardTypes.BOOLEAN,
                    argumentTypes = {"T", "T"}) MethodHandle equals,
            @SqlNullable @SqlType("T") Slice value1,
            @SqlNullable @SqlType("T") Slice value2)
    {
        if (value1 == null && value2 == null) {
            return true;
        }
        if (value1 == null || value2 == null) {
            return false;
        }
        return (boolean) equals.invokeExact(value1, value2);
    }

    // ...and so on for each native container type
}

Еще один пример скалярной функции#

Функция lowercaser принимает один аргумент VARCHAR и возвращает VARCHAR, то есть аргумент, преобразованный в нижний регистр:

public class ExampleStringFunction
{
    @ScalarFunction("lowercaser")
    @Description("Converts the string to alternating case")
    @SqlType(StandardTypes.VARCHAR)
    public static Slice lowercaser(@SqlType(StandardTypes.VARCHAR) Slice slice)
    {
        String argument = slice.toStringUtf8();
        return Slices.utf8Slice(argument.toLowerCase());
    }
}

Обратите внимание, что для большинства распространенных строковых функций, включая преобразование строки в строчные буквы, библиотека Slice также предоставляет реализации, которые работают напрямую на основеbyte[], которые имеют гораздо лучшую производительность. Эта функция не имеет@SqlNullableаннотации, означающие, что если аргументNULL, результат будет автоматическиNULL(функция не будет вызвана).

Реализация функции агрегирования#

Функции агрегации используют структуру, аналогичную скалярным функциям, но немного сложнее.

  • AccumulatorState:

    Все функции агрегирования накапливают входные строки в объект состояния; этот объект должен реализоватьAccumulatorState. Для простых агрегаций просто продлеватьAccumulatorStateв новый интерфейс с геттерами и сеттерами вы хотите, и фреймворк сгенерирует все реализации и сериализаторы для вас. Если вам нужен более сложный объект состояния, вам понадобится реализоватьAccumulatorStateFactoryиAccumulatorStateSerializer и предоставить их черезAccumulatorStateMetadataаннотация.

Следующий код реализует функцию агрегирования.avg_doubleкоторый вычисляет среднее значениеDOUBLEстолбец:

@AggregationFunction("avg_double")
public class AverageAggregation
{
    @InputFunction
    public static void input(
            LongAndDoubleState state,
            @SqlType(StandardTypes.DOUBLE) double value)
    {
        state.setLong(state.getLong() + 1);
        state.setDouble(state.getDouble() + value);
    }

    @CombineFunction
    public static void combine(
            LongAndDoubleState state,
            LongAndDoubleState otherState)
    {
        state.setLong(state.getLong() + otherState.getLong());
        state.setDouble(state.getDouble() + otherState.getDouble());
    }

    @OutputFunction(StandardTypes.DOUBLE)
    public static void output(LongAndDoubleState state, BlockBuilder out)
    {
        long count = state.getLong();
        if (count == 0) {
            out.appendNull();
        }
        else {
            double value = state.getDouble();
            DOUBLE.writeDouble(out, value / count);
        }
    }
}

Среднее значение состоит из двух частей: суммыDOUBLEв каждой строке столбца иLONGподсчет количества просмотренных строк.LongAndDoubleStateэто интерфейс который простираетсяAccumulatorState:

public interface LongAndDoubleState
        extends AccumulatorState
{
    long getLong();

    void setLong(long value);

    double getDouble();

    void setDouble(double value);
}

Как говорилось выше, для простогоAccumulatorStateобъекты, достаточно просто определите интерфейс с помощью геттеров и сеттеров, а также структуру сгенерирует для вас реализацию.

Углубленный взгляд на различные аннотации, имеющие отношение к написанию агрегата. функция следующая:

  • @InputFunction:

    Аннотация @InputFunction объявляет функцию, принимающую входные строки и сохраняющую их в AccumulatorState. Как и в скалярных функциях, аргументы должны быть аннотированы @SqlType. Обратите внимание, что, в отличие от скалярного примера выше, где для VARCHAR используется Slice, здесь для входного аргумента используется примитивный тип double. В этом примере входная функция просто отслеживает текущее число строк (через setLong()) и текущую сумму (через setDouble()).

  • @CombineFunction:

    Аннотация @CombineFunction объявляет функцию, используемую для объединения двух объектов состояния. Эта функция используется для объединения всех состояний частичной агрегации. Она принимает два объекта состояния и объединяет результаты в первый (в примере выше — просто складывает их).

  • @OutputFunction:

    @OutputFunction — последняя функция, вызываемая при вычислении агрегации. Она принимает итоговый объект состояния (результат объединения всех частичных состояний) и записывает результат в BlockBuilder.

    • Где происходит сериализация, и что такое GroupedAccumulatorState?

    @InputFunction обычно запускается на другом worker-узле, чем @CombineFunction, поэтому объекты состояния сериализуются и передаются между этими worker-узлами framework-ом агрегации. GroupedAccumulatorState используется при выполнении агрегации GROUP BY, и реализация будет автоматически сгенерирована, если вы не укажете AccumulatorStateFactory

Устаревшая функция#

Аннотация @Deprecated должна использоваться для любой функции, которую больше не следует использовать. Эта аннотация заставляет Trino генерировать предупреждение всякий раз, когда SQL-операторы используют устаревшую функцию. Когда функция устарела, @Description нужно заменить примечанием об устаревании и функции-замене:

public class ExampleDeprecatedFunction
{
    @Deprecated
    @ScalarFunction("bad_function")
    @Description("(DEPRECATED) Use good_function() instead")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean bad_function()
    {
        return false;
    }
}