Learning Platform
Глоссарий Troubleshooting
Урок 04.02 · 24 мин
Продвинутый
jinjacontextproviders

Иерархия контекстов: Base / Macro / Provider / GenerateName

dbt-core имеет пять основных context classes, образующих иерархию. Каждый class — это «environment» для Jinja rendering: какие функции доступны, какие переменные, какой execute flag. Senior, который не знает разницы, регулярно сталкивается с «макрос работает в model, но не работает в pre-hook» — это разные contexts.

В этом уроке мы построим полное mental model иерархии — какие классы, кто кого extends, где какой используется.


Иерархия классов

Context hierarchy в dbt-core
BaseContextБазовый context. Минимум: var, env_var, log, return, exceptions, modules. Используется для raw Jinja rendering (например, в configs профайла).
extends
TargetContextAdds target object. Используется при рендере profiles.yml.
SecretContextAdds env_var with privileged access. Internal, для secrets management.
extends
ConfiguredContextProject + Profile rendered. Adds project_name, target, fully-configured environment. Используется для rendering YAML конфигов до полной parse phase.
extends
MacroResolvingContextConfiguredContext + macro resolution. Для контекстов, где нужно вызывать macros (например, generate_schema_name, get_columns_in_relation).
extends
MacroContextДля rendering self-contained макросов (singular tests, hooks без model context). Adds full macro support.
ProviderContextГлавный context для моделей и snapshots. Adds ref, source, this, model, adapter, statement, run_query.
ParseProviderexecute=False, для parse phase. statement/run_query/adapter methods return dummy.
RuntimeProviderexecute=True, для execute phase. Все методы работают по-настоящему.
GenerateNameContextSpecialized: для generate_schema_name / generate_database_name / generate_alias_name override macros. Limited scope.

Каждый child class добавляет объекты к parent. Например, ProviderContext имеет всё что у MacroResolvingContext + ref + source + this + model + adapter.

Наследование, MRO, C3 linearization в Python generate_schema_name override: per-developer и clean prod schemas

BaseContext — минимальный

core/dbt/context/base.py:

class BaseContext:
    def __init__(self, cli_vars):
        self.cli_vars = cli_vars
    
    @contextproperty
    def var(self):
        return Var(self.cli_vars)
    
    @contextproperty
    def env_var(self):
        return env_var  # function
    
    @contextproperty
    def log(self):
        return log  # function
    
    @contextproperty
    def return_(self):
        return return_  # specifically named because 'return' reserved
    
    @contextproperty
    def exceptions(self):
        return exceptions_jinja  # namespace
    
    @contextproperty
    def modules(self):
        return modules  # Python modules whitelist: datetime, pytz, re, itertools

Доступно в BaseContext:

  • var(), env_var()
  • log() — write to terminal
  • return() — early exit из macro
  • exceptions.raise_compiler_error, exceptions.warn
  • modules.datetime, modules.pytz, modules.re, modules.itertools — whitelist Python modules

Where used: rendering profiles.yml через ProfileRenderer. Когда вы пишете password: "{{ env_var('DB_PASSWORD') }}" в profiles.yml, dbt использует BaseContext для рендера.

Не доступно: ref(), source(), target, this, model, adapter, run_query, statement.


TargetContext — adds target

core/dbt/context/target.py:

class TargetContext(BaseContext):
    @contextproperty
    def target(self):
        return self._target_dict  # {name, type, schema, database, ...}

target — это dict с info про active target из profiles.yml:

target.name          # 'dev'
target.type          # 'duckdb'
target.database      # 'analytics'
target.schema        # 'main'
target.threads       # 4
target.user          # 'alice'
# adapter-specific: target.warehouse, target.role, etc.

Where used: profiles.yml rendering (после BaseContext), некоторые internal configs где нужен target.


ConfiguredContext — full project + profile

core/dbt/context/configured.py:

class ConfiguredContext(BaseContext):
    def __init__(self, cli_vars, profile, project, target):
        super().__init__(cli_vars)
        self.profile = profile
        self.project = project
        self.target = target
    
    @contextproperty
    def target(self):
        return self._target
    
    @contextproperty
    def project_name(self):
        return self.project.project_name
    
    @contextproperty
    def vars(self):
        return self._merged_vars  # CLI + project + profile vars

ConfiguredContext = BaseContext + target + project info.

Where used: rendering dbt_project.yml после profiles.yml loaded. Когда вы пишете +materialized: "{{ 'table' if target.name == 'prod' else 'view' }}" в dbt_project.yml, используется ConfiguredContext (или derivative).


MacroResolvingContext + MacroContext

core/dbt/context/macros.py (в реальности в providers.py):

class MacroResolvingContext(ConfiguredContext):
    def __init__(self, ...):
        super().__init__(...)
        self.macros = ManifestMacroNamespace(self.manifest)
    
    def __getitem__(self, item):
        # Custom getattribute — позволяет {{ my_macro() }} в Jinja
        if item in self.macros:
            return self.macros[item]
        return super().__getitem__(item)

MacroContext — для cases где нужно run macros но без model context:

  • generate_schema_name(custom_name, node) — macro для overrides
  • generate_database_name(custom_name, node)
  • generate_alias_name(custom_name, node)
  • Singular tests (without ref’d model)
  • on-run-start / on-run-end hooks (с partial model context)

Эти macros имеют access к target, vars, project info, и к другим macros. Но не к ref/source/this (потому что нет «текущей модели»).


ProviderContext — главный для моделей

core/dbt/context/providers.py — самый важный файл (~1200 строк).

class ProviderContext(MacroResolvingContext):
    def __init__(self, model, config, manifest, provider, ...):
        super().__init__(...)
        self.model = model  # current model node
        self.provider = provider  # ParseProvider or RuntimeProvider
    
    @contextproperty
    def ref(self):
        return self.provider.ref  # ParseProvider или RuntimeProvider method
    
    @contextproperty
    def source(self):
        return self.provider.source
    
    @contextproperty
    def this(self):
        # Relation object для current model
        return Relation.create_from(self.config, self.model)
    
    @contextproperty
    def adapter(self):
        return self.provider.adapter
    
    @contextproperty
    def model(self):
        # Dict с метаданными model (для introspection)
        return self.model.to_dict()
    
    @contextproperty
    def execute(self):
        return self.provider.execute
    
    @contextproperty
    def statement(self):
        return self.provider.statement
    
    @contextproperty
    def run_query(self):
        return self.provider.run_query

ProviderContext = MacroResolvingContext + ref + source + this + model + adapter + statement + run_query + execute.

Where used:

  • Rendering model SQL (most common)
  • Rendering snapshot SQL
  • Rendering custom materializations
  • Compiled SQL post-processing

Два провайдера:

class ParseProvider:
    execute = False
    def ref(self, ...): # register dependency, return placeholder
    def source(self, ...): # register, return placeholder
    def adapter(self): # ParseTime adapter (most methods dummy)
    def statement(self, ...): # do nothing
    def run_query(self, ...): # return None

class RuntimeProvider:
    execute = True
    def ref(self, ...): # return real Relation
    def source(self, ...): # return real Relation
    def adapter(self): # full adapter
    def statement(self, ...): # send SQL
    def run_query(self, ...): # send SQL, return agate.Table

GenerateNameContext — специализированный

Для generate_schema_name, generate_database_name, generate_alias_name macros — специальный context:

class GenerateNameContext(BaseContext):
    def __init__(self, ...):
        super().__init__(...)
        self.target = target
        self.node = node  # current node для которого generates name
    
    # Limited: only var, env_var, target, node

Doesn’t have: ref, source, this, adapter, statement.

Это потому что name generation должен быть deterministic и не зависеть от warehouse state.

Пример override macro:

{% macro generate_schema_name(custom_schema_name, node) %}
  {% if target.name == 'prod' %}
    {{ custom_schema_name | trim or target.schema }}
  {% else %}
    {{ target.schema }}_{{ custom_schema_name | trim if custom_schema_name else 'default' }}
  {% endif %}
{% endmacro %}

target и node available. ref, source, this — нет. run_query — нет.


Какой context когда

WhereContextexecute
profiles.yml renderingTargetContextN/A
dbt_project.yml renderingConfiguredContextN/A
generate_schema_name()GenerateNameContextN/A
models/*.sql на parseProviderContext + ParseProviderFalse
models/*.sql на executeProviderContext + RuntimeProviderTrue
snapshots/*.sqlProviderContextdepends on phase
macros/*.sql при вызове из modelProviderContextinherited from model context
macros/*.sql при вызове из hookMacroContext (limited)depends
Singular test (tests/*.sql)ProviderContextdepends
materializationsProviderContext + RuntimeProviderTrue (materialization runs only on execute)
on-run-start/end hookMacroContext or hybridTrue
dbt run-operationMacroContextTrue

Senior gotcha: hook context limitation

-- on-run-start hook в dbt_project.yml
on-run-start:
  - "{% set models = dbt.models | length %}{{ log('Found ' ~ models ~ ' models') }}"

dbt namespace и models в hook context — это NOT same as ProviderContext’s model. Hook доступ к graph object (compiled Manifest) ограниченно.

on-run-end:
  # Эти работают в hook context:
  - "{{ log('Run finished', info=True) }}"
  - "{{ adapter.commit() }}"  # commit any pending transactions
  - "GRANT USAGE ON SCHEMA {{ target.schema }} TO analyst_role"  # if execute
  
  # Эти НЕ работают (no ref, no this):
  - "SELECT * FROM {{ ref('foo') }}"  # FAIL — no ref in hook context

Для hooks с complex logic — extract в macro:

on-run-end:
  - "{{ my_grants_macro() }}"
-- macros/my_grants_macro.sql
{% macro my_grants_macro() %}
  {% if execute %}
    {% for node in graph.nodes.values() %}
      {% if node.resource_type == 'model' %}
        GRANT USAGE ON {{ node.relation_name }} TO analyst_role;
      {% endif %}
    {% endfor %}
  {% endif %}
{% endmacro %}

graph object в hook context contains parsed Manifest — можно итерировать nodes.


Дебаг “какой context?”

В любом Jinja коде можно вывести type context’а:

{{ log("Context class: " ~ self.__class__.__name__, info=True) }}
{{ log("execute: " ~ execute, info=True) }}

Это покажет в логах:

Context class: RuntimeProvider
execute: True

Helpful для дебага «почему здесь нет ref». Если context — GenerateNameContext или MacroContext, ref недоступен — это by design.


Попробуй сам

  1. Откройте core/dbt/context/providers.py. Найдите class ProviderContext. Прочитайте методы — это ~300 строк relevant content.
  2. Найдите ParseProvider vs RuntimeProvider в том же файле. Сравните methods.
  3. Откройте core/dbt/context/base.py. Найдите BaseContext. Сравните с ProviderContext — что добавили?
  4. В вашем проекте создайте macro:
    `{% macro inspect_context() %}`
      `{{ log("Class: " ~ self.__class__.__name__, info=True) }}`
      `{{ log("execute: " ~ execute, info=True) }}`
      `{{ log("has ref: " ~ (ref is defined), info=True) }}`
      `{{ log("has run_query: " ~ (run_query is defined), info=True) }}`
      `{{ log("target.name: " ~ target.name, info=True) }}`
    `{% endmacro %}`
  5. Запустите в разных contexts:
    # В model body — ProviderContext
    echo "SELECT 1 as x" > models/test_ctx.sql
    echo "`{{ inspect_context() }}`" > models/test_ctx2.sql
    dbt compile --select test_ctx2 2>&1 | grep -E "Class|execute|has|target"
    
    # В hook — MacroContext (with limited access)
    # Edit dbt_project.yml:
    # on-run-start: ["`{{ inspect_context() }}`"]
    dbt run --select test_ctx 2>&1 | grep -E "Class|execute|has|target"

Это даст visceral понимание разницы contexts.


Проверка знанийKnowledge check
Вы пишете макрос audit_log_finish() который должен в конце run сделать INSERT в audit table с info о всех модели run. Вызываете его из on-run-end. В macro используете graph.nodes для итерации, this для текущей модели, и run_query для INSERT. Часть работает, часть нет. Что в каждой из этих trёх вызовов будет работать в hook context, что нет, и как исправить?
ОтветAnswer
(1) graph.nodes — works в hook context. После parse phase Manifest рендерится в hook через graph object. Можно итерировать через graph.nodes.values() и фильтровать по resource_type, status, etc. Это корректный способ access метаданных всех моделей. (2) this — NOT works в hook context. this — это Relation для current model, но в hook нет "current model" — hook runs once для всего run, не per-model. Если попытаетесь использовать this, либо undefined либо мусор. Fix: итерируйте через graph.nodes, для каждого node делайте node.relation_name (это fully-qualified table name). (3) run_query — works если execute=True. В on-run-end execute=True (это runtime phase). Но: убедитесь, что execute проверка явно в макросе для safety: {% if execute %} {% set result = run_query(insert_sql) %} {% endif %}. Иначе на parse phase падает (parse рендерит hooks тоже для validation). Sustainable solution: '''sql {% macro audit_log_finish() %} {% if execute %} {% set successful_runs = [] %} {% for node in graph.nodes.values() %} {% if node.resource_type == 'model' %} {% set run_result = (node | get_run_result) %} -- pseudocode {% if run_result and run_result.status == 'success' %} {{ successful_runs.append({'model': node.name, 'rows': run_result.adapter_response.rows_affected}) }} {% endif %} {% endif %} {% endfor %} {% if successful_runs %} {% set sql = "INSERT INTO audit.run_log VALUES " ~ values_clause(successful_runs) %} {{ run_query(sql) }} {% endif %} {% endif %} {% endmacro %} '''. Note: run_result access из hook не направно — это часть API на newer versions через results variable или through hooks. Production approach — store run_results.json в SQS/S3 на CI, process отдельно для observability, не в dbt hooks.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Senior пишет on-run-end hook macro, который должен log info про executed models. Использует ref(...) и this — оба undefined в hook. Почему?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5