Обработка данных в UDF-функциях

Для правильной работы со сложными типами данных, необходимо обработать и сохранить информацию об их структуре. Это делается на этапе формирования сигнатуры udf-функции. Пример класса, который содержит эту информацию

struct TInputIndices {
        // В эти два поля мы занесем индексы полей в типе Struct. Эти индексы будут использоваться в дальнейшем для того, чтобы по конкретному объекту структуы получить значения ее полей.
        ui32 oneInputFieldIndex = 0;
        ui32 anotherInputFieldIndex = 0;

        static constexpr const int FieldsCount = 2;

        NYql::NUdf::TType* ResultStructType = nullptr;

        NYql::NUdf::TType* StructType() const {
            return ResultStructType;
        }

        TInputIndices(NYql::NUdf::IFunctionTypeInfoBuilder& builder) {
            auto optionalAnotherInputFieldType = builder.Optional()->Item(TDataType<TDatetime>::Id).Build();

            // После вызова Build, в oneInputFieldIndex и anotherInputFieldIndex окажется индекс соответствующих полей в структуре.
            ResultStructType = builder.Struct(FieldsCount)
                                   ->
                               AddField<double>("oneInputFieldName", &oneInputFieldIndex)
                                   .
                               AddField("anotherInputFieldName", optionalAnotherInputFieldType, &anotherInputFieldIndex)
                                   .
                               Build();
        }

        TSomeStruct UnpackUnboxedValue(const NYql::NUdf::TUnboxedValue& item) const {
            TSomeStruct someStruct;

            // Таким образом можно можно использовать индекс, чтобы получить значение конкретного поля.
            auto oneInputFieldIValue = item.GetElement(oneInputFieldIndex);
            auto anotherInputFieldIValue = item.GetElement(anotherInputFieldIndex);

            ...

            return someStruct;
        }
    };

Для формирования типа входных данных понадобится IFunctionTypeInfoBuilder.
Данный интерфейс поддерживает генерацию сложных типов, таких как структуры, списки, словари и т.д.
Допустим на вход подается структура, содержащая два поля - "oneInputField" типа Double и "anotherInputField" типа TDatetime?. С помощью билдера IFunctionTypeInfoBuilder, создается optional-тип для второго поля и создается тип структуры, который заполняется нужными полями.

Объявление типа структуры просиходит с помощью билдера через вызов метода Struct(num_of_fields). Под капотом, структура в YQL UDF представляется как обычный массив из значений полей.

Добавление полей осуществляется с помощью метода AddField возвращаемого интерфейса для работы со структурами.
После выполнения метода Build(), переменные oneInputFieldIndex и anotherInputFieldIndex будут содержать индексы, по которым во входном массиве данных буду расположены данные конкретного поля. С их помощью можно получить значение конкретного поля из входного массива данных, как это сделано в методе UnpackUnboxedValue в примере выше.

Далее этот класс можно использовать в методе Run udf-функции для распаковки
входных данных с помощью метода UnpackUnboxedValue.

Для упаковки результатов работы вашей udf можно использовать такие же классы для
хранения информации уже о выходных данных. Например:

struct TOutputIndices {
    ui32 firstOutputFieldIndex = 0;
    ui32 secondOutputFieldIndex = 0;
    ui32 thirdOutputFieldIndex = 0;

    static constexpr const int FieldsCount = 3;

    NYql::NUdf::TType* ResultStructType = nullptr;

    NYql::NUdf::TType* StructType() const {
        return ResultStructType;
    }

    TOutputIndices(NYql::NUdf::IFunctionTypeInfoBuilder& builder) {
        auto thirdOutputFieldType = builder.List()->Item(TDataType<ui32>::Id).Build()

        ResultStructType =
            builder.Struct(FieldsCount)
                  ->
            AddField<ui32>("firstOutputFieldName", &firstOutputFieldIndex)
                  .
            AddField<char*>("secondOutputFieldName", &secondOutputFieldIndex)
                  .
            AddField("thirdOutputFieldName", thirdOutputFieldType, &thirdOutputFieldIndex);
                  .
            Build();
    }

    NYql::NUdf::TUnboxedValue PackUnboxedValue(const NYql::NUdf::IValueBuilder* valueBuilder, ui32 valueForFirstField, const TString& valueForSecondField, std::vector<ui32> valuesForThirdField) {
        // Обратите внимание на то, что происходит в этом месте. Здесь ДВА выходных параметра: result - это то что вам надо будет вернуть. А вот в elemItems будет записан указатель на массив, в который вам нужно занести значения полей.
        TUnboxedValue* elemItems = nullptr;
        auto result = valueBuilder->NewArray(FieldsCount, elemItems);

        // Заполняем значения для полей
        elemItems[firstOutputFieldIndex] = TUnboxedValuePod(valueForFirstField);
        elemItems[secondOutputFieldIndex] = valueBuilder->NewString(valueForSecondField);

        std::vector<TUnboxedValue> thirdFieldItems;
        thirdFieldItems.reserve(valuesForThirdField.size());

        for (const auto& value : valuesForThirdField) {
            TUnboxedValue yqlValue = TUnboxedValuePod(value);

            if (yqlValue) {
                thirdFieldItems.push_back(std::move(yqlValue));
            }
        }

        auto thirdFieldValueHandler = valueBuilder->NewList(thirdFieldItems.data(),
                                                            thirdFieldItems.size());

        elemItems[thirdOutputFieldIndex] = thirdFieldValueHandler;

        return result;
    }
};

Формирование типа выходной структуры данных ничем не отличается от формирования типа входной структуры - так же нужно объявить поля структуры и сохранить их индексы для последующего заполнения выходного массива данных. В примере выше эта структура будет состоять из трех поле: одно поле будет иметь примитивный числовой тип, другое представляет собой строку, а третье - список. Для создания типа списка, нужно воспользоваться методом List() интерфейса NYql::NUdf::IFunctionTypeInfoBuilder. Этот метод вернет указатель на интерфейс, позволяющий создать тип списка. Указание типа элементов списка происходит через метод Item(type). Таким образом, кроме списков можно создавать словари, структуры, кортежи и т.д.

Формирование выходного массива данных происходит с помощью NYql::NUdf::IValueBuilder и его метода NewArray, который принимает на вход количество выходных элкметов + ссылку на указатель, а возвращает TUnboxedValue описывающий объект целиком + заполняет значением переданный указатель, с помощью которого будет происходить дальнейшее заполнение массива.

Данный метод возвращает NYql::NUdf::TUnboxedValue, который нужно вернуть после заполнения выходного массива необходимыми значениями. Важно понимать, что для заполнения выходного массива нужно использовать именно тот указатель,
который метод заполнил, а не то, что он вернул. При заполнении массива конкретным значением для конкретного поля, нужно использовать те индексы, которые ассоциированны с данным полем на этапе формирования типа выходных данных.

Если возвращаемое значение является примитивным числовым типом, то это значение можно просто обернуть в TUnboxedValuePod и положить это значение по соответствующему индексу.

Если возвращаемое значение является строкой, то необходимо воспользоваться методом NewString интерфейса NYql::NUdf::IValueBuilder, в который передать нужную строку.

Если возвращаемое значение является, например, списком, то необходимо создать массив значений TUnboxedValue, заполнить его, а затем передать его размер и указатель в метод NewList интерфейса NYql::NUdf::IValueBuilder. Данный метод вернет TUnboxedValue, который можно положить в выходной массив по соответствующему данному полю индексу.

Предыдущая
Следующая