Cross-project ref: project_dependencies.yml и резолвинг
В прошлом уроке разобрали Mesh как architectural pattern. Сейчас спускаемся на уровень механики: как именно dbt резолвит ref('other_project', 'model') через project_dependencies.yml.
Это знание нужно, чтобы:
- Setup-ить Mesh правильно.
- Debug-ить ошибки cross-project ref (“Project ‘finance’ not found”).
- Понимать failure modes (stale manifest, missing public access).
project_dependencies.yml: декларация зависимостей
project_dependencies.yml — отдельный файл рядом с dbt_project.yml. Он декларирует, на какие другие dbt-проекты этот зависит:
# marketing_dbt/project_dependencies.yml
projects:
- name: finance
- name: product_analytics
Минимальная декларация — только имя. dbt при parse:
- Читает
project_dependencies.yml. - Для каждого declared project пытается найти manifest этого проекта.
- Загружает manifest в memory.
- Регистрирует public models из manifest как доступные через
ref('finance', 'model_name').
Где dbt ищет manifest? Это самая важная часть setup.
Где dbt ищет cross-project manifests
Способов несколько, выбирается через --state или --defer-state:
Способ 1: dbt Cloud (managed)
В dbt Cloud все проекты в одном account имеют central manifest registry. dbt автоматически находит manifest по project name. Это zero-config — работает из коробки.
Способ 2: —state каталог для каждого проекта
В dbt-core (open source) нужно вручную указать каталоги:
dbt run --state path/to/finance-state/ path/to/product_analytics-state/
Внутри path/to/finance-state/ — файл manifest.json от последнего run проекта finance. dbt находит его по имени проекта из manifest (project_name field).
Способ 3: dbt-loom (community tool)
dbt-loom — open source extension, который:
- Скачивает manifests из S3/GCS/HTTP per project.
- Регистрирует их в dbt as cross-project sources.
- Автоматически refresh при каждом run.
Config:
# dbt-loom.config.yml
manifests:
- name: finance
type: s3
bucket: my-dbt-manifests
key: finance/manifest.json
- name: product_analytics
type: s3
bucket: my-dbt-manifests
key: product_analytics/manifest.json
Это самый популярный paттерн в open source. На каждом dbt run loom скачивает свежие manifests, ref()‘ы резолвятся.
Как ref(‘finance’, ‘fct_revenue’) работает внутри dbt
Trace через source code (упрощённо):
# В dbt/contracts/graph/nodes.py — упрощено
def resolve_cross_project_ref(project_name, model_name, manifest):
# 1. Найти manifest другого проекта
if project_name not in self.project_manifests:
raise CompilationError(f"Project '{project_name}' not found")
other_manifest = self.project_manifests[project_name]
# 2. Найти модель в этом manifest
target_node = None
for node in other_manifest.nodes.values():
if node.name == model_name and node.resource_type == 'model':
target_node = node
break
if target_node is None:
raise CompilationError(f"Model '{model_name}' not found in '{project_name}'")
# 3. Проверить access
if target_node.access != 'public':
raise CompilationError(
f"Model '{model_name}' in '{project_name}' has access='{target_node.access}'. "
f"Cross-project ref requires access='public'."
)
# 4. Вернуть relation (database.schema.table)
return target_node.relation_name # например, 'analytics.finance.fct_revenue'
dbt при компилировании SQL подставляет relation_name:
-- Source:
select * from {{ ref('finance', 'fct_revenue') }}
-- Compiled:
select * from analytics.finance.fct_revenue
После этого compiled SQL отправляется на warehouse. Warehouse физически читает данные из analytics.finance.fct_revenue — таблица, которую раньше создал finance dbt-проект.
Ограничение: physical access to data
dbt при cross-project ref подставляет имя таблицы — но warehouse должен иметь physical permissions к ней.
Пример: marketing dbt-run использует role MARKETING_TRANSFORMER. Эта role должна иметь SELECT на analytics.finance.fct_revenue:
-- Setup в Snowflake (run from admin)
GRANT USAGE ON SCHEMA analytics.finance TO ROLE MARKETING_TRANSFORMER;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics.finance TO ROLE MARKETING_TRANSFORMER;
GRANT SELECT ON FUTURE TABLES IN SCHEMA analytics.finance TO ROLE MARKETING_TRANSFORMER;
Без этих grants — compile_sql ОК, но runtime upadет с “Permission denied” или “Table does not exist”.
Самая частая ошибка в Mesh setup — забыли cross-team GRANT. dbt compile проходит (он только смотрит на manifest), но dbt run падает на warehouse permission error. Симптом: модель работала локально (один user, все permissions), не работает в CI (другая role).
Что включается в cross-project ref
Не любую модель можно cross-project ref. Требования:
access: public(см. урок 03).- Materialized —
table,incremental,view. Ephemeral модели нельзя ref cross-project (они inlined SQL, не имеют физической таблицы). - Имя model не overlap — если в обоих проектах есть
stg_users, ref должен быть с project name (ref('marketing', 'stg_users')).
Можно ли ref source? Нет. Sources — это локальные для проекта концепты. Чтобы expose source — нужно сделать view/table в проекте и mark public.
Versioned cross-project ref
Public model может иметь versions (concept из dbt-ii). Cross-project ref может pin to version:
-- Marketing uses pinned version
select * from {{ ref('finance', 'fct_revenue', v=2) }}
dbt резолвит к fct_revenue_v2. Это даёт Marketing safety от breaking changes — Finance может deprecate v1, добавить v2, Marketing pin to v2 explicitly.
Без version — dbt берёт latest_version:
# finance_dbt/models/schema.yml
models:
- name: fct_revenue
latest_version: 2
versions:
- v: 1
deprecation_date: 2026-12-31
- v: 2
ref('finance', 'fct_revenue') без version -> берёт v=2 (latest_version).
Детальнее versions разберём в уроке 04.
Loading cross-project manifest: performance
На больших Mesh-проектах с 5-10 dependencies — loading manifests добавляет overhead на parse. Реалистично:
| Setup | Parse time |
|---|---|
| Один проект, 500 моделей | 5-10s |
| Mesh, 5 проектов × 500 моделей | 15-25s (manifest load × 5) |
| Mesh, 10 проектов × 500 моделей | 30-50s |
dbt-loom добавляет network latency (download from S3). На 10 manifests — 2-5 секунд download. Можно кэшировать через dbt-loom --cache-manifests.
Failure modes
Production gotchas:
1. Project not found
CompilationError: Project 'finance' not found in project_dependencies.yml
Причина: либо забыли добавить в project_dependencies.yml, либо manifest для finance не загружен (dbt-loom misconfigured, или --state path неверный).
Fix:
- Проверить
project_dependencies.yml. - Проверить, что manifest finance существует и accessible.
- Запустить
dbt parse --debug— увидите, какие manifests dbt пытался загрузить.
2. Model not public
CompilationError: Model 'int_revenue_adjusted' in 'finance' has access='private'.
Cross-project ref requires access='public'.
Причина: marketing пытается ref Internal модель Finance. Finance не expose её.
Fix:
- Если модель должна быть public — Finance team добавляет
access: publicв schema.yml. - Если не должна — Marketing должна не использовать её. Можно сделать derived public model в Finance.
3. Stale manifest
CompilationError: Model 'fct_revenue' not found in 'finance'
Если эта модель была недавно создана. Причина — finance manifest на disk старый, не включает новую модель.
Fix:
- Запустить finance dbt-run, выгрузить новый manifest.
- На marketing — обновить cached manifest (через dbt-loom refresh).
4. Permission denied at runtime
Database error: Insufficient privileges to operate on schema 'finance'
Compile OK, run падает. Это physical access issue.
Fix:
- Admin делает GRANT через Snowflake/BQ.
- Marketing user должен иметь SELECT на finance schema.
5. Column drift с contracts
Database error: column 'amount_usd' not found in table 'fct_revenue'
Marketing ссылается на колонку, которую Finance переименовали. Если был contract — dbt не позволил бы Finance переименовать без bump version. Без contract — silent breakage.
Fix:
- Long-term: enforce contracts на public models.
- Short-term: Marketing обновляет SQL под новое column name.
Best practices для cross-project ref
-
Только через explicit ref(‘project’, ‘model’). Не делайте
select * from {{ source('finance_db', 'fct_revenue') }}— это обходит Mesh, нет lineage. -
access: publicявно — для каждой модели, которую другие проекты используют. Defaultprivate— safe-by-default. -
Contracts на public — обязательно. Без них public model не stable.
-
Versions для breaking changes — добавляйте v2, оставляйте v1 deprecated, дайте consumers migrate.
-
Documentation — public models должны иметь хорошие descriptions, examples в meta.
-
Slim CI поддержка — manifest sharing setup так, чтобы CI auto-update.
-
Monitoring deprecations — alert когда consumer ещё использует deprecated version.
DuckDB Mesh example (для обучения)
В labs мы покажем mini-Mesh на DuckDB через attach:
# marketing_dbt/profiles.yml
marketing_dbt:
target: dev
outputs:
dev:
type: duckdb
path: './marketing.duckdb'
attach:
- path: '../finance_dbt/finance.duckdb'
alias: finance_db
# marketing_dbt/project_dependencies.yml
projects:
- name: finance
# Sequence:
cd finance_dbt && dbt run # генерирует finance.duckdb + manifest.json
cd ../marketing_dbt && dbt run --state ../finance_dbt/target/
Marketing привязывает к finance.duckdb через attach (read-only access). ref('finance', 'fct_revenue') резолвится к finance_db.main.fct_revenue в attached DB.
Это упрощённый Mesh для обучения. В production — Snowflake/BQ multi-schema.
Sample setup script
Production-аккуратный setup CI для Mesh с polyrepo:
# .github/workflows/marketing-dbt-ci.yml
name: Marketing dbt CI
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Скачать manifest зависимостей
- name: Download finance manifest
run: |
aws s3 cp s3://dbt-manifests/finance/manifest.json target/finance-state/manifest.json
- name: Download product_analytics manifest
run: |
aws s3 cp s3://dbt-manifests/product_analytics/manifest.json target/product-state/manifest.json
- name: dbt parse
run: dbt parse --state target/finance-state/ target/product-state/
- name: dbt build
run: |
dbt build \
--select state:modified+ \
--state target/prod-state/ \
--defer
# Upload свой manifest для downstream проектов
- name: Upload marketing manifest
if: github.ref == 'refs/heads/main'
run: |
aws s3 cp target/manifest.json s3://dbt-manifests/marketing/manifest.json
Каждый проект на merge to main uploads свой manifest. Downstream projects на следующем CI run pick up latest.
DuckDB и cross-project: ограничения
В DuckDB через attach есть ограничения для cross-project:
- Read-only attach — Marketing не может писать в finance.duckdb. Только read.
- Concurrent attach — два процесса одновременно attached к одному .duckdb могут конфликтовать.
- Schema isolation — каждый проект пишет в свою DB, no shared catalog.
Это работает для educational scenarios. Production Mesh — другой warehouse.
Резюме
project_dependencies.yml— декларация cross-project dependencies.ref('project', 'model')— cross-project reference, резолвится через manifest другого проекта.- Manifest sharing — central piece infra. Через dbt Cloud / dbt-loom /
--statepath. - Compile vs runtime — compile проверяет manifest, runtime проверяет warehouse permissions.
access: publicобязательно — private/protected модели нельзя cross-project ref.- Versioning через
ref(..., v=2)для safety от breaking changes. - Failure modes: project not found, model not public, stale manifest, permission denied, column drift.
- DuckDB Mesh через
attach— для обучения, в prod Snowflake/BQ multi-schema.