What you will be able to do
- Reproduce a failure on a single model and confirm a patch fixes it
- Select the patched model and everything downstream of it for a verification build
- Add a unit test that keeps a fixed bug from coming back
- Explain what a dbt CI job checks on a pull request before the fix is merged
Key concept
Fix-then-verify loop — Shrink the failure to the smallest thing that reproduces it, patch it, then rebuild and test the patched model and everything that depends on it, first in development and then in CI, before the change reaches production.
1.Reproduce the failure on one model, then patch it
A fix you cannot reproduce is a fix you cannot verify. Before you edit anything, cut the failure down to the smallest invocation that still shows it. Usually that means running only the model named in the error, and if the cause is still unclear, undoing the change that broke it. Once you know which error type you have and which file it points to (see [reading dbt error messages](lesson:3.1)), each further step either confirms your guess or rules it out.
Database Errors need extra care, because the warehouse wrote the message and dbt only passes it on. In most cases the mistake is in the model's own SQL. Open the model and its compiled SQL side by side, as covered in [troubleshooting with compiled code](lesson:3.2). Then run the compiled query by itself in a query runner, or with the Preview button in the Studio IDE. If that query fails, the problem is in the SQL itself and not in how dbt runs it. Fix the model, then rerun the model that failed, not the whole project.
Some failures come from queries dbt runs behind the scenes: hooks, schema creation, introspection, or the merge and insert statements of incremental models and snapshots. None of these appear in the model file, so logs/dbt.log is where you look. Its contents build up over many runs. Empty the file, re-execute dbt run for only the failing model, and the log then holds just the queries from that one attempt.
Checkpoint 1 of 7· Put it in order
A model fails with a Database Error. Put the fix-and-confirm steps in order.
- 1.Re-execute the compiled query on its own to isolate the error
- 2.Fix the mistake in the model
- 3.Open the model file next to its compiled SQL
- 4.Rerun the failed model
Running the compiled query on its own shows where the error comes from before you change anything. Rerunning the failed model afterwards confirms the patch worked.
“Try to re-execute the SQL to isolate the error”Source: docs.getdbt.com
Sources1
2.Rebuild the patched model and everything downstream
When the failed model builds again, you have proved only that one model works. A patch can rename a column, change the grain, or drop rows, and every child model reads that output. To check the patch properly, select the model and all its descendants. Put the + graph operator to the right of the model name: dbt build --select model_name+. With + on the left you get the model's ancestors instead, which rebuilds what feeds the model and leaves untested the models that depend on the fix.
# select my_model and all of its children
dbt run --select "my_model+"
# select my_model, its children, and the parents of its children
dbt run --select @my_modelUse dbt build for this step, not dbt run. build runs seeds, models, snapshots and tests together in DAG order, so a data test on a child model checks your patch in the same command that builds it. When a node fails, dbt skips every node that depends on it, and those skips show you how far the problem spreads. To check the selection before spending warehouse time, dbt ls with the same selector prints the nodes it matches.
Two cases need more than a plain rebuild. If you changed the logic of an incremental model, rows that were built earlier still hold the old result, so rebuild the model from scratch with --full-refresh. If the fix is a refactor that should not change any output, the dbt_audit_helper package compares the results of two queries so you can confirm the old and new versions return the same rows.
Checkpoint 2 of 7· Check yourself
You patched stg_orders. Which command verifies the patch against every model that consumes it, and runs their tests too?
A trailing + adds all descendants, and build runs tests along with the models. A leading + selects ancestors, which leaves the downstream consumers untested.
“Placed after a model/resource — Includes the resource itself and all its descendants (downstream dependencies).”Source: docs.getdbt.com
Checkpoint 3 of 7· Exam question
A `stg_orders` model fails with a database error. You open the compiled file under `target/run/` and find the executed statement references a column named `oder_id` instead of `order_id`, a typo you introduced in the model's source `.sql` file. After correcting the typo, which command rebuilds this model together with every downstream model and test that depends on it, before you open a pull request?
Correct answer: A — dbt build --select stg_orders+
- A. The `+` after the model name selects every descendant of `stg_orders`, and `dbt build` materializes each selected model and runs its tests, so this rebuilds the fix and verifies nothing downstream broke.
- B. This only rematerializes `stg_orders` itself; it neither runs any tests nor touches the downstream models that consume this fixed column, so the fix is unverified beyond the single model.
- C. Compiling only renders the Jinja into SQL files without executing anything against the warehouse, so it cannot confirm the typo fix actually produces a working table.
- D. Testing alone checks assertions on `stg_orders` as it currently exists in the warehouse; it does not rebuild the model with the corrected SQL or touch any downstream dependents.
3.Lock the fix in with a unit test
A successful rebuild proves the fix works on today's data. It does not stop the same bug from coming back the next time someone edits the model. Unit tests close that gap. A unit test feeds the model a small set of fixed input rows and compares its output with rows you wrote down in advance. It tests the model's SQL logic, not the data in the warehouse, and it runs before the model is materialized. dbt names logic that already had bugs reported as one of the clearest cases for a unit test. Edge cases you have not yet seen in real data, and work done just before a major refactor, are two more.
For a fix, write the test from the bug report. Turn the input that broke the model into fixture rows, and use the correct result as the expected output. Before the patch, the test fails. After the patch, it passes. From then on it runs on every change to the model. The YAML structure for given and expect is covered in [unit tests and other test types](lesson:5.1). One limitation applies: unit tests support only SQL models in the current project.
Unit tests belong in development and CI, not production. Their inputs are fixed, so running them in a production build spends compute and checks nothing new. Exclude them from production builds with --exclude-resource-type, and while you work, run only the unit tests with the test_type selector.
Checkpoint 4 of 7· Fill the gap
Complete the command that runs only unit tests while you iterate on a fix.
dbt test --select " ? :unit"The test_type selector method picks out unit tests, so you can run just those on demand without the data tests.
Source: docs.getdbt.comCheckpoint 5 of 7· Exam question
You're troubleshooting a `fct_revenue` model that throws a SQL error only when dbt runs it, even though the raw SQL looks syntactically valid when pasted directly into your warehouse's query editor. Which techniques correctly help you determine whether the failure originates from dbt's Jinja and `ref()` rendering rather than from the underlying SQL itself? (Select all that apply)(Select 3)
Correct answers: A, B, E — Compare the rendered SQL in `target/compiled/` against the executed SQL in `target/run/` to see exactly what dbt substituted for `ref()` and `source()` calls.; Tail `logs/dbt.log` for the run to see the exact SQL dbt sent to the warehouse, plus any Jinja rendering errors logged just before it.; Isolate the model with `dbt run --select fct_revenue` on its own so upstream failures elsewhere in the DAG don't contaminate the compiled output you inspect.
- A. The compiled file shows what dbt rendered before execution, while the run file shows what actually reached the warehouse; diffing the two isolates whether the ref/source substitution itself is the source of the broken SQL.
- B. The debug log captures both the final SQL dbt issued and any Jinja rendering events that preceded it, so scanning it can surface a rendering problem the compiled file alone doesn't explain.
- C. Recreating the file from scratch is a guess that discards the diagnostic evidence you already have and does not identify whether the failure is a rendering issue or a plain SQL bug.
- D. The documentation catalog reflects column metadata for the docs site; it plays no role in how a model is compiled or executed, so regenerating it cannot reveal a rendering bug.
- E. Running the model on its own removes noise from unrelated models in the same invocation, giving compiled and run output you can trust as specific to this model's rendering behavior.
- F. A query timeout controls how long the warehouse waits before aborting a statement; it does not affect whether dbt correctly substitutes `ref()` and `source()` calls during compilation.
Sources4
4.Let a CI job test the pull request
Local checks run in your own development schema, against whatever is already there. A CI job repeats the check in a clean, shared environment before the merge. In dbt, a CI job runs whenever someone opens a pull request or pushes a commit to one. dbt Labs recommends putting the job in its own deployment environment connected to a staging database, so temporary CI schemas stay separate from production builds.
The default command is dbt build --select state:modified+, sometimes called Slim CI. Rather than rebuilding the whole project, dbt builds and tests only the nodes your branch changed, plus their descendants. To decide what counts as changed, dbt compares your branch with the last successful run in a deferred environment, which is normally Production (see [state selection](lesson:7.1)). If no deferred environment is selected, there is nothing to compare against. In a setup that does not use state, the @ operator plays a similar role: it also builds the parents of your model's children, which will not exist yet in a fresh CI schema.
| Setting | What it controls |
|---|---|
| Triggered by pull requests | On by default; the job runs when a PR is opened or a commit is pushed to it |
| Commands | Defaults to dbt build --select state:modified+ to build changed models and their downstream dependents |
| Compare changes against an environment (Deferral) | The environment whose state the PR code is compared with, by default Production |
| dbt compare | Enterprise option that compares production state with the PR changes; record-level comparison needs a primary key constraint or uniqueness test |
| Run timeout | Cancels the CI run if it exceeds the timeout, limiting warehouse use |
CI runs do not get in each other's way. Each pull request builds into its own schema, so ten developers testing ten fixes at once never overwrite each other's results, and dbt drops each schema when its PR is merged. CI jobs also never block a production run. Unlimited CI jobs can run at once. If you push a new commit while an older run is still in progress, dbt cancels the older run and starts again on the new code. A green check on the PR therefore means the latest version of your fix built and passed its tests together with everything downstream of it.
Checkpoint 6 of 7· Check yourself
Which statement about dbt CI jobs is FALSE?
CI runs are isolated from production scheduling. The other three statements describe how CI jobs actually behave.
“CI jobs will never block a production run.”Source: docs.getdbt.com
Checkpoint 7 of 7· Exam question
A teammate's pull request fails during dbt's parsing step with a YAML compilation error. The file `models/staging/stg_customers.yml` reads: ```yaml models: - name: stg_customers columns: - name: customer_id tests: - unique - name: email tests: - not_null ``` Which line is causing the parsing failure?
Correct answer: A — `- name: email` is indented one level shallower than the `tests:` key that follows it, breaking the column's nested key structure.
- A. The `email` column's `tests:` key sits at a shallower indentation than the `- name: email` line above it, so YAML no longer treats it as a property of that column and the parser rejects the structure.
- B. `unique` is itself a valid built-in generic test in dbt; nothing about this file requires swapping it for `accepted_values`, and doing so would not fix an indentation problem.
- C. `columns:` is already correctly indented one level under `- name: stg_customers` in this file, matching how properties files nest a model's column list; adding another level would itself break the structure.
- D. This file defines a model named `stg_customers` with column tests, which is exactly what belongs under `models:`; renaming the key to `sources:` would misdeclare it as a raw source table instead.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.dbt build --select "+my_model" verifies a patch against the models that consume my_model.Why is that wrong?
A leading + selects ancestors. To include the downstream models that read the patched output, put the + after the model name.
Covered in Rebuild the patched model and everything downstream
2.A unit test that guards a bug fix should run in every production build too.Why is that wrong?
Unit tests use fixed inputs, so production runs add nothing. Run them in development and CI, and exclude them from production builds.
Covered in Lock the fix in with a unit test
3.state:modified+ in a CI job can find the changed models without any other environment configured.Why is that wrong?
dbt needs a comparison state to detect modifications, and it takes that state from the deferred environment, usually Production.
Covered in Let a CI job test the pull request
Practise it for real
Take a broken model from failure to a verified fix ready for a pull request
1.Empty logs/dbt.log, then run dbt run --select for only the failing model
Why: The log then holds only the queries from this one attempt
You should see: The same error as before, and a short log ending with the failing query
2.Run the compiled query for the model on its own in a query runner or with Preview, then fix the model's SQL
Why: Running the compiled SQL separately shows whether the error is in the SQL itself
You should see: The corrected query runs without error
3.Run dbt build --select "my_model+", replacing my_model with the name of the patched model
Why: Builds and tests the patched model and every descendant in DAG order
You should see: All selected nodes pass, and none are skipped
4.Add a unit test reproducing the reported bug and run dbt test --select "test_type:unit"
Why: Keeps the fixed logic from breaking again
You should see: The unit test passes against the patched model
5.Commit, push and open a non-draft pull request
Why: Triggers the CI job running dbt build --select state:modified+
You should see: A CI check appears on the PR in your git provider within seconds
Stuck? Get a nudge
If the build skips child models, a node earlier in the selection failed. Fix that node first.
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.https://docs.getdbt.com/guides/debug-errorsOfficial docs
“Then, re-execute dbt run for just the problematic model.”
↩︎ Reproduce the failure on one model, then patch it“Try to re-execute the SQL to isolate the error”
↩︎ Reproduce the failure on one model, then patch it“Isolate the problem — for example, by running one model a time, or by undoing the code that broke things.”
↩︎ Key concept - 2.https://docs.getdbt.com/guides/set-up-ciOfficial docs
“If something fails, nodes that depend on it will be skipped.”
↩︎ Rebuild the patched model and everything downstream“Once each PR is merged, dbt will drop that schema.”
↩︎ Let a CI job test the pull request“it will be automatically canceled and replaced with the fresh code.”
↩︎ Prediction“CI jobs will never block a production run.”
↩︎ Checkpoint - 3.https://docs.getdbt.com/docs/build/dbt-tipsOfficial docs
“Use the --full-refresh flag to rebuild an incremental model from scratch.”
↩︎ Rebuild the patched model and everything downstream“Use it when refactoring existing logic to ensure that the new results are identical.”
↩︎ Rebuild the patched model and everything downstream“Use the @ operator on the left of a model in a non-state-aware CI setup to test it.”
↩︎ Let a CI job test the pull request - 4.https://docs.getdbt.com/docs/build/unit-testsOfficial docs
“Logic for which you had bugs reported before.”
↩︎ Lock the fix in with a unit test“We currently only support unit testing SQL models.”
↩︎ Lock the fix in with a unit test“dbt Labs strongly recommends only running unit tests in development or CI environments.”
↩︎ Exam trap 2 - 5.https://docs.getdbt.com/docs/deploy/ci-jobsOfficial docs
“This informs dbt to build only new or changed models and their downstream dependents.”
↩︎ Let a CI job test the pull request“Importantly, state comparison can only happen when there is a deferred environment selected to compare state to.”
↩︎ Exam trap 3
Also cited
“Placed after a model/resource — Includes the resource itself and all its descendants (downstream dependencies).”
↩︎ Exam trap 1“Placed after a model/resource — Includes the resource itself and all its descendants (downstream dependencies).”
↩︎ Checkpoint