Running dbt run on a Tuva Project demo setup (dbt 1.11, dbt-duckdb, Tuva 0.17.2) failed on seven models with this:
Compilation Error in model condition (models/condition.sql)
'limit_zero' is undefined. This can happen when calling a macro that does not exist. Check for typos and/or install package dependencies with "dbt deps".
dbt deps had run, and Tuva ships a limit_zero macro. The macro existed. dbt just wasn’t looking where it was. The fix is a small local macro file, but getting there means understanding two separate lookups dbt does: resolving a macro name, and dispatching to an adapter-specific implementation.
What limit_zero does
The failing models were the input-layer placeholders in the root project (models/condition.sql, encounter.sql, location.sql, medication.sql, patient.sql, practitioner.sql, procedure.sql), copied from the Tuva demo. Each one selects typed NULL columns and ends with {{ limit_zero() }}, so the model materializes as an empty relation with the right schema:
select {% if target.type == 'fabric' %} top 0 {% else %}{% endif %}
cast(null as {{ dbt.type_string() }} ) as practitioner_id
, cast(null as {{ dbt.type_string() }} ) as npi
-- ...more typed null columns...
, cast(null as {{ dbt.type_timestamp() }} ) as tuva_last_run
{{ limit_zero()}}
On DuckDB, limit_zero() should compile to:
limit 0
Tuva defines the macro in its own package, at macros/cross_database_utils/limit_zero.sql:
{% macro limit_zero() -%}
{% if adapter.type == 'fabric' %}
{# No limit for Fabric #}
{{ return('') }}
{% else %}
{{ adapter.dispatch('limit_zero')() }}
{% endif %}
{%- endmacro %}
{% macro default__limit_zero() -%}
limit 0
{%- endmacro %}
{% macro fabric__limit_zero() -%}
{# No limit statement for Fabric #}
{%- endmacro %}
Lookup 1: resolving a bare macro name
When a model calls a macro without a prefix ({{ limit_zero() }}, not {{ the_tuva_project.limit_zero() }}), dbt resolves the name from the node’s point of view:
- The package the calling node belongs to. A Tuva model sees Tuva’s macros; a model in your project sees your project’s macros.
- Your root project’s macros.
- dbt’s built-in and adapter macros (
run_query,log,return, …).
Other installed packages are not searched for bare names. Package namespacing is intentionally shallow: a macro in package B isn’t callable as a bare name from your project just because B is installed. You call it as B.macro_name.
That explains the first error. The seven failing models belong to the root project (the_tuva_project_demo). Tuva’s limit_zero lives in the the_tuva_project namespace, so from a root model the bare name resolves to nothing. Tuva’s own models, which call the same bare name, compiled fine because step 2 finds the macro in their own package.
Lookup 2: adapter dispatch
adapter.dispatch('limit_zero') is a second, separate lookup. It searches a namespace for adapter-specific implementations, in order:
duckdb__limit_zero(current adapter)default__limit_zero(fallback)
If neither is found, dispatch fails even though the dispatcher macro itself was found.
That was the second error, after a first attempt at a local macros/limit_zero.sql that forwarded to Tuva’s macro:
In dispatch: No macro named 'limit_zero' found within namespace: 'None'
Searched for: 'duckdb__limit_zero', 'default__limit_zero'
> in macro limit_zero (macros/cross_database_utils/limit_zero.sql)
> called by macro limit_zero (macros/limit_zero.sql)
> called by model condition (models/condition.sql)
Tuva’s dispatcher was reached, but running on behalf of a root-project model it didn’t find an implementation: Tuva’s default__limit_zero was not picked up, and the root project didn’t define one yet.
The fix
Create macros/limit_zero.sql in the root project with the macro and both implementations:
{% macro limit_zero() %}
limit 0
{% endmacro %}
{% macro default__limit_zero() %}
limit 0
{% endmacro %}
{% macro duckdb__limit_zero() %}
limit 0
{% endmacro %}
limit_zero()fixes lookup 1: root models now find the bare name in their own project.default__limit_zero()andduckdb__limit_zero()fix lookup 2: any dispatch forlimit_zerothat searches the root project finds an implementation.
If you ever run the project on Fabric, mirror Tuva’s fabric__limit_zero, since Fabric uses top 0 instead of LIMIT.
Debugging “X is undefined” in dbt
# Step 1: confirm the macro exists somewhere in the project or its packages
find . -name "*.sql" -path "*/macros/*" | xargs grep -l "limit_zero"
# Step 2: check which namespaces dbt actually parsed it into
dbt parse
python -c "import json; m = json.load(open('target/manifest.json')); print(sorted(k for k in m['macros'] if k.endswith('limit_zero')))"
# Step 3: compile just one failing model to isolate it
dbt compile --select condition
# Step 4: look at the compiled output
cat target/compiled/<project_name>/models/condition.sql
In this project, step 2 printed:
['macro.the_tuva_project.default__limit_zero', 'macro.the_tuva_project.fabric__limit_zero', 'macro.the_tuva_project.limit_zero', 'macro.the_tuva_project_demo.default__limit_zero', 'macro.the_tuva_project_demo.duckdb__limit_zero', 'macro.the_tuva_project_demo.limit_zero']
The middle part of each key is the namespace. Before the fix, only the the_tuva_project entries existed, which is exactly why root models couldn’t see the macro. Matches under a package’s integration_tests/ folder don’t count: those files aren’t parsed when the package is installed as a dependency.
If compilation succeeds and the compiled SQL ends in limit 0, the macro resolves from your project.
The general path for any “X is undefined” error:
- Confirm the macro exists in an installed package (grep, or the manifest).
- Check which namespace it lives in, and which project the failing node belongs to. A bare name only resolves in the root project, the node’s own package and dbt’s built-ins.
- If it’s a dispatcher, check that an implementation exists for your adapter or
default__, in a namespace the dispatch searches. - Call it with its package prefix, or define it (and its implementations) locally.
Gotchas
- Overriding a package macro takes dispatch config. Because a package’s own macros come first for its models, a same-named macro in your project doesn’t replace it there. That’s what the
dispatch/search_ordersetting indbt_project.ymlis for. - “Installed” isn’t “visible”.
dbt depsinstalls packages, but their macros stay namespaced. Bare calls from your own models won’t reach them. - Template models inherit the problem. Models copied from a package’s demo or integration tests assume that package’s macros are in scope. Once they live in your project, they aren’t.
- Two errors, two lookups.
'X' is undefinedis a name-resolution failure;In dispatch: No macro named 'X' found within namespaceis a dispatch failure. They need different fixes. - A clean compile isn’t a clean run. In the same Tuva run,
quality_measures__int_adh_statins_numeratorandquality_measures__int_adh_diabetes_numeratorfailed at runtime with DuckDB internal errors on window-function CTEs, unrelated to macros.
Related
- Running the Tuva Project on DuckDB: what breaks and how to fix it collects the other failures from the same setup.
- dbt + DuckDB: the good, the bad and the workarounds puts these issues in the context of the adapter.
- What is the Tuva Project and why should data engineers care explains what the package does.
- DuckDB concurrency in 2026: why you can’t run dbt and DBeaver at the same time covers the other error you’ll hit while debugging this.