dbt parse: отдельная команда и use cases
В предыдущих уроках мы видели parse как стадию pipeline dbt run. Но в dbt-core есть отдельная команда — dbt parse — которая выполняет только стадии 1-4 pipeline (CLI, RuntimeConfig, ManifestLoader, Linker) и записывает Manifest на диск.
В этом уроке мы разберём use cases команды, как использовать её программно через dbtRunner, как дебажить parsing issues через dbt parse --no-partial-parse, и paterns для CI/CD.
Что делает dbt parse
dbt parse
Выполняет:
- Load
RuntimeConfig(dbt_project.yml + profiles.yml). - Load
ManifestчерезManifestLoader(с partial parse если возможно). - Build
Graph(через Linker). - Write
target/manifest.json+target/partial_parse.msgpack. - Exit (без execution).
Не выполняет:
- SelectionSpec (не нужен — селекция на execute phase)
- GraphQueue
- Runners
- adapter.execute
dbt parse — это только парсинг.
# core/dbt/task/parse.py (упрощённо)
class ParseTask(ConfiguredTask):
def run(self):
# 1. Load Manifest
self.manifest = ManifestLoader.get_full_manifest(self.config, write_perf_info=True)
# 2. Compile manifest (build graph)
Compiler(self.config).compile(self.manifest)
# 3. Write manifest.json
write_manifest(self.manifest, self.config.target_path)
return self.manifest
write_perf_info=True — флаг для записи target/perf_info.json с временами parse phase, полезно для дебага.
Use case 1: prebuild Manifest для CI
В CI/CD pipeline часто полезно:
- Сначала parse + validate проекта (быстро, без execution).
- Если parse failed — fail fast (no SQL execution).
- Если success — proceed to execution с pre-loaded Manifest.
# .github/workflows/ci.yml (упрощённо)
jobs:
parse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install dbt-core dbt-duckdb
- run: dbt deps
- run: dbt parse # 30-60 sec, validates Manifest
- uses: actions/upload-artifact@v4
with:
name: dbt-manifest
path: target/manifest.json
run:
needs: parse
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install dbt-core dbt-duckdb
- uses: actions/download-artifact@v4
with:
name: dbt-manifest
path: target/
- run: dbt run --select state:modified+ --defer --state target/
dbt run --defer --state target/ — это canonical Slim CI pattern. --state target/ указывает dbt-core использовать prebuilt Manifest как baseline.
Benefit: parse считается один раз, не на каждом subsequent step. Если у вас 5 steps в CI (parse, test, run, lint, docs) — parse делается в один общий step, остальные читают artifact.
Use case 2: programmatic post-parse hooks
После dbt parse у вас есть target/manifest.json — гигантский JSON со всем проектом. Senior может писать инструменты, которые анализируют Manifest до execution.
Примеры:
Doc coverage check
import json
with open('target/manifest.json') as f:
manifest = json.load(f)
models = [n for n in manifest['nodes'].values() if n['resource_type'] == 'model']
no_desc = [m['name'] for m in models if not m.get('description')]
if no_desc:
print(f"FAIL: {len(no_desc)} models without description:")
for name in no_desc:
print(f" - {name}")
exit(1)
В CI:
- run: dbt parse
- run: python scripts/check_doc_coverage.py
Contract enforcement check
critical_marts = [
'fct_orders', 'dim_customers', 'agg_revenue'
]
for name in critical_marts:
node = next((n for n in manifest['nodes'].values() if n.get('name') == name), None)
if not node:
print(f"FAIL: model {name} not found")
exit(1)
if not node.get('contract', {}).get('enforced'):
print(f"FAIL: model {name} should have contract enforced")
exit(1)
Schema namespace check
prod_schemas = ['marts', 'staging', 'intermediate']
for node in manifest['nodes'].values():
if node['resource_type'] != 'model':
continue
if 'dev' in node['schema'] or 'ci' in node['schema']:
print(f"FAIL: {node['name']} has invalid schema {node['schema']}")
exit(1)
Эти проверки до execution. Если что-то не так — fail fast, no warehouse queries fired.
Use case 3: state comparison для Slim CI
dbt build --select state:modified+ --defer --state path/to/prev/manifest. Эта команда:
- Load
target/manifest.jsonтекущий (build после dbt parse). - Load
path/to/prev/manifest/manifest.json(от prod build). - Compare — найти
state:modifiednodes. - Select их и descendants (
state:modified+). --defer— для зависимостей вне modified set, использовать prod schemas.
dbt parse упрощает CI:
- run: dbt parse # genera new manifest.json
- run: dbt build --select state:modified+ --defer --state s3://my-bucket/prod-manifest/
--state принимает либо local path, либо URI (s3:// в новых версиях).
State comparison требует Manifest как input. dbt parse — это way to generate его без execution.
Use case 4: debug parsing issues
Когда что-то с парсингом не так:
# Forced full parse, ignore msgpack cache
dbt parse --no-partial-parse 2>&1 | tee parse.log
--no-partial-parse отключает кэш. Полезно когда:
- Подозреваете, что cache stale (rare, но bывает после crashes)
- Хотите чистый timing для baseline
- Хотите воспроизвести prod CI behavior локально (CI часто не имеет cache)
Debug log показывает:
DBT_LOG_LEVEL=debug dbt parse --no-partial-parse 2>&1 | grep -i error
Найдёт все warnings и errors:
- “static parser failure on model X”
- “model X refs unknown model Y”
- “duplicate macro foo defined in package bar”
- “YAML schema validation error in _models.yml”
Use case 5: warm-up partial parse cache
Чтобы избежать cold parse на первом dbt run в CI:
- run: dbt parse # writes partial_parse.msgpack
- run: dbt run --select my_model # uses partial parse cache
Хотя dbt run сам бы делал parse, но если у вас несколько subsequent commands:
- run: dbt parse # 30 sec — warm-up
- run: dbt deps # 5 sec
- run: dbt seed # 10 sec, uses cache
- run: dbt run # 5 sec parse + execution
- run: dbt test # 2 sec parse + execution
- run: dbt docs generate # 5 sec parse + catalog gen
Total parse cost — 30 + 4×~5 = 50 sec vs 5×30 = 150 sec без cache. 3x speedup просто за счёт явного warm-up step.
Programmatic invocation
dbtRunner API позволяет вызывать parse программно:
from dbt.cli.main import dbtRunner
runner = dbtRunner()
# Parse only
result = runner.invoke(["parse"])
if result.success:
manifest = result.result # Manifest object
print(f"Parsed {len(manifest.nodes)} nodes")
else:
print("Parse failed:")
for exc in result.exception or [result.exception]:
print(f" {exc}")
Это эквивалент dbt parse но без subprocess. Manifest доступен как Python object с type hints — удобнее, чем JSON.
# Используйте Manifest object
for unique_id, node in manifest.nodes.items():
if node.resource_type.value == 'model':
print(f"{node.name}: materialized={node.config.materialized}")
Модуль 06 курса детально разбирает dbtRunner API.
perf_info.json — где parse тратит время
При dbt parse пишется target/perf_info.json:
{
"path_count": 1245,
"is_partial_parse_enabled": true,
"is_static_analysis_enabled": true,
"node_counts": {
"model": 856,
"source": 234,
"test": 1402,
"snapshot": 12,
"seed": 45
},
"version": "1.11.5",
"elapsed": 4.32,
"projects": [
{
"project_name": "my_project",
"load_macros": 0.45,
"parse_project": 2.89,
"parse_yaml": 0.78,
"process_refs": 0.12,
"process_sources": 0.05
},
{
"project_name": "dbt_utils",
"load_macros": 0.18,
...
}
]
}
Используйте для:
- Identify slow steps — если
parse_yamlест 3 сек, у вас гигантские YAML files - Identify slow packages — если
dbt_utilsест больше своего main project, что-то странное - Track regression — store perf_info.json в git, sравнить между versions
Anti-pattern: dbt parse и потом dbt run без —defer
dbt parse # generates target/manifest.json (cold parse)
dbt run --select my_model # ALSO does parse internally
dbt run всё равно делает parse при старте (через partial parse cache). Так что вторая команда тратит ~3 сек на parse повторно (warm). Это не «двойной parse» — благодаря cache.
Но если вы хотите полностью пропустить parse в dbt run, нужна --defer --state target/ сoupling:
dbt parse
dbt run --defer --state target/ --select my_model
--state target/ — это путь к prebuilt manifest. Используется только для state comparison, не для skip parsing своего проекта. dbt run всё равно parsит свой проект.
True skip parse невозможен без programmatic API. Через dbtRunner можно передать pre-loaded Manifest:
runner = dbtRunner()
# Step 1: parse once
parse_result = runner.invoke(["parse"])
manifest = parse_result.result
# Step 2: pass manifest to subsequent invocations
runner = dbtRunner(manifest=manifest)
runner.invoke(["run", "--select", "model_a"]) # no re-parse!
runner.invoke(["run", "--select", "model_b"]) # no re-parse!
runner.invoke(["test", "--select", "model_a"]) # no re-parse!
Это самая мощная оптимизация для long-running processes (Dagster, Airflow), модуль 06.
Anti-pattern 2: dbt parse в CI без deps
- run: dbt parse # fails: "Package dbt_utils not installed"
Нужно dbt deps first:
- run: dbt deps # clones packages
- run: dbt parse
dbt parse требует, чтобы все packages были на disk (dbt_packages/). dbt deps загружает их.
В CI с cache: cache dbt_packages/ отдельно от target/. dbt deps fast (sec-level), но в большом проекте может быть 30+ sec на cold.
Попробуй сам
dbt parseна любом проекте:time dbt parse ls target/ cat target/perf_info.json | jq .- Прочитайте perf_info.json, найдите slow projects.
- Force full parse:
dbt parse --no-partial-parse - Compare manifest.json sizes:
Manifest.json — JSON, более читаемый, больше. Msgpack — binary, меньше, faster load.wc -c target/manifest.json wc -c target/partial_parse.msgpack - Programmatic parse via dbtRunner:
from dbt.cli.main import dbtRunner r = dbtRunner() result = r.invoke(["parse"]) manifest = result.result print(f"Nodes: {len(manifest.nodes)}") print(f"Sources: {len(manifest.sources)}") print(f"Macros: {len(manifest.macros)}") - Use parsed Manifest для analysis:
# Models with most dependencies nodes = [(uid, len(n.depends_on.nodes)) for uid, n in manifest.nodes.items()] nodes.sort(key=lambda x: -x[1]) for uid, dep_count in nodes[:10]: print(f"{uid}: {dep_count} deps")