Skip to content
>GLB_
Go back

dbt macro resolution order: a real-world debugging story

The error message was clear enough:

Compilation Error in model quality_measures__int_adh_statins_numerator
  'limit_zero' is undefined. Did you mean one of these: dbt_utils.limit_zero

The suggested fix (dbt_utils.limit_zero) didn’t work — the macro was undefined regardless of how it was called. Understanding why required reading exactly how dbt resolves macro names.

The resolution order

When dbt compiles a model that calls {{ limit_zero() }}, it searches for a macro named limit_zero in this order:

  1. Your projectmacros/ directory in the root dbt project
  2. Installed packages — in the order they appear in packages.yml
  3. dbt core — built-in macros like run_query, log, return

The search stops at the first match. If no match is found, you get the “undefined” error.

Adapter dispatch is a separate lookup

Many macros in dbt are adapter-dispatched. The dispatcher pattern looks like this:

{% macro limit_zero() %}
  {{ return(adapter.dispatch('limit_zero', 'dbt_utils')()) }}
{% endmacro %}

When you call {{ limit_zero() }}, dbt first finds the dispatcher macro (via the name lookup above), then dispatches to an adapter-specific implementation:

  1. dbt_utils__duckdb__limit_zero() — adapter + package prefix
  2. duckdb__limit_zero() — adapter prefix only
  3. dbt_utils__default__limit_zero() — package + default prefix
  4. default__limit_zero() — default only

The dispatch namespace parameter ('dbt_utils') is critical. It tells dbt which package’s namespace to search in for the implementation. If neither an adapter-specific nor a default version is found in that namespace, dispatch fails.

The debugging steps

# Step 1: confirm the macro exists somewhere in the project
grep -r "limit_zero" $(dbt debug --config-dir 2>/dev/null)/packages/
# or
find . -name "*.sql" -path "*/macros/*" | xargs grep -l "limit_zero"

# Step 2: check what dbt can see
dbt ls --resource-type macro 2>/dev/null | grep -i limit

# Step 3: compile just the failing model to isolate
dbt compile --select quality_measures__int_adh_statins_numerator

# Step 4: look at the compiled output
cat target/compiled/the_tuva_project/models/quality_measures/.../model.sql

Step 2 reveals dbt_utils.limit_zero exists but duckdb__limit_zero is not found in the dbt_utils namespace for this DuckDB + dbt-utils version combination. The compiled SQL in step 4 shows exactly where the macro call appears.

Where the Tuva failure comes from

In Tuva 0.17.2, some model files call {{ limit_zero() }} as a bare name. The dispatcher for this macro uses 'dbt_utils' as the namespace. The dbt-utils version installed as a transitive dependency of Tuva may not include a duckdb__limit_zero implementation — it depends on when the version of dbt-utils was cut relative to when dbt-duckdb added DuckDB to its adapter dispatch table.

The result: the dispatcher macro is found, dispatch is triggered, the DuckDB-specific implementation isn’t found in dbt_utils namespace, no fallback is defined, error.

The fix and why it works

Create macros/limit_zero.sql in your project root:

{% macro default__limit_zero() %}
    limit 0
{% endmacro %}

{% macro duckdb__limit_zero() %}
    limit 0
{% endmacro %}

Your project is checked first in the name resolution order. By defining duckdb__limit_zero locally, you inject it into the adapter dispatch chain before the dbt_utils namespace lookup runs. The dispatcher finds your implementation and stops.

The default__limit_zero is a safety net. If you switch adapters (Snowflake, BigQuery), the default variant covers the limit 0 behavior there too — though on some warehouses you’d use where false instead if LIMIT isn’t legal in that context.

The broader pattern

Any time dbt reports “X is undefined”, the debugging path is:

  1. Confirm whether the macro exists anywhere in the installed packages (grep or dbt ls)
  2. Check if there’s an adapter dispatch chain and whether your adapter is covered
  3. Check the dispatch namespace — if the dispatcher uses a package namespace, the implementation needs to be in that package or in your project
  4. If the macro exists but isn’t resolving for your adapter, add a local version

Package namespacing in dbt is intentionally shallow — it doesn’t automatically re-export macros from transitive dependencies. If package A depends on package B, a macro in package B isn’t automatically callable from your project without B.macro_name unless it’s also defined in A under a bare name.


Share this post:

Related Posts


Previous Post
Connecting Streamlit to DuckDB: read-only mode and the lock problem
Next Post
dbt + DuckDB: the good, the bad, and the workarounds