Иерархия контекстов: Base / Macro / Provider / GenerateName
dbt-core имеет пять основных context classes, образующих иерархию. Каждый class — это «environment» для Jinja rendering: какие функции доступны, какие переменные, какой execute flag. Senior, который не знает разницы, регулярно сталкивается с «макрос работает в model, но не работает в pre-hook» — это разные contexts.
В этом уроке мы построим полное mental model иерархии — какие классы, кто кого extends, где какой используется.
Иерархия классов
Каждый child class добавляет объекты к parent. Например, ProviderContext имеет всё что у MacroResolvingContext + ref + source + this + model + adapter.
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 terminalreturn()— early exit из macroexceptions.raise_compiler_error,exceptions.warnmodules.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 для overridesgenerate_database_name(custom_name, node)generate_alias_name(custom_name, node)- Singular tests (without ref’d model)
on-run-start/on-run-endhooks (с 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 когда
| Where | Context | execute |
|---|---|---|
| profiles.yml rendering | TargetContext | N/A |
| dbt_project.yml rendering | ConfiguredContext | N/A |
| generate_schema_name() | GenerateNameContext | N/A |
| models/*.sql на parse | ProviderContext + ParseProvider | False |
| models/*.sql на execute | ProviderContext + RuntimeProvider | True |
| snapshots/*.sql | ProviderContext | depends on phase |
| macros/*.sql при вызове из model | ProviderContext | inherited from model context |
| macros/*.sql при вызове из hook | MacroContext (limited) | depends |
| Singular test (tests/*.sql) | ProviderContext | depends |
| materializations | ProviderContext + RuntimeProvider | True (materialization runs only on execute) |
| on-run-start/end hook | MacroContext or hybrid | True |
| dbt run-operation | MacroContext | True |
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.
Попробуй сам
- Откройте
core/dbt/context/providers.py. Найдитеclass ProviderContext. Прочитайте методы — это ~300 строк relevant content. - Найдите ParseProvider vs RuntimeProvider в том же файле. Сравните methods.
- Откройте
core/dbt/context/base.py. НайдитеBaseContext. Сравните с ProviderContext — что добавили? - В вашем проекте создайте 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 %}` - Запустите в разных 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.