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:
- Your project —
macros/directory in the root dbt project - Installed packages — in the order they appear in
packages.yml - 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:
dbt_utils__duckdb__limit_zero()— adapter + package prefixduckdb__limit_zero()— adapter prefix onlydbt_utils__default__limit_zero()— package + default prefixdefault__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:
- Confirm whether the macro exists anywhere in the installed packages (
grepordbt ls) - Check if there’s an adapter dispatch chain and whether your adapter is covered
- Check the dispatch namespace — if the dispatcher uses a package namespace, the implementation needs to be in that package or in your project
- 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.