Snowflake предлагает возможность использовать JavaScript (JS) для написания определяемых пользователем функций (UDF). В этой статье я хочу показать, как автономно тестировать такие пользовательские функции и как можно использовать DBT для их создания в Snowflake.
Это не учебник по Snowflake или DBT. Я предполагаю базовые знания как о JavaScript, так и о нем. Для простоты я использую минимальный пример, исключая использование какой-либо среды модульного тестирования JS или более сложной структуры проекта.
Кроме того, интеграция такого модульного тестирования и развертывания DBT в более крупный конвейер выходит за рамки этой статьи. .
Пример пользовательской функции
Мы будем использовать пример UDF из официальной документации Snowflake.
CREATE OR REPLACE FUNCTION "CHAR_SUM"(INS STRING) RETURNS TABLE (NUM FLOAT) LANGUAGE JAVASCRIPT AS '{ processRow: function (row, rowWriter, context) { this.ccount = this.ccount + 1; this.csum = this.csum + row.INS.length; rowWriter.writeRow({NUM: row.INS.length}); }, finalize: function (rowWriter, context) { rowWriter.writeRow({NUM: this.csum}); }, initialize: function(argumentInfo, context) { this.ccount = 0; this.csum = 0; }}';
Эта определяемая пользователем функция подсчитывает количество символов в заданных строках и записывает как количество символов в строке, так и общую сводку в качестве результата.
CREATE TABLE parts (p FLOAT, s STRING); INSERT INTO parts VALUES (1, 'michael'), (1, 'kelly'), (1, 'brian'); INSERT INTO parts VALUES (2, 'clara'), (2, 'maggie'), (2, 'reagan'); select * from parts, table(char_sum(s)); +--------+---------+-----+ | P | S | NUM | +--------+---------+-----+ | 1 | michael | 7 | | 1 | kelly | 5 | | 1 | brian | 5 | | 2 | clara | 5 | | 2 | maggie | 6 | | 2 | reagan | 6 | | [NULL] | [NULL] | 34 | +--------+---------+-----+
Проект DBT
Мы используем проект DBT udf_testing с тремя основными папками: макросы, модели и udf_js_tests, а также dbt_project.yml и профили. yml файлы.
Мы помещаем наш код UDF в macros/udfs. Чтобы иметь возможность протестировать код JS отдельно, мы разделили оператор UDF на два макроса: char_sum_js.sql для кода JavaScript и create_char_sum_udf.sqlдля кода JavaScript. Оператор SQL для создания пользовательской функции.
Кроме того, мы определяем одну модель, которую можно использовать в качестве тестовых данных для UDF. Файл модели parts.sql находится в папке models/char_sum.
Папка udf_js_tests позже будет использоваться для размещения нашего модульного теста.
udf_testing/ macros/ udfs/ char_sum_js.sql create_char_sum_udf.sql models/ char_sum/ parts.sql udf_js_tests/ dbt_project.yml profiles.yml
макросы/udfs/char_sum_js.sql
{% macro char_sum_js() %} { processRow: function (row, rowWriter, context) { this.ccount = this.ccount + 1; this.csum = this.csum + row.INS.length; rowWriter.writeRow({NUM: row.INS.length}); }, finalize: function (rowWriter, context) { rowWriter.writeRow({NUM: this.csum}); }, initialize: function(argumentInfo, context) { this.ccount = 0; this.csum = 0; } } {% endmacro %}
макросы/udfs/create_char_sum_udf.sql
{% macro create_char_sum_udf() %} CREATE OR REPLACE FUNCTION "CHAR_SUM"(INS STRING) RETURNS TABLE (NUM FLOAT) LANGUAGE JAVASCRIPT AS '{{ char_sum_js() }}'; {% endmacro %}
models/char_sum/parts.sql
SELECT $1 as p, $2 as s FROM VALUES (1, 'michael'), (1, 'kelly'), (1, 'brian'), (2, 'clara'), (2, 'maggie'), (2, 'reagan')
profiles.yml
udf_testing: outputs: char_sum: account: "" database: "" password: "" schema: "public" threads: 1 type: snowflake user: ""
dbt_project.yml
name: 'udf_testing' version: '1.0.0' config-version: 2 profile: 'udf_testing' macro-paths: ["macros"] model-paths: ["models"] models: udf_testing: char_sum: +post-hook: - "{{ create_char_sum_udf() }}"
Запустите проект DBT
Учитывая, что необходимая недостающая информация в profiles.yml была предоставлена, теперь мы можем запустить проект.
dbt run — profiles-dir . — target char_sum
Обратите внимание, что DBT не печатает выполнение макроса в случае успешного запуска DBT.
Теперь мы можем протестировать нашу пользовательскую функцию в Snowflake, выполнив ее на тестовых данных в контексте базы данных, указанной в profiles.yml.
select * from parts, table(char_sum(s));
Модульный тест UDF
Для модульного теста мы будем использовать nodejs и ванильный JavaScript. Nodejs позволяет нам выполнять JavaScript из командной строки. Установка проста, соответствующие инструкции можно найти здесь: https://nodejs.dev/learn/how-to-install-nodejs.
Мы создаем файл udf_js_tests/test_char_sum.js, который будет содержать наши тесты и некоторые вспомогательные функции.
Сначала добавим небольшую вспомогательную функцию для проверки результатов наших тестов. В версии производственного кода эта функциональность, конечно же, будет обеспечиваться средой модульного тестирования.
function assert(test_name, is_true) { if (is_true == false) { console.log(' Failed test: ' + test_name); } else { console.log(' Ok test: ' + test_name); } }
Далее нам нужен способ загрузки кода JS из macros/udfs/char_sum_js.sql. Следующая функция сделает это, а также преобразует код в объект среды выполнения, который можно вызывать в наших тестах.
const fs = require('fs'); function load_udf_to_test(file_name) { const udf_string = fs. readFileSync(file_name, 'utf8'). replace(/{%(.)*%}/g,'') .trim(); const udf_object = eval('(function self(){return ' + udf_string + ';})()'); return udf_object; };
- Макросы DBT начинаются с {% macro .. %} и заканчиваются {% endmacro %}. Эту часть кода, а также символы новой строки в начале и конце блока JS необходимо удалить. Это достигается с помощью регулярного выражения replace и операции trim.
- Чтобы преобразовать JS-код (строку) в полезный объект, используется операция eval. Функция-обертка нужна для получения доступа к JS-объекту. Более подробное объяснение последнего можно найти здесь: https://stackoverflow.com/questions/7399024/how-can-i-use-js-eval-to-return-a-value
Теперь мы можем загрузить нашу UDF, передав путь к файлу sql в функцию.
const char_sum_udf = load_udf_to_test('../macros/udfs/char_sum_js.sql');
Теперь мы можем добавить наш первый тест. Мы гарантируем, что функция initialize работает правильно.
console.log('Test initialize function'); char_sum_udf.initialize({}, {}); assert('Init csum', char_sum_udf.csum == 0); assert('Init ccount', char_sum_udf.ccount == 0);
Затем мы можем протестировать функцию processRow. Однако эта функция ожидает аргументы: средство записи строки, а также строку. Теперь мы можем просто сымитировать автора и предоставить тестовую строку.
const row_writer_result = []; const mock_row_writer = { writeRow: function(o) { row_writer_result.push(o); } }; const test_row = { INS: 'michael' }; char_sum_udf.processRow(test_row, mock_row_writer, {}); assert('Process row result exists', row_writer_result.length == 1); assert('Process row result correct', row_writer_result[0].NUM == 7); assert('Process row csum correct', my_udf.csum == 7); assert('Process row ccount correct', my_udf.ccount == 1);
Модульный тест функции finalise следует тому же подходу. Теперь мы можем запустить наш тестовый файл и проверить результаты.
node test_char_sum.js
Заключение
- Возможность поддерживать пользовательские функции через DBT и проводить их автономное модульное тестирование, как и другие артефакты кода, помогает обеспечить высокое качество и следовать передовым инженерным практикам. Пользовательские функции — это просто код в репозитории, который можно изменять, просматривать, тестировать и развертывать в контролируемых конвейерах.
- Возможность изолированного тестирования UDF также может помочь в исследовании и выборке базовой производительности при различных нагрузках без участия реального экземпляра Snowflake. В целом это полезно, особенно в локальных средах разработки и тестирования.
- Несмотря на то, что эта статья упростила процедуры тестирования и структуру проекта, общий подход не сильно изменился бы в условиях производственного уровня. Могут быть введены надлежащие среды модульного тестирования, а тест JS может быть вызван, например. конвейеры gitlab в качестве обязательного шага перед применением DBT или тестом DBT на фактическом экземпляре Snowflake.
- Обертывание кода JS в макрос добавляет некоторый шум в файл и имеет недостатки, например. подсветка кода в редакторе. Однако на данный момент, похоже, нет другого способа создавать файлы, которые принимает DBT.
- Snowflake также поддерживает пользовательские функции Java. Их можно использовать как встроенные, как JS, или путем загрузки и ссылки на JAR-файлы. В обоих случаях также возможны процедуры тестирования в сочетании с контролируемым созданием DBT.
- Хранимые процедуры JS можно тестировать так же, как и пользовательские функции JS. Потребуется имитация объекта snowflake и связанных с ним функций.