Skip to content
>GLB_
Go back

The limit_zero macro bug: how dbt resolves macro names across packages

· Updated

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:

  1. The package the calling node belongs to. A Tuva model sees Tuva’s macros; a model in your project sees your project’s macros.
  2. Your root project’s macros.
  3. 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:

  1. duckdb__limit_zero (current adapter)
  2. 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 %}

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:

  1. Confirm the macro exists in an installed package (grep, or the manifest).
  2. 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.
  3. If it’s a dispatcher, check that an implementation exists for your adapter or default__, in a namespace the dispatch searches.
  4. Call it with its package prefix, or define it (and its implementations) locally.

Gotchas


Share this post:

Related Posts


Previous Post
Soda Core with SQL Server: connection, Windows auth, SSL errors and checks
Next Post
Tracking S3 file changes with ETags and LastModified