О конфигурации dispatch
dbt может расширять функциональность на поддерживаемых платформах данных через систему множественной диспетчеризации. Поскольку синтаксис SQL, типы данных и поддержка DDL/DML различаются в зависимости от адаптеров, dbt может определять и вызывать универсальные функциональные макросы, а затем "диспетчеризировать" этот макрос к соответствующей реализации для текущего адаптера.
Синтаксис
Аргументы:
macro_name[обязательный]: Имя макроса для диспетчеризации. Должно быть строковым литералом.macro_namespace[необязательный]: Пространство имен (пакет) макроса для диспетчеризации. Должно быть строковым литералом.
Использование:
{% macro my_macro(arg1, arg2) -%}
{{ return(adapter.dispatch('my_macro')(arg1, arg2)) }}
{%- endmacro %}
dbt использует два критерия при поиске подходящего макроса-кандидата:
- Префикс адаптера
- Пространство имен (пакет)
Префикс адаптера: Макросы, специфичные для адаптера, имеют префикс с именем адаптера в нижнем регистре и двумя подчеркиваниями. Учитывая макрос с именем my_macro, dbt будет искать:
- Postgres:
postgres__my_macro - Redshift:
redshift__my_macro - Snowflake:
snowflake__my_macro - BigQuery:
bigquery__my_macro - OtherAdapter:
otheradapter__my_macro - по умолчанию:
default__my_macro
Если dbt не найдет реализацию, специфичную для адаптера, он диспетчеризирует к реализации по умолчанию.
Пространство имен: Обычно dbt будет искать реализации в корневом проекте и внутренних проектах (например, dbt, dbt_postgres). Если аргумент macro_namespace предоставлен, он вместо этого ищет в указанном пространстве имен (пакете) подходящие реализации. Также возможно динамически маршрутизировать поиск по пространствам имен, определяя конфигурацию проекта dispatch; см. примеры ниже для подробностей.
Примеры
Простой пример
Предположим, я хочу определить макрос concat, который компилируется в SQL-функцию concat() как его поведение по умолчанию. Однако на Redshift и Snowflake я хочу использовать оператор ||.
{% macro concat(fields) -%}
{{ return(adapter.dispatch('concat')(fields)) }}
{%- endmacro %}
{% macro default__concat(fields) -%}
concat({{ fields|join(', ') }})
{%- endmacro %}
{% macro redshift__concat(fields) %}
{{ fields|join(' || ') }}
{% endmacro %}
{% macro snowflake__concat(fields) %}
{{ fields|join(' || ') }}
{% endmacro %}
Верхний макрос concat следует специальной, жесткой формуле: он назван с "основным именем" макроса, concat, которое будет использоваться для вызова макроса в других местах. Он принимает один аргумент, названный fields. Единственная функция этого макроса — диспетчеризация, то есть поиск и возврат, используя основное имя макроса (concat) в качестве поискового термина. Он также хочет передать, в свою конечную реализацию, все переданные ему аргументы. В данном случае есть только один аргумент, названный fields.
Ниже этого макроса я определил три возможные реализации макроса concat: одну для Redshift, одну для Snowflake и одну для использования по умолчанию на всех других адаптерах. В зависимости от адаптера, с которым я работаю, будет выбран один из этих макросов, ему будут переданы указанные аргументы в качестве входных данных, он выполнит операции над этими аргументами и вернет результат исходному диспетчеризующему макросу.
Более сложный пример
Я нашел существующую реализацию макроса concat в пакете dbt-utils. Однако я хочу переопределить его реализацию макроса concat на Redshift в частности. Во всех других случаях, включая реализацию по умолчанию, я полностью доволен использованием реализаций, определенных в dbt_utils.concat.
{% macro concat(fields) -%}
{{ return(adapter.dispatch('concat')(fields)) }}
{%- endmacro %}
{% macro default__concat(fields) -%}
{{ return(dbt_utils.concat(fields)) }}
{%- endmacro %}
{% macro redshift__concat(fields) %}
{% for field in fields %}
nullif({{ field }},'') {{ ' || ' if not loop.last }}
{% endfor %}
{% endmacro %}
Если я работаю на Redshift, dbt будет использовать мою версию; если я работаю на любой другой базе данных, макрос concat() будет использовать версию, определенную в dbt_utils.
Для поддерживающих пакеты
Диспетчеризируемые макросы из пакетов должны предоставлять аргумент macro_namespace, так как это указывает пространство имен (пакет), где планируется искать кандидатов. Чаще всего это совпадает с именем вашего пакета, например, dbt_utils. (Возможно, хотя и редко желательно, определить диспетчеризируемый макрос не в пакете dbt_utils и диспетчеризировать его в пространство имен dbt_utils.)
Здесь у нас есть определение макроса dbt_utils.concat, который указывает как macro_name, так и macro_namespace для диспетчеризации:
{% macro concat(fields) -%}
{{ return(adapter.dispatch('concat', 'dbt_utils')(fields)) }}
{%- endmacro %}
Переопределение макросов пакета
Следуя второму примеру выше: Всякий раз, когда я вызываю свою версию макроса concat в своем собственном проекте, он будет использовать мою специальную версию с обработкой null на Redshift. Но версия макроса concat внутри пакета dbt-utils не будет использовать мою версию.
Почему это важно? Другие макросы в dbt-utils, такие как surrogate_key, вызывают макрос dbt_utils.concat напрямую. Что если я хочу, чтобы dbt_utils.surrogate_key использовал мою версию concat, включая мою пользовательскую логику на Redshift?
Как пользователь, я могу достичь этого через конфигурацию dispatch на уровне проекта. Когда dbt идет диспетчеризировать dbt_utils.concat, он знает из аргумента macro_namespace, что нужно искать в пространстве имен dbt_utils. Конфигурация ниже определяет динамическую маршрутизацию для этого пространства имен, указывая dbt искать через упорядоченную последовательность пакетов, вместо того чтобы просто искать в пакете dbt_utils.
dispatch:
- macro_namespace: dbt_utils
search_order: ['my_project', 'dbt_utils']
Обратите внимание, что эта конфигурация должна быть указана в корневом dbt_project.yml пользователя. dbt проигнорирует любые конфигурации dispatch, определенные в файлах проекта установленных пакетов.
Префиксы адаптеров все еще имеют значение: dbt будет искать только реализации, совместимые с текущим адаптером. Но dbt будет отдавать приоритет специфичности пакета над специфичностью адаптера. Если я вызываю макрос concat, работая на Postgres, с конфигурацией выше, dbt будет искать следующие макросы в порядке:
my_project.postgres__concat(не найден)my_project.default__concat(не найден)dbt_utils.postgres__concat(не найден)dbt_utils.default__concat(найден! используем его)
Как человек, устанавливающий пакет, эта функциональность позволяет мне изменить поведение другого, более сложного макроса (dbt_utils.surrogate_key), переопределив один из его модульных компонентов.
Как поддерживающий пакет, эта функциональность позволяет пользователям моего пакета расширять, переопределять или изменять поведение по умолчанию, не нуждаясь в форке исходного кода пакета.
Переопределение глобальных макросов
Некоторые функции, такие как ref, source и config, не могут быть переопределены с помощью пакета, используя конфигурацию dispatch. Это потому, что ref, source и config являются свойствами контекста в dbt и не диспетчеризируются как глобальные макросы. Обратитесь к этому обсуждению на GitHub для получения дополнительной информации.
Я поддерживаю внутренний пакет утилит в моей организации, названный my_org_dbt_helpers. Я использую этот пакет для переопределения встроенных макросов dbt от имени всех моих коллег, использующих dbt, которые работают в ряде проектов dbt.
Мой пакет может определять пользовательские версии любых диспетчеризируемых глобальных макросов, которые я выберу, от generate_schema_name до test_unique. Я могу определить новую версию этого макроса по умолчанию (например, default__generate_schema_name) или пользовательские версии для конкретных data warehouse адаптеров (например, spark__generate_schema_name).
Каждый корневой проект, устанавливающий мой пакет, просто должен включать конфигурацию dispatch на уровне проекта, которая ищет мой пакет перед dbt для глобального пространства имен dbt:
dispatch:
- macro_namespace: dbt
search_order: ['my_project', 'my_org_dbt_helpers', 'dbt']
Управление различными глобальными переопределениями в разных пакетах
Вы можете переопределять глобальные поведения различными способами для каждого проекта, который установлен как пакет. Это верно для всех глобальных макросов: generate_schema_name, create_table_as и т.д. При разборе или запуске ресурса, определенного в пакете, определение глобального макроса в этом пакете имеет приоритет над определением в корневом проекте, потому что оно более специфично для этих ресурсов.
Комбинируя переопределения на уровне пакета и dispatch, можно достичь трех различных шаблонов:
-
Пакет всегда выигрывает — Как разработчик моделей dbt в проекте, который будет развернут в другом месте как пакет, вы хотите полный контроль над макросами, используемыми для определения и материализации моих моделей. Ваши макросы всегда должны иметь приоритет для ваших моделей, и не должно быть никакого способа их переопределить.
- Механизм: Каждый проект/пакет полностью переопределяет макрос по его имени, например,
generate_schema_nameилиcreate_table_as. Не используйте dispatch.
- Механизм: Каждый проект/пакет полностью переопределяет макрос по его имени, например,
-
Условное применение (корневой проект выигрывает) — Как поддерживающий один проект dbt в сети из нескольких, ваша команда хочет условного применения этих правил. При запуске вашего проекта отдельно (в разработке) вы хотите применять пользовательское поведение; но при установке в качестве пакета и развертывании вместе с несколькими другими проектами (в производстве) вы хотите, чтобы применялись правила корневого проекта.
- Механизм: Каждый пакет реализует свое "локальное" переопределение, регистрируя кандидата для диспетчеризации с префиксом адаптера, например,
default__generate_schema_nameилиdefault__create_table_as. Корневой проект может затем зарегистрировать своего собственного кандидата для диспетчеризации (default__generate_schema_name), выигрывая порядок поиска по умолчанию или явно переопределяя макрос по имени (generate_schema_name).
- Механизм: Каждый пакет реализует свое "локальное" переопределение, регистрируя кандидата для диспетчеризации с префиксом адаптера, например,
-
Одни и те же правила везде и всегда — Как член команды платформы данных, ответственный за согласованность между командами в вашей организации, вы хотите создать "пакет макросов", который каждая команда может установить и использовать.
- Механизм: Создайте отдельный пакет только с кандидатами макросов, например,
default__generate_schema_nameилиdefault__create_table_as. Добавьте конфигурациюdispatchна уровне проекта вdbt_project.ymlкаждого проекта.
- Механизм: Создайте отдельный пакет только с кандидатами макросов, например,
Для поддерживающих адаптеры
Большинство пакетов изначально были разработаны для работы на четырех оригинальных адаптерах dbt. Используя макрос dispatch и конфигурацию проекта, можно "подогнать" существующие пакеты для работы на других адаптерах с помощью пакетов совместимости от третьих сторон.
Например, если я хочу использовать dbt_utils.concat на Apache Spark, я могу установить пакет совместимости, spark-utils, вместе с dbt-utils:
packages:
- package: dbt-labs/dbt_utils
version: ...
- package: dbt-labs/spark_utils
version: ...
Затем я включаю spark_utils в порядок поиска для диспетчеризируемых макросов в пространстве имен dbt_utils. (Я все еще включаю свой собственный проект первым, на случай, если я захочу переопределить какие-либо макросы с моей собственной пользовательской логикой.)
dispatch:
- macro_namespace: dbt_utils
search_order: ['my_project', 'spark_utils', 'dbt_utils']
При диспетчеризации dbt_utils.concat, dbt будет искать:
my_project.spark__concat(не найден)my_project.default__concat(не найден)spark_utils.spark__concat(найден! используем его)spark_utils.default__concatdbt_utils.postgres__concatdbt_utils.default__concat
Как поддерживающий пакет совместимости, мне нужно только переопределить основные строительные блоки макросов, которые инкапсулируют низкоуровневые синтаксические различия. Переопределяя низкоуровневые макросы, такие как spark__dateadd и spark__datediff, пакет spark_utils предоставляет доступ к более сложным макросам (dbt_utils.date_spine) "бесплатно".
Как пользователь dbt-spark, устанавливая dbt_utils и spark_utils вместе, я не только получаю доступ к более высоким уровням утилитных макросов. Я даже могу установить и использовать пакеты без специфической для Spark логики, которые никогда не тестировались на Spark, при условии, что они полагаются на макросы dbt_utils для совместимости между адаптерами.
Наследование адаптеров
Некоторые адаптеры "наследуются" от других адаптеров (например, dbt-postgres → dbt-redshift, и dbt-spark → dbt-databricks). Если используется дочерний адаптер, dbt также включит любые реализации родительского адаптера в свой порядок поиска. Вместо того чтобы просто искать redshift__ и возвращаться к default__, dbt будет искать redshift__, postgres__ и default__, в этом порядке.
Дочерние адаптеры, как правило, имеют очень похожий синтаксис SQL с их родителями, поэтому это позволяет им пропустить переопределение макроса, который уже был переопределен родительским адаптером.
Следуя примеру выше с dbt_utils.concat, полный порядок поиска на Redshift фактически:
my_project.redshift__concatmy_project.postgres__concatmy_project.default__concatdbt_utils.redshift__concatdbt_utils.postgres__concatdbt_utils.default__concat
В редких случаях дочерний адаптер может предпочесть реализацию по умолчанию специфической реализации родительского адаптера. В этом случае дочерний адаптер должен определить макрос, специфичный для адаптера, который вызывает реализацию по умолчанию. Например, синтаксис PostgreSQL для добавления дат должен работать и на Redshift, но я могу предпочесть простоту dateadd:
{% macro dateadd(datepart, interval, from_date_or_timestamp) %}
{{ return(adapter.dispatch('dateadd')(datepart, interval, from_date_or_timestamp)) }}
{% endmacro %}
{% macro default__dateadd(datepart, interval, from_date_or_timestamp) %}
dateadd({{ datepart }}, {{ interval }}, {{ from_date_or_timestamp }})
{% endmacro %}
{% macro postgres__dateadd(datepart, interval, from_date_or_timestamp) %}
{{ from_date_or_timestamp }} + ((interval '1 {{ datepart }}') * ({{ interval }}))
{% endmacro %}
{# Используйте синтаксис по умолчанию вместо синтаксиса postgres #}
{% macro redshift__dateadd(datepart, interval, from_date_or_timestamp) %}
{{ return(default__dateadd(datepart, interval, from_date_or_timestamp) }}
{% endmacro %}