SOURCES.YML
The sources.yml file declares where your raw data lives. It maps database tables to source references that your models can use with {{ source('name', 'table') }}.
THE CONNECTION: PROPERTY
The connection:property is DVT's key extension to dbt's sources.yml. Declared under the source's meta:block (dbt's schema only allows custom keys there), it links a source to a specific output in profiles.yml, telling DVT which database this data lives on.
Every source — including default-target ones
Give every source a meta.connection naming its profiles.yml output. dbt ignores meta entirely, so it is harmless for standard models — and it is what federated models use to find the data. Bonus: when you later switch the default target, sources that already carry their connection keep working without edits.
The fallback when meta.connection is absent
If a source has no meta.connection, DVT falls back to the source's own name: a source called mysql_ops binds to the mysql_ops output. If neither resolves, federated models reading that source fail at extraction (DVT011). Standard models are unaffected — dbt resolves them from database/schema alone.
EXAMPLE
# models/staging/sources.yml
version: 2
sources:
# ─── External Sources (need connection:) ───
- name: crm
description: "CRM data from MySQL operational database"
meta:
connection: mysql_ops # ← REQUIRED: points to profiles.yml
tables:
- name: customers
description: "Customer master data"
- name: orders
description: "Sales orders"
- name: products
- name: warehouse
description: "ERP data from Snowflake"
meta:
connection: sf_warehouse # ← REQUIRED: points to profiles.yml
database: PROD_DB
schema: RAW
tables:
- name: invoices
- name: shipments
- name: vendors
- name: data_lake
description: "Raw files from S3"
meta:
connection: data_lake # ← works with bucket storages too
tables:
- name: web_events
- name: app_logs
# ─── Default Target Sources ───
- name: analytics
description: "Tables on the default PostgreSQL target"
schema: public
meta:
connection: pg_dev # ← recommended even here: dbt ignores
tables: # it, and a future target switch is free
- name: seed_categories
- name: dim_dates
- name: historical_metricsHOW DVT USES SOURCES
The execution path is chosen by the model's materialized config — not by the sources. What meta.connection controls is where a federated model extracts each source from:
| SOURCE CONFIG | WHAT HAPPENS | SQL DIALECT |
|---|---|---|
| Standard model (table / view / incremental) | Sources on the default target: dbt resolves database/schema and runs natively. Sources on a foreign connection: DVT coerces the model to f_table / f_incremental automatically, with a warning | Target's native SQL (DuckDB SQL if coerced) |
| Federated model (f_table / f_incremental) | Each source extracted from its meta.connection (or name-matched output) via Sling into DuckDB | DuckDB SQL |
A standard model should only read sources that live on the default target. The moment a model needs data from another engine, declare it federated yourself — DVT will coerce it for you rather than fail, but an explicit materialization states your intent and silences the warning. See f_table on the federated materializations page.
COMMON PATTERNS
Multi-source extraction model:
-- models/marts/cross_engine_report.sql
-- crm is on MySQL, warehouse is on Snowflake
-- Both get extracted → DuckDB joins them → result loads to default target
{{ config(materialized='f_table') }}
SELECT
c.customer_name,
i.invoice_date,
i.total_amount
FROM {{ source('crm', 'customers') }} c -- MySQL (extracted)
JOIN {{ source('warehouse', 'invoices') }} i -- Snowflake (extracted)
ON c.customer_id = i.customer_idDefault-target pushdown model:
-- models/staging/stg_categories.sql
-- analytics source has no connection: → pushdown on default target
-- SQL dialect: PostgreSQL (native)
{{ config(materialized='view') }}
SELECT id, name, parent_id
FROM {{ source('analytics', 'seed_categories') }}Bucket storage source:
-- models/staging/stg_web_events.sql
-- data_lake source points to S3 → extracted via Sling
{{ config(materialized='f_table') }}
SELECT event_id, user_id, event_type, event_timestamp
FROM {{ source('data_lake', 'web_events') }}SOURCE PROPERTIES REFERENCE
| PROPERTY | REQUIRED | DESCRIPTION |
|---|---|---|
| name | yes | Unique name for this source group |
| meta.connection | no | profiles.yml output name. Recommended on every source; without it the source name itself must match an output for federated reads. |
| database | no | Database name (overrides the output's default) |
| schema | no | Schema name (overrides the output's default) |
| description | no | Human-readable description for documentation |
| tables | yes | List of table objects with at minimum a name field |
| tags | no | Tags for source selection (dvt run --select tag:...) |
| freshness | no | Freshness check configuration (standard dbt) |
COMMON MISTAKES
WRONG
sources:
- name: analytics
connection: pg_dev # ← top level: dbt REJECTS this
tables:
- name: dim_datesCORRECT
sources:
- name: analytics
meta:
connection: pg_dev # ← under meta: dbt allows, DVT reads
tables:
- name: dim_datesconnection: must live under meta:. dbt validates sources.yml against its schema and errors on unknown top-level keys — meta is dbt's sanctioned extension point, which is exactly why DVT (a wrapper, not a fork) uses it.
WRONG
sources:
- name: crm
# Missing connection!
tables:
- name: customers # lives on MySQLCORRECT
sources:
- name: crm
meta:
connection: mysql_ops # external source
tables:
- name: customersExternal sources must have connection:. Without it, DVT warns and falls back to a profiles.yml output named after the source ('crm') — and if no such output exists, extraction fails with DVT011. Add meta.connection to every source: one rule, no exceptions.