Empty Builds as a Check

Concepts covered: Compilation

Every check so far ran on rows, written by hand or read from the warehouse. A project can also be built on no rows at all. dbt run --empty and dbt build --empty render every ref() and source() as a subquery that returns 0 rows, then create every model, view or table, with the warehouse running each statement over an empty input.

create table analytics.int_lesson_outcomes as
select
    completion_id,
    user_id,
    score,
    case
        when score >= 90 then 'legendary'
        when score >= 70 then 'pass'
        else 'retry'
    end as outcome,
    score >= 70 as counts_for_streak
from (
    select *
    from analytics.stg_lesson_completions
    where false
    limit 0
) _dbt_limit_subq_stg_lesson_completions

The statement runs to completion, so everything the warehouse checks before it reads a row gets checked: every referenced column exists, every function exists for the types it receives, every join compares comparable types, every union has matching column counts, and the role can read each relation and write each schema. A contract from the models lessons is checked too, because dbt compares the empty table's columns and types against the contract after creating it. What stays unproven is anything about rows. A data test over an empty table finds 0 failing rows and passes, an aggregate returns nothing, and a filter that drops every row looks the same as one that keeps them.

An empty build proves
  • Each model's SQL runs in the warehouse
  • Columns and functions resolve for the types in play
  • Contracts match the shape each model produces
  • The DAG builds end to end in order
It cannot prove
  • Any data test, since 0 rows means 0 failures
  • A join's keys line up
  • A filter keeps the right rows
  • Values fall in range

A rename, caught in a minute

A teammate renames score to points in stg_lesson_completions and opens a change with the staging model updated and nothing else. A full build reaches the failure only after building, on its real rows, every model scheduled ahead of int_lesson_outcomes, then fails there on the missing column. The empty build fails at the same model with the same message from the warehouse, that column score cannot be resolved, after reading 0 rows, and reports every model downstream as skipped. The cost is the warehouse's planning time per statement, a few seconds each, which is why the check fits before every merge where a full build does not.

dbt builddbt build --empty
Rows readEvery row of every input0
TimeScales with the dataScales with the model count
CatchesBad rows, bad shape, bad logicBad shape
Data testsMeaningfulPass on every table
Unit testsRun on fixturesRun on fixtures

Empty first, then unit tests

Section 0 noted that a unit test casts its given rows to the input's real column types, which means the input relation must exist. On a fresh schema nothing does, and the unit test cannot run. dbt run --empty creates every relation with the right columns and 0 rows, and dbt test --select "test_type:unit" then runs every unit test on its fixtures, so a change can be checked for shape and for logic without reading a production row. dbt build --empty does both in 1 command and also runs the data tests, which pass on empty tables and so prove nothing there; those run against real data in the scheduled build.

About this lesson

This section is part of the Testing: Advanced lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.