Precedence и dispatch: 6 уровней
В реальном dbt-проекте materialization с одним именем может быть определён в трёх местах одновременно: в вашем проекте, в установленном пакете и в global project (встроенные в dbt-adapters). Если все три объявляют materialization audit — какой выберется?
Это precedence problem. dbt-core имеет чёткий алгоритм из 6 уровней приоритета. Знать его обязательно, потому что иначе вы будете дебажить «магически странное поведение» — модель использует не тот materialization, что вы думаете.
Materialization-aware dispatch (dbt II)Правило одной строкой
adapter-specific > default
local project > installed packages > global project (built-in)
На каждом из трёх «уровней проекта» (local / package / global) сначала ищется adapter-specific, потом default. Если ничего не найдено — спускаемся на следующий уровень.
Итого 6 точек поиска, в порядке убывания приоритета:
Где это в коде dbt-core
Алгоритм реализован в dbt-core/dbt/context/providers.py, метод _materialization_macro:
def _materialization_macro(
self,
materialization_name: str,
adapter_type: str,
) -> Optional[MacroProtocol]:
# 1. Local project, adapter-specific
local_specific = self.manifest.find_materialization_macro_by_name(
project_name=self.config.project_name,
materialization_name=materialization_name,
adapter_type=adapter_type,
)
if local_specific:
return local_specific
# 2. Local project, default
local_default = self.manifest.find_materialization_macro_by_name(
project_name=self.config.project_name,
materialization_name=materialization_name,
adapter_type='default',
)
if local_default:
return local_default
# 3-4. Installed packages (любой)
for package_name in self.manifest.macros_by_project_dependency_order():
package_specific = self.manifest.find_materialization_macro_by_name(
project_name=package_name,
materialization_name=materialization_name,
adapter_type=adapter_type,
)
if package_specific:
return package_specific
package_default = self.manifest.find_materialization_macro_by_name(
project_name=package_name,
materialization_name=materialization_name,
adapter_type='default',
)
if package_default:
return package_default
# 5-6. Global project (dbt-adapters)
# ... аналогично
Это упрощённая версия — реальная имеет дополнительные проверки на behavior flags и search_order. Но суть та же: проход по уровням от high к low priority.
Внутри одного «уровня» (например, между двумя пакетами) приоритет определяется search_order в dbt_project.yml пакетов или порядком установки. Это редкая ситуация — обычно пакеты не определяют одинаковые materializations.
Пример коллизии: project default vs package adapter-specific
Допустим, у вас есть:
Local project (my_project/macros/audit.sql):
{% materialization audit, default %}
-- Простая реализация: создаёт таблицу с audit_log колонкой
{% endmaterialization %}
Установленный пакет (dbt_audit_pkg/macros/audit.sql):
{% materialization audit, adapter='snowflake' %}
-- Продвинутая Snowflake-specific реализация с CDC и time travel
{% endmaterialization %}
Adapter: Snowflake.
Что выберется? — Package adapter-specific (уровень 3).
Это часто сюрприз для разработчиков, которые думают: «У меня в проекте есть materialization, он точно используется». Но adapter-specific важнее default, даже когда default — в local project, а adapter-specific — в пакете.
Как это узнать заранее:
dbt --debug compile --select test_audit
# В логах ищите:
# Found materialization for 'audit' from project 'dbt_audit_pkg' (adapter: snowflake)
Или через программную проверку:
import json
with open('target/manifest.json') as f:
manifest = json.load(f)
# Найти все materializations с именем 'audit'
for macro_id, macro in manifest['macros'].items():
if 'materialization_audit' in macro_id:
print(f"{macro['package_name']}.{macro_id}: {macro.get('macro_sql', '')[:80]}")
Когда возникают коллизии
В практике senior’а коллизии бывают в трёх ситуациях:
Случай 1: package overrides built-in
Пакет вроде dbt_external_tables определяет materialization external, default. Это перекрывает built-in external materialization из dbt-duckdb (которая materialization external, adapter='duckdb').
Что произойдёт:
Level 1 (local adapter): nothing
Level 2 (local default): nothing
Level 3 (package adapter): nothing (dbt_external_tables не имеет 'external, duckdb')
Level 4 (package default): MATCH — dbt_external_tables 'external, default'
Built-in adapter-specific (external, duckdb из dbt-duckdb) не выбирается, потому что пакет уже «съел» его на уровне 4.
Решение: либо удалить package если он не нужен, либо явно объявить materialization external, adapter='duckdb' в своём проекте (уровень 1):
-- macros/external_override.sql
{% materialization external, adapter='duckdb' %}
-- Скопировать реализацию из dbt-duckdb и использовать
{% endmaterialization %}
Случай 2: project overrides package
Пакет имеет audit materialization. Вы хотите кастомизировать только для своего проекта.
-- my_project/macros/audit_override.sql
{% materialization audit, default %}
-- Своя реализация — перекрывает package version
{% endmaterialization %}
Уровень 2 (local default) > уровень 4 (package default). Ваш override победит.
Но если пакет имеет adapter-specific version (уровень 3), а вы — только default (уровень 2) — пакет всё ещё выиграет на adapter’е, где есть его version!
Решение: либо явно объявить и adapter-specific:
-- my_project/macros/audit_override.sql
{% materialization audit, default %}
-- Универсальная version
{% endmaterialization %}
{% materialization audit, adapter='snowflake' %}
-- Snowflake-specific
{% endmaterialization %}
{% materialization audit, adapter='duckdb' %}
-- DuckDB-specific
{% endmaterialization %}
Либо понять, что вы action на packageless adapter’е, где package не имеет adapter-specific.
Случай 3: dbt-utils macro precedence (не совсем materialization)
Похожая логика применяется к обычным макросам в dbt — особенно для adapter.dispatch(). Если у вас есть macro current_timestamp в проекте и в dbt_utils, что выберется?
Это управляется через dispatch: секцию в dbt_project.yml:
dispatch:
- macro_namespace: dbt
search_order: ['my_project', 'dbt']
«Сначала искать в my_project, потом в dbt-default». Для materializations такого explicit конфига нет — порядок hardcoded в dbt-core.
Debugging precedence
Если model работает не так как ожидаете и подозреваете wrong materialization:
Шаг 1: —debug compile
dbt --debug compile --select my_model 2>&1 | grep -i materialization
Найдёте строки:
Building materialization 'audit' from package 'dbt_external_tables' (adapter: default)
Это говорит: используется уровень 4 (package default), а не то, что вы могли ожидать.
Шаг 2: проверка через manifest.json
import json
with open('target/manifest.json') as f:
manifest = json.load(f)
# Найти все материализации с именем X
target_name = 'audit'
for macro_id, macro in manifest['macros'].items():
if f'materialization_{target_name}_' in macro_id:
print(f"{macro['package_name']}.{macro['name']}")
Выведет:
my_project.materialization_audit_default
dbt_external_tables.materialization_audit_snowflake
dbt_external_tables.materialization_audit_default
Видно, что есть три кандидата. Зная precedence rules, можно вывести: на snowflake выберется dbt_external_tables.materialization_audit_snowflake (уровень 3), на остальных — my_project.materialization_audit_default (уровень 2).
Шаг 3: dbt run --debug и search в логах
21:23:45 Found materialization for 'audit' from package 'dbt_external_tables' (adapter: snowflake)
dbt-core версии 1.5+ пишет эту строку в —debug mode для каждой materialization-dispatch операции. Это самый прямой способ узнать, что используется.
Behavior flag: require_explicit_package_overrides
dbt 1.7 добавил behavior flag для безопасности при коллизиях. Если в вашем проекте есть materialization с именем, совпадающим с built-in (table, view, incremental, и т.д.), dbt предупредит:
Warning: Materialization 'view' is defined in your project, but you have not
explicitly opted in to overriding the built-in materialization.
Включить «strict» поведение:
# dbt_project.yml
flags:
require_explicit_package_overrides_for_builtin_materializations: true
С включённым флагом dbt падает если local project переопределяет built-in без explicit opt-in. Это защищает от accidental overrides.
Когда нужно: большие проекты с многими разработчиками, где есть риск, что кто-то случайно скопирует view.sql из dbt-adapters в свой проект и сломает поведение.
Реальный кейс: коллизия между dbt-duckdb и проектом
dbt-duckdb имеет встроенный materialized='external'. Допустим, мы написали свой (как в уроке 4) с именем my_external — конфликта нет.
Но что если мы назвали наш materialization просто external (как в research)?
Cases:
-
Если у нас в проекте:
{% materialization external, adapter='duckdb' %}:- Level 1: MATCH. Используется наш materialization.
- dbt-duckdb’s
materialization external, adapter='duckdb'не используется (level 5).
-
Если у нас:
{% materialization external, default %}:- Level 1: nothing (we don’t have adapter=‘duckdb’).
- Level 2: MATCH. Наш default используется.
- dbt-duckdb’s adapter-specific не используется, потому что уровень 2 победил уровень 5.
-
Если у нас вообще нет
external:- Levels 1-4: nothing.
- Level 5 (global adapter-specific): dbt-duckdb’s
materialization external, adapter='duckdb'. MATCH.
Точное знание этого позволяет аккуратно расширять / переопределять built-in без сюрпризов.
В прошлых уроках мы намеренно использовали имена my_view, my_external — чтобы избежать коллизий с built-in. В production советую делать так же: имена начинать с префикса проекта (mycompany_view, mycompany_external), чтобы коллизии были невозможны.
Как искать materialization в установленном пакете
Чтобы понять, какие materializations предоставляет пакет:
# Список всех файлов в установленном пакете
find dbt_packages/dbt_external_tables -name "*.sql" | xargs grep -l "materialization"
# Извлечь имена materializations
grep -h "{% materialization" dbt_packages/dbt_external_tables/macros/**/*.sql
Или через Python:
import json
with open('target/manifest.json') as f:
manifest = json.load(f)
# Все materializations из всех packages
for macro_id, macro in manifest['macros'].items():
if 'materialization_' in macro_id and macro['package_name'] != 'my_project':
print(f"{macro['package_name']}: {macro['name']}")
Полезный аудит перед добавлением пакета — посмотреть, какие materializations он принесёт.
Попробуй сам
-
Создайте
macros/my_view_override.sqlс{% materialization view, default %}(намеренно — чтобы переопределить built-in).`{% materialization view, default %}` `{{ log("USING CUSTOM VIEW MATERIALIZATION", info=True) }}` -- Простейшая реализация `{%- set target_relation = this.incorporate(type='view') -%}` `{% call statement('main') %}` CREATE OR REPLACE VIEW `{{ target_relation }}` AS (`{{ sql }}`) `{% endcall %}` `{{ adapter.commit() }}` {{ return({'relations': [target_relation]}) }} `{% endmaterialization %}` -
Создайте модель с
materialized='view'. Запуститеdbt run. В логах найдитеUSING CUSTOM VIEW MATERIALIZATION— это значит, ваш override победил built-in. -
Включите strict flag:
flags: require_explicit_package_overrides_for_builtin_materializations: trueЗапустите
dbt parse. Должны увидеть warning/error о том, что переопределяете built-in. -
Эксперимент: добавьте
{% materialization view, adapter='duckdb' %}(если на DuckDB). Запустите. Должен использоваться adapter-specific (уровень 1) вместо default (уровень 2). -
Уберите ваш
viewmaterialization. Установитеdbt_utilsили другой пакет. Черезmanifest.jsonпосмотрите, какие materializations имеет пакет (если есть).
Ключевые выводы
-
6 уровней precedence: local adapter -> local default -> package adapter -> package default -> global adapter -> global default. От high к low priority.
-
Adapter-specific важнее default на каждом уровне. То есть package adapter-specific победит local default — частый сюрприз.
-
Debug через
--debugcompile или manifest.json analysis — главные инструменты для понимания, какой materialization фактически выбран. -
Behavior flag
require_explicit_package_overrides_for_builtin_materializations(dbt 1.7+) защищает от случайных overrides built-in. -
Best practice: давайте custom materializations имена с префиксом проекта (
mycompany_external), чтобы избежать коллизий. -
Реальные коллизии возникают в трёх случаях: package overrides built-in, project overrides package, dispatch macros (контролируется через
dispatch:в project config).