Optional: Enriching with an LLM
Ingestion lands raw JSON. dbt turns those files into tested tables the backend can trust. This chapter is the final-project dbt map: which variables matter, how read_files works, which schemas you write, and how to model stg → int → marts in this repo. It is not a dbt fundamentals course - that was Week 10 and Week 13.
Run builds in Running the Pipeline Locally. Change models after you understand ingest (Ingestion, ACA, and CI/CD). Publish is a separate Python step (Publish to Postgres).
By the end of this chapter, you should be able to:
LANDING_PATH and DBT_SCHEMA for a personal Path A runread_files and schemaHintsEverything dbt lives under data/dbt/ in your team monorepo. Starter layout (teams B/C; team A may rename models for events):
data/dbt/
├── dbt_project.yml # project name, vars, materializations
├── profiles.yml # Databricks target (gitignored secrets via env)
├── .sqlfluff # SQL lint rules used in CI
├── models/
│ ├── staging/
│ │ ├── stg_postings.sql # read_files + rename + dedupe
│ │ └── _stg_postings.yml # tests on staging grain
│ ├── intermediate/ # optional joins / explode helpers
│ │ ├── int_posting_tags.sql
│ │ └── _int_posting_tags.yml
│ └── marts/
│ ├── fct_postings.sql
│ ├── fct_postings_enriched.py # optional enrichment (dict or LLM)
│ ├── fct_tag_popularity.sql
│ └── _fct_*.yml # tests on keys the backend reads
└── tests/ # singular SQL tests (not schema.yml)
├── assert_postings_not_empty.sql
└── …
| Path | You change it when… |
|---|---|
models/staging/ |
New source shape: read_files path, schemaHints, renames, dedupe |
models/intermediate/ |
Shared logic between staging and more than one mart |
models/marts/ |
Product tables / enrichment; keep names stable for publish when you can |
tests/ |
Cross-model checks that do not fit a single .yml test |
dbt_project.yml |
Default materializations, landing_path var wiring, model configs |
There is no models/sources/ folder for landing JSON. Staging reads the volume with read_files (next section). Profiles and connection env: Secrets & Variables.
These live in data/.env for Path A and Astro. Full tables: Secrets & Variables.
| Name | Typical Path A value | Role |
|---|---|---|
LANDING_PATH |
/Volumes/team_b/landing/dev/your-name (B/C parent) or …/events (team A full path) |
Fed into dbt as landing_path; staging read_files uses it |
DBT_SCHEMA |
dev_<yourname> |
Databricks schema where your models materialize |
DATABRICKS_CATALOG |
team_a / team_b / team_c |
Unity Catalog for your team |
DATABRICKS_HOST / DATABRICKS_HTTP_PATH / DATABRICKS_TOKEN |
From .env.example + personal PAT |
Warehouse connection |
LANDING_PREFIX is an ingest write setting. dbt never reads it. On Path A, the last segment of the parent LANDING_PATH must still match LANDING_PREFIX so you read the files you just wrote.
<aside>
💡 Team path shapes. Teams B/C: LANDING_PATH is the parent only; staging SQL appends /postings. Team A: LANDING_PATH includes /events and staging matches that full path. Copy .env.example from your own repo.
</aside>
On the team Airflow VM, the same ideas are Airflow Variables (LANDING_PATH, LANDING_PATH_DEV, DBT_SCHEMA, …), not your laptop .env. Update them when you promote: ch12.
| Runner | Databricks schema | Landing dbt should read |
|---|---|---|
| Path A / Astro Mode 1 | dev_<yourname> |
Personal …/dev/<you>/… |
| Astro Mode 2 | still dev_<yourname> |
Shared …/dev/aca-dev/… (job wrote there) |
| Mode 3 (VM) | shared dev_airflow |
…/dev/aca-dev/… |
| Mode 4 (VM) | analytics |
…/landing/prod/… |
Your laptop cannot write analytics. Mode 4 is merge + the prod DAG only.
flowchart LR
files[JSON in landing volume]
stg[stg_*]
int[int_* optional]
mart[fct_* / dim_*]
files --> stg --> int --> mart
stg --> mart
read_files and schema hintsThere is no dbt source() table for raw landing files. Staging calls Databricks read_files over a volume path. That is why lineage in dbt docs starts at staging - the API → file hop lives in Airflow and ingest, not in dbt. Architecture detail: Project Architecture.
Typical teams B/C pattern from the starter stg_postings.sql (parent landing_path + /postings in SQL):
read_files(
'{{ var("landing_path") }}/postings'
, format => 'json'
, schemaHints => 'slug string, title string, …, created_at bigint'
)
schemaHints must match source field names and types on disk, not your cleaned mart column names. After you swap APIs, open a landed JSON file and align hints with what ingest actually wrote. Nested fields need nested types in the hint.
Then a rename CTE maps to clean names (posting_id, posted_at, …). Fix date casts to match the bytes: Arbeitnow often uses unix seconds (timestamp_seconds); many feeds use ISO strings. Leaving the wrong cast after a source swap breaks dbt or produces nonsense dates.
Keep a dedupe (qualify row_number()…) so unique tests survive daily re-listings.
<aside>
⌨️ Hands on: Open your team's stg_*.sql and find the read_files call. Note whether it appends /postings or relies on a full LANDING_PATH. Compare schemaHints keys to one sample JSON from the starter feed.
</aside>
Use dbt layer names (Week 10): staging, optional intermediate, then marts.
| Layer | Folder | Job |
|---|---|---|
| Staging | data/dbt/models/staging/ |
One row grain from files; rename; light casts; dedupe; tests on keys |
| Intermediate | data/dbt/models/intermediate/ (if present) |
Joins and business logic that are not yet a product table |
| Marts | data/dbt/models/marts/ |
Tables the backend (and publish) will read; tests on query keys |
Keep or rename. Prefer keeping model names stg_postings / fct_postings while you change columns, so publish defaults stay stable. Rename only when the domain name matters for the backend (for example jobs). If you rename, update publish in the same change.
When you leave the starter feed, update models in the same change set as ingest:
read_files at the right folder; refresh schemaHints and rename CTE..yml tests (unique keys, not_null on the grain).fct_* (and any int_*) at the new staging columns. Drop starter-only enrichment you do not need; keep or replace fct_postings_enriched if publish still reads it..yml tests on the keys the backend will query (dbt Tests).dbt build and open the mart in Catalog Explorer before you call Path A done.Enrichment (LLM or domain rules) is optional and lives as a dbt Python model when you need it: LLM enrichment.
<aside> ❗ Thin ingestion, rich dbt. Do not move joins and cleaning into Pandas in the ingest package unless a teacher agrees. SQL in dbt is easier to test and review.
</aside>
Publish to Postgres assumes you know which mart name publish will read.
data/dbt/models/{staging,intermediate,marts} and say what each ownsLANDING_PATH and DBT_SCHEMA for a personal runread_files + schemaHints vs cleaned column namesdev_<you>, dev_airflow, and analytics are different writersNext up: Publish to Postgres, then Running the Pipeline Locally.