Skip to content
>GLB_
Go back

The limit_zero macro bug: how dbt resolves macros across packages

Running dbt run on a Tuva Project install produced this:

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 fix is simple, but the root cause reveals something specific about how dbt handles macro resolution across packages.

What limit_zero does

limit_zero is a utility macro that generates an empty result set with the right schema. In practice, it compiles to:

limit 0

Tuva uses it to create empty staging tables that establish column types before any data is loaded. Every adapter needs this to materialize the empty skeleton of a model before populating it.

How dbt resolves macros

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

  1. Your project’s macros/ directory
  2. Installed packages listed in packages.yml, in declaration order
  3. The global dbt namespace (built-in macros)

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

There is a subtlety: packages are searched by their declared namespace, not by traversing all macros in all installed packages indiscriminately. If Tuva calls limit_zero without a namespace prefix (not dbt_utils.limit_zero), dbt looks in Tuva’s own macros first, then falls back to your project and the global namespace. It does not automatically descend into Tuva’s transitive dependencies.

Adapter dispatch makes this messier

dbt supports adapter-specific macro implementations through the dispatch pattern. A macro like limit_zero is typically structured as:

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

This means a call to limit_zero() actually dispatches to duckdb__limit_zero() when running against DuckDB, falling back to default__limit_zero() if no adapter-specific version exists.

The dispatch namespace parameter ('dbt_utils') tells dbt where to look for the implementation. If neither the adapter-specific nor the default version is found in that namespace, you get the “undefined” error even though the dispatcher macro itself was found.

Where the Tuva 0.17.2 failure comes from

In some model files in Tuva 0.17.2, the call is {{ limit_zero() }} as a bare name. The dispatcher for this macro uses a namespace that, depending on the exact combination of dbt-duckdb adapter version and dbt-utils version installed, fails to locate duckdb__limit_zero in the correct namespace.

The specific failure sequence:

  1. limit_zero dispatcher found in dbt_utils
  2. Dispatcher calls adapter.dispatch('limit_zero', 'dbt_utils')
  3. Looks for duckdb__limit_zero in dbt_utils namespace
  4. Not found in this version combination
  5. No fallback defined for this specific adapter/namespace pair
  6. Error: undefined

The fix

Create macros/limit_zero.sql in your local dbt project:

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

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

This works because dbt always checks your project’s macros first, before packages. By defining both default__limit_zero and duckdb__limit_zero locally, you cover:

The implementation is trivially limit 0 for DuckDB. Other databases like BigQuery use where false or limit 0 depending on whether they support the LIMIT clause on a zero-row result.

Why this happens at the package boundary

The core issue is that dbt packages don’t automatically re-export their dependencies’ macros into the calling context. Tuva depends on dbt-utils, and that dependency is declared in Tuva’s own packages.yml. When you install Tuva, dbt-utils gets installed too — but macros from dbt-utils are namespaced under dbt_utils, not automatically available as bare names from your project’s perspective.

If Tuva called {{ dbt_utils.limit_zero() }} everywhere, this would never surface. The bare {{ limit_zero() }} call is what creates the resolution ambiguity.

Verifying the fix

After adding the local macro:

dbt compile --select quality_measures__int_adh_statins_numerator

If compilation succeeds and produces limit 0 in the output SQL, the macro is resolving from your local project. The model itself may still fail for other reasons (DuckDB 1.10 assertion errors on window function CTEs), but the compilation phase will pass cleanly.


Share this post:

Related Posts


Previous Post
Running the Tuva Project on DuckDB — what breaks and how to fix it
Next Post
What 167k synthetic Medicare claims taught me about US healthcare data