Табличные функции#

Табличные функции возвращают таблицы. Они позволяют пользователям динамически вызывать пользовательскую логику прямо из SQL-запроса. Они вызываются в предложении FROM запроса, а соглашение о вызове похоже на вызов скалярной функции. Описание использования табличных функций см. в table functions.

Trino поддерживает добавление пользовательских табличных функций. Они объявляются коннекторами через реализацию специальных интерфейсов.

Объявление табличной функции#

Чтобы объявить табличную функцию, нужно реализовать ConnectorTableFunction. Наследование от AbstractConnectorTableFunction - удобный способ сделать это. Метод getTableFunctions() коннектора должен возвращать набор ваших реализаций.

Конструктор#

public class MyFunction
        extends AbstractConnectorTableFunction
{
    public MyFunction()
    {
        super(
                "system",
                "my_function",
                List.of(
                        ScalarArgumentSpecification.builder()
                                .name("COLUMN_COUNT")
                                .type(INTEGER)
                                .defaultValue(2)
                                .build(),
                        ScalarArgumentSpecification.builder()
                                .name("ROW_COUNT")
                                .type(INTEGER)
                                .build()),
                GENERIC_TABLE);
    }
}

Конструктор принимает следующие аргументы:

  • имя схемы

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

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

  • имя функции

  • список ожидаемых аргументов

Поддерживаются три различных типа аргументов: скалярные аргументы, аргументы descriptor и аргументы table. Подробности см. в Типы аргументов. Можно указывать значения по умолчанию для скалярных аргументов и аргументов descriptor. Аргументы, для которых задано значение по умолчанию, можно пропускать при вызове табличной функции.

  • тип возвращаемой строки

Он описывает тип строки, создаваемой табличной функцией.

Если табличная функция принимает аргументы table, она может дополнительно передавать в вывод столбцы входных таблиц с помощью механизма pass-through. Тип возвращаемой строки должен описывать только столбцы, создаваемые функцией, в отличие от pass-through столбцов.

В примере тип возвращаемой строки - GENERIC_TABLE, что означает, что тип строки заранее неизвестен и определяется динамически на основе переданных аргументов.

Когда тип возвращаемой строки известен статически, его можно объявить так:

new DescribedTable(descriptor)

Если табличная функция не создает собственных столбцов и выводит только pass-through столбцы, используйте ONLY_PASS_THROUGH в качестве возвращаемого типа строки.

Note

Табличная функция должна возвращать как минимум один столбец. Это может быть либо собственный столбец (produced by the function), либо pass-through столбец.

Типы аргументов#

Табличные функции принимают три типа аргументов: scalar arguments, descriptor arguments и table arguments.

Скалярные аргументы#

Они могут иметь любой поддерживаемый тип данных. Можно указывать значение по умолчанию.

ScalarArgumentSpecification.builder()
        .name("COLUMN_COUNT")
        .type(INTEGER)
        .defaultValue(2)
        .build()
ScalarArgumentSpecification.builder()
        .name("ROW_COUNT")
        .type(INTEGER)
        .build()

Аргументы descriptor#

Descriptor состоят из полей с именами и необязательными типами данных. Это удобный способ передать в функцию требуемый тип результирующей строки или, например, сообщить функции, какие входные столбцы ей следует использовать. Для аргументов descriptor можно задавать значения по умолчанию. Аргумент descriptor может быть null.

DescriptorArgumentSpecification.builder()
        .name("SCHEMA")
        .defaultValue(null)
        .build()

Аргументы table#

Табличная функция может принимать любое количество входных relations. Это позволяет одновременно обрабатывать несколько источников данных.

При объявлении аргумента table нужно указать характеристики, определяющие, как обрабатывается входная таблица. Также обратите внимание, что для аргумента table нельзя задавать значение по умолчанию.

TableArgumentSpecification.builder()
        .name("INPUT")
        .rowSemantics()
        .pruneWhenEmpty()
        .passThroughColumns()
        .build()
Семантика set или row#

Семантика set используется для аргументов table по умолчанию. Аргумент table с семантикой set обрабатывается по partition за partition. При вызове функции пользователь может задать для аргумента partitioning и ordering. Если partitioning не задано, аргумент обрабатывается как один partition.

Аргумент table с семантикой row обрабатывается построчно. Partitioning и ordering в этом случае неприменимы.

Prune или keep при пустом входе#

Свойство prune when empty указывает, что если данный аргумент table пуст, функция возвращает пустой результат. Это свойство используется для оптимизации запросов с табличными функциями. Свойство keep when empty указывает, что функцию нужно выполнять, даже если аргумент table пуст. Пользователь может переопределить это свойство при вызове функции. Использование свойства keep when empty может негативно повлиять на производительность, когда аргумент table не пуст.

Столбцы pass-through#

Если аргумент table имеет pass-through columns, все его столбцы передаются в вывод. Для аргумента table без этого свойства в вывод передаются только столбцы partitioning.

Метод analyze()#

Чтобы предоставить движку Trino всю необходимую информацию, класс должен реализовать метод analyze(). Этот метод вызывается движком на этапе анализа обработки запроса. Метод analyze() также является местом, где выполняются пользовательские проверки аргументов:

@Override
public TableFunctionAnalysis analyze(ConnectorSession session, ConnectorTransactionHandle transaction, Map<String, Argument> arguments)
{
    long columnCount = (long) ((ScalarArgument) arguments.get("COLUMN_COUNT")).getValue();
    long rowCount = (long) ((ScalarArgument) arguments.get("ROW_COUNT")).getValue();

    // custom validation of arguments
    if (columnCount < 1 || columnCount > 3) {
         throw new TrinoException(INVALID_FUNCTION_ARGUMENT, "column_count must be in range [1, 3]");
    }

    if (rowCount < 1) {
        throw new TrinoException(INVALID_FUNCTION_ARGUMENT, "row_count must be positive");
    }

    // determine the returned row type
    List<Descriptor.Field> fields = List.of("col_a", "col_b", "col_c").subList(0, (int) columnCount).stream()
            .map(name -> new Descriptor.Field(name, Optional.of(BIGINT)))
            .collect(toList());

    Descriptor returnedType = new Descriptor(fields);

    return TableFunctionAnalysis.builder()
            .returnedType(returnedType)
            .handle(new MyHandle(columnCount, rowCount))
            .build();
}

Метод analyze() возвращает объект TableFunctionAnalysis, который содержит всю информацию, необходимую движку для анализа, планирования и выполнения вызова табличной функции:

  • Тип возвращаемой строки, задаваемый как optional Descriptor. Его нужно передавать тогда и только тогда, когда табличная функция объявлена с возвращаемым типом GENERIC_TABLE.

  • Требуемые столбцы из аргументов table, задаваемые как map из имен аргументов table в списки индексов столбцов.

  • Любая информация, собранная на этапе анализа и полезная при планировании или выполнении, в виде ConnectorTableFunctionHandle. ConnectorTableFunctionHandle - это marker interface, предназначенный для переноса информации через последующие этапы обработки запроса способом, непрозрачным для движка.

Выполнение табличной функции#

Для табличных функций доступны два пути выполнения.

  1. Pushdown в коннектор

Коннектор, предоставляющий табличную функцию, реализует метод applyTableFunction(). Этот метод вызывается на этапе оптимизации обработки запроса. Он возвращает ConnectorTableHandle и список ColumnHandle, представляющих результат табличной функции. После этого вызов табличной функции заменяется на TableScanNode.

Этот путь выполнения удобен для табличных функций, результаты которых легко представить как ConnectorTableHandle, например query pass-through. Он поддерживает только скалярные аргументы и аргументы descriptor.

  1. Выполнение оператором

В Trino есть отдельный оператор для табличных функций. Он может обрабатывать табличные функции с любым количеством аргументов table, а также скалярные аргументы и аргументы descriptor. Чтобы использовать этот путь выполнения, нужно предоставить реализацию процессора.

Если ваша табличная функция имеет один или более аргументов table, нужно реализовать TableFunctionDataProcessor. Он обрабатывает страницы входных данных.

Если ваша табличная функция является source operator (не имеет аргументов table), нужно реализовать TableFunctionSplitProcessor. Он обрабатывает split. Коннектор, предоставляющий функцию, должен предоставить ConnectorSplitSource для функции. Благодаря split задача может быть разделена так, чтобы каждый split представлял отдельную подзадачу.

Контроль доступа#

Контроль доступа для табличных функций может быть реализован как на системном, так и на уровне коннектора. Он основан на полном имени табличной функции, которое состоит из имени каталога, имени схемы и имени функции, в синтаксисе catalog.schema.function.