Статьи

Apache Drill: как создать новую функцию?

Apache Drill позволяет пользователям исследовать любые типы данных, используя ANSI SQL. Это замечательно, но Drill идет еще дальше и позволяет создавать пользовательские функции для расширения механизма запросов. Эти пользовательские функции обладают всей производительностью любой из примитивных операций Drill, но их использование делает написание этих функций немного сложнее, чем можно было ожидать.

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

Давайте создадим новую функцию, которая позволит вам маскировать некоторые символы в строке, и давайте сделаем это очень просто. Новая функция позволит пользователю скрыть x символов в начале и заменить их на любые символы по своему выбору. Это будет выглядеть так:

1
MASK( 'PASSWORD' , '#' , 4 ) => ####WORD

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

Предпосылки

Для этого вам понадобится:

  • Java Developer Kit 7 или более поздняя версия
  • Apache Drill 1.1 или более поздняя версия
  • Maven 3.0 или более поздняя версия

зависимости

Следующая зависимость Drill должна быть добавлена ​​в ваш проект maven

1
2
3
4
5
<dependency>
      <groupId>org.apache.drill.exec</groupId>
      <artifactId>drill-java-exec</artifactId>
      <version>1.1.0</version>
</dependency>

Источник

Функция Mask является реализацией DrillSimpleFunc .

Разработчики могут создавать 2 типа пользовательских функций:

  • Простые функции: эти функции имеют одну строку в качестве входных данных и выдают одно значение в качестве выходных
  • Функции агрегации: которые будут принимать несколько строк в качестве входных данных и выдают одно значение в качестве выходных

Простые функции часто называют UDF, которые обозначают пользовательские функции. Функции агрегирования называются UDAF, что обозначает пользовательскую функцию агрегирования.

В этом примере нам просто нужно преобразовать значение столбца в каждой строке, поэтому достаточно простой функции.

Создать функцию

Первым шагом является реализация интерфейса DrillSimpleFunc .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package org.apache.drill.contrib.function;
 
import org.apache.drill.exec.expr.DrillSimpleFunc;
import org.apache.drill.exec.expr.annotations.FunctionTemplate;
 
@FunctionTemplate(
        name="mask",
        scope= FunctionTemplate.FunctionScope.SIMPLE,
        nulls = FunctionTemplate.NullHandling.NULL_IF_NULL
)
public class SimpleMaskFunc implements DrillSimpleFunc{
 
    public void setup() {
 
    }
 
    public void eval() {
 
    }
}

Поведение функции определяется аннотациями (строка 6-10) * Имя функции * Область действия функции, в нашем случае Simple * Что делать, если значение равно NULL, в этом случае Reverse просто возвращает NULL

Теперь нам нужно реализовать логику функции, используя методы setup() и eval() .

  • setup не требует пояснений, и в нашем случае нам не нужно ничего настраивать.
  • eval который является ядром функции. Как видите, этот метод не имеет никакого параметра и возвращает void. Итак, как это работает?

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import io.netty.buffer.DrillBuf;
import org.apache.drill.exec.expr.DrillSimpleFunc;
import org.apache.drill.exec.expr.annotations.FunctionTemplate;
import org.apache.drill.exec.expr.annotations.Output;
import org.apache.drill.exec.expr.annotations.Param;
import org.apache.drill.exec.expr.holders.IntHolder;
import org.apache.drill.exec.expr.holders.NullableVarCharHolder;
import org.apache.drill.exec.expr.holders.VarCharHolder;
 
import javax.inject.Inject;
 
 
@FunctionTemplate(
        name = "mask",
        scope = FunctionTemplate.FunctionScope.SIMPLE,
        nulls = FunctionTemplate.NullHandling.NULL_IF_NULL
)
public class SimpleMaskFunc implements DrillSimpleFunc {
 
    @Param
    NullableVarCharHolder input;
 
    @Param(constant = true)
    VarCharHolder mask;
 
    @Param(constant = true)
    IntHolder toReplace;
 
    @Output
    VarCharHolder out;
 
    @Inject
    DrillBuf buffer;
 
    public void setup() {
    }
 
    public void eval() {
 
    }
 
}

Нам нужно определить параметры функции. В этом случае у нас есть 3 параметра, каждый из которых определяется с @Param аннотации @Param . Кроме того, мы также должны определить возвращаемое значение с @Output аннотации @Output .

Параметры нашей функции маски:

  • Обнуляемая строка
  • Маска чар или строка
  • Количество символов для замены, начиная с первого

Функция возвращает:

  • Строка

Для каждого из этих параметров вы должны использовать класс держателя. Для String это управляется с помощью VarCharHolder или NullableVarCharHolder -lines 21, 24,30-, который обеспечивает буфер для эффективного управления большими объектами. Так как мы манипулируем VarChar вы также должны VarChar другой буфер, который будет использоваться для выходной строки 33-. Обратите внимание, что Drill на самом деле не использует кучу Java для данных, обрабатываемых в запросе, но вместо этого сохраняет эти данные вне кучи и управляет жизненным циклом для нас без использования сборщика мусора Java.

Мы почти закончили, так как у нас есть подходящий класс, объект ввода / вывода, нам просто нужно реализовать сам метод eval() и использовать эти объекты.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public void eval() {
 
    // get the value and replace with
    String maskValue = org.apache.drill.exec.expr.fn.impl.StringFunctionHelpers.getStringFromVarCharHolder(mask);
    String stringValue = org.apache.drill.exec.expr.fn.impl.StringFunctionHelpers.toStringFromUTF8(input.start, input.end, input.buffer);
 
    int numberOfCharToReplace = Math.min(toReplace.value, stringValue.length());
 
    // build the mask substring
    String maskSubString = com.google.common.base.Strings.repeat(maskValue, numberOfCharToReplace);
    String outputValue = (new StringBuilder(maskSubString)).append(stringValue.substring(numberOfCharToReplace)).toString();
 
    // put the output value in the out buffer
    out.buffer = buffer;
    out.start = 0;
    out.end = outputValue.getBytes().length;
    buffer.setBytes(0, outputValue.getBytes());
}

Код довольно прост:

  • Получи саму маску — строка 4
  • Получить значение — строка 5
  • Получить номер символа для замены — строка 7
  • Генерация новой строки с замаскированными значениями — строки 10/11
  • Создайте и заполните выходной буфер — строки с 14 по 17

Этот код, однако, выглядит немного странным для того, кто привык читать код Java. Эта странность возникает из-за того, что окончательный код, который выполняется в запросе, будет фактически создан на лету. Это позволяет Drill использовать компилятор JIT (Just-in-Time) для максимальной скорости. Чтобы сделать это, вы должны соблюдать некоторые основные правила:

  • Не используйте импорт, но вместо этого используйте полное имя класса , это то, что делается в строке 10 с классом Strings . (из API Google Guava, упакованного в Apache Drill)
  • Классы ValueHolders , в нашем случае VarCharHolder и IntHolder должны управляться как структуры, поэтому вы должны вызывать вспомогательные методы, например getStringFromVarCharHolder и toStringFromUTF8 . Вызов таких методов, как toString , приведет к очень серьезным проблемам.

Теперь мы готовы развернуть и протестировать эту новую функцию.

пакет

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

Самый простой способ сделать это — использовать maven для создания вашего проекта и, в частности, использовать maven-source-plugin, например, в своем файле pom.xml :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-source-plugin</artifactId>
    <version>2.4</version>
    <executions>
        <execution>
            <id>attach-sources</id>
            <phase>package</phase>
            <goals>
                <goal>jar-no-fork</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Теперь при сборке с использованием mvn package Maven сгенерирует 2 файла jar:

  • JAR по умолчанию с классами и ресурсами ( drill-simple-mask-1.0.jar )
  • Вторая банка с источниками ( drie-simple-mask-1.0-sources.jar )

Наконец, вы должны добавить файл drill-module.conf в папку ресурсов вашего проекта, чтобы сообщить Drill, что ваш jar-файл содержит пользовательскую функцию. Если у вас нет конкретной конфигурации для вашей функции, вы можете оставить этот файл пустым.

У нас все готово, теперь вы можете упаковать и развернуть новую функцию, просто упакуйте и скопируйте Jars в папку Drill стороннего производителя; $ DRILL_HOME / jars / 3rdparty, где $ DRILL_HOME — ваша папка установки Drill.

1
2
3
mvn clean package
 
cp target/*.jar  $DRILL_HOME/jars/3rdparty

Перезапустите тренировку.

Бег !

Теперь вы сможете использовать свою функцию в своих запросах:

01
02
03
04
05
06
07
08
09
10
SELECT MASK(first_name, '*' , 3) FIRST , MASK(last_name, '#', 7) LAST  FROM cp.`employee.json` LIMIT 5;
+----------+------------+
FIRST   |    LAST    |
+----------+------------+
| ***ri    | ######     |
| ***rick  | #######    |
| ***hael  | ######     |
| ***a     | #######ez  |
| ***erta  | #######    |
+----------+------------+

Вывод

В этом простом проекте вы научились писать, развертывать и использовать пользовательские функции Apache Drill. Теперь вы можете расширить это, чтобы создать свою собственную функцию.

При расширении Apache Drill следует помнить одну важную вещь (используя пользовательскую функцию, плагин хранилища или формат): динамическая среда Drill генерирует динамически много кода. Это означает, что вам, возможно, придется использовать очень специфический шаблон при написании и развертывании ваших расширений. С нашей основной функцией это означало, что мы должны были:

  • Развертывание классов И источников
  • использовать полностью квалифицированные имена классов
  • использовать классы держателей значений и вспомогательные методы для управления параметрами *
Ссылка: Apache Drill: как создать новую функцию? от нашего партнера JCG Tugdual Grall в блоге Tug’s Blog .