dbt

Deep Dive into dbt Macro Dispatch (adapter.dispatch): Adapter-Aware Macros for Practice and the Exam

2026-04-19
NicheeLab Editorial Team

Even when the logic is identical, the optimal SQL and functions differ per adapter. dbt's macro dispatch automatically routes a call to the macro implementation matching the active adapter at runtime.

This article covers the basics of naming conventions (default__, snowflake__, etc.) and lookup order (dispatch configuration), through to real-world overrides, validation, and anti-patterns, with an eye on the topics commonly tested on the exam.

Macro Dispatch Basics

adapter.dispatch delegates from a wrapper macro to an implementation macro tuned for the active adapter. Callers use a single macro name while, behind the scenes, dbt picks between snowflake__..., bigquery__..., default__..., and so on.

The default lookup order follows the dispatch settings in your project. When unset, dbt generally searches your project first and then the package that owns the wrapper, looking for the adapter-prefixed macro (e.g. snowflake__macro) and falling back to default__macro when none is found.

  • Wrapper side: call adapter.dispatch('macro_name', 'package_name')
  • Implementation side: define snowflake__macro_name / bigquery__macro_name / default__macro_name inside the package
  • Project-level override: put your project name at the head of dispatch.search_order
MechanismCharacteristicsTypical use cases
adapter.dispatchAutomatically picks the best implementation per adapterCross-adapter package support and distributing shared macros
default__ implementationSafe backward-compatible fallback when no adapter-specific implementation existsGeneric logic that can be written in ANSI SQL
Project override (dispatch)Lets you swap out an external package's implementation locallyTemporary bug workarounds and dialect tuning

Minimal wrapper and implementation

{% macro my_pkg.my_macro(col) %}\n  {%- set impl = adapter.dispatch('my_macro', 'my_pkg') -%}\n  {{ impl(col) }}\n{% endmacro %}\n\n{# Implementations: inside the same package (my_pkg) #}\n{% macro my_pkg.default__my_macro(col) %}\n  upper({{ col }})  {# Generic example that runs on any DWH #}\n{% endmacro %}\n\n{% macro my_pkg.snowflake__my_macro(col) %}\n  upper({{ col }})::string  {# Example of Snowflake-specific tuning #}\n{% endmacro %}

Naming Conventions and Lookup Order (dispatch)

Implementation macro names follow the shape "prefix__original_macro_name". The prefix is the adapter type (snowflake, bigquery, redshift, postgres, spark, databricks, etc.) or default. The wrapper itself has no prefix and uses adapter.dispatch with the macro name and the package name.

Lookup order is controlled via dispatch in dbt_project.yml. Set macro_namespace to the package that owns the wrapper, and use search_order to set the package search priority. Most projects put their own project at the head to allow overrides, followed by the original package.

  • Implementation naming: snowflake__macro, bigquery__macro, default__macro
  • The wrapper belongs to a package namespace, and the macro name passed to dispatch is identical
  • The canonical search_order is project then package

adapter.dispatch lookup flow

call: adapter.dispatch(m, p)search_order arraye.g. [my_project, my_pkg]pkg.{adapter}__m exists?Yes → use it / No → continuepkg.default__m exists?Yes → use it / No → next packageError if not found anywhereIn each package, search adapter__m then default__m; if neither is found, advance to the next package

dispatch configuration example (dbt_project.yml)

dispatch:\n  - macro_namespace: my_pkg\n    search_order: ['my_project', 'my_pkg']\n\n# Lookup order from wrapper my_pkg.my_macro:\n# 1) my_project.snowflake__my_macro → fallback to my_project.default__my_macro\n# 2) my_pkg.snowflake__my_macro     → fallback to my_pkg.default__my_macro

Project-Level Override Strategy

When you need to temporarily adjust the behavior of an external package macro, put your project at the head of search_order and define an implementation macro with the same name to override it. The benefit is that you can maintain just the diff without forking the package itself.

Even when overriding, keep the name unchanged. Example: if the source package has my_pkg.snowflake__my_macro, define snowflake__my_macro under your own project's namespace as well.

  • Place your own project at the head of search_order
  • Define using the same implementation names (snowflake__..., default__...) as upstream
  • Remove the override and revert once a future package release resolves the difference

Project-level override example

{# packages.yml #}\npackages:\n  - package: my_org\n    version: 1.0.0\n\n{# dbt_project.yml #}\ndispatch:\n  - macro_namespace: my_org\n    search_order: ['my_project', 'my_org']\n\n{# Project side (override): models/macros/snowflake__my_macro.sql #}\n{% macro my_project.snowflake__my_macro(col) %}\n  to_varchar(upper({{ col }}))  {# Fine-grained override for Snowflake #}\n{% endmacro %}

Testing and Debugging the Resolution

To see which implementation was resolved, write a small macro that logs the result via run-operation. Including adapter.type or target.type alongside makes it easier to match against the execution environment.

In CI, run sample models against multiple profiles (e.g. snowflake, bigquery) to verify that the same wrapper resolves to different implementations per environment as intended.

  • Log the implementation name with run-operation to confirm it
  • Set up smoke tests across multiple adapters
  • Detect unintended default fallbacks

Log which implementation was chosen

{% macro debug_my_macro_resolution() %}\n  {# Wrapper name and namespace #}\n  {% set m = adapter.dispatch('my_macro', 'my_pkg') %}\n  {% do log('adapter: ' ~ target.type, info=True) %}\n  {# The macro object itself usually contains the name when stringified #}\n  {% do log('resolved macro: ' ~ (m|string), info=True) %}\n{% endmacro %}\n\n# Example: dbt run-operation debug_my_macro_resolution

Anti-Patterns and Pitfalls

Switching SQL with if-branches on adapter.type scales poorly, is hard to maintain, and is unsuitable for package distribution. With dispatch, adding support for a new adapter is as simple as adding another implementation macro.

If the wrapper's namespace and the package name passed to dispatch do not match, the intended implementation will not be found—either silently falling through to default or erroring out unresolved.

  • BAD: scattering conditionals like if target.type == 'snowflake' across models
  • BAD: passing a different package name to dispatch than the wrapper's package my_pkg
  • Watch out: misspelling implementation prefixes (snowflake/bigquery/databricks, etc.)

Refactoring from raw branching to dispatch

{# Bad example #}\n{% macro my_macro(col) %}\n  {% if target.type == 'snowflake' %}\n    {{ return('upper(' ~ col ~ ')::string') }}\n  {% elif target.type == 'bigquery' %}\n    {{ return('cast(upper(' ~ col ~ ') as string)') }}\n  {% else %}\n    {{ return('upper(' ~ col ~ ')') }}\n  {% endif %}\n{% endmacro %}\n\n{# Good example: wrapper + implementations #}\n{% macro my_pkg.my_macro(col) %}\n  {% set impl = adapter.dispatch('my_macro', 'my_pkg') %}\n  {{ impl(col) }}\n{% endmacro %}\n\n{% macro my_pkg.default__my_macro(col) %} upper({{ col }}) {% endmacro %}\n{% macro my_pkg.bigquery__my_macro(col) %} cast(upper({{ col }}) as string) {% endmacro %}\n{% macro my_pkg.snowflake__my_macro(col) %} upper({{ col }})::string {% endmacro %}

Practical Patterns for Multi-Adapter Support

Open-source Spark and Databricks often share similar SQL dialects, but function names, data types, and DDL behavior can differ in practice. Provide both spark__ and databricks__ prefixes as needed and optimize each with minimal diffs, rather than collapsing one into the default.

When building an external package, start by hardening default__, then incrementally add minimal implementations for the major adapters (snowflake/bigquery/redshift/postgres/spark/databricks). Leaving project-level search_order overrides as an escape hatch makes day-to-day operations much easier.

  • Keep Spark and Databricks under separate prefixes (spark__/databricks__)
  • Always implement default__ to preserve backward compatibility
  • Confine differences to the implementation macros so the call sites stay clean

Skeleton for minimal multi-adapter implementations

{% macro util_pkg.normalize_text(col) %}\n  {% set impl = adapter.dispatch('normalize_text', 'util_pkg') %}\n  {{ impl(col) }}\n{% endmacro %}\n\n{% macro util_pkg.default__normalize_text(col) %}\n  trim(lower({{ col }}))\n{% endmacro %}\n\n{% macro util_pkg.bigquery__normalize_text(col) %}\n  trim(lower(cast({{ col }} as string)))\n{% endmacro %}\n\n{% macro util_pkg.databricks__normalize_text(col) %}\n  trim(lower({{ col }}))  {# Add normalization functions as needed #}\n{% endmacro %}

Check Your Understanding

Analytics Engineer

問題 1

Given the configuration and implementations below, with Snowflake as the target, which implementation is selected when {{ my_pkg.my_macro('name') }} is called?

  1. my_project.snowflake__my_macro
  2. my_project.default__my_macro
  3. my_pkg.snowflake__my_macro
  4. my_pkg.default__my_macro

正解: A

With dispatch set to macro_namespace: my_pkg and search_order: ['my_project', 'my_pkg'], the lookup order is: 1) my_project.snowflake__my_macro → my_project.default__my_macro if absent, then 2) my_pkg.snowflake__my_macro → my_pkg.default__my_macro if absent. On a Snowflake target, if the first one (my_project.snowflake__my_macro) exists, it is selected.

Frequently Asked Questions

What is the lookup order when dispatch is not configured?

Generally, dbt searches your own project first, then the package that owns the wrapper. Within each, it looks for the adapter-prefixed macro (e.g. snowflake__) and falls back to default__ if that is missing. For deterministic control, declare dispatch explicitly in dbt_project.yml.

Do the wrapper and its implementations need to live in the same package?

Implementations (default__, snowflake__, etc.) are defined against the namespace the wrapper belongs to. When distributing an external package, ship the implementations inside that package; for project-level overrides, place your own project at the head of search_order.

Should I provide Spark or Databricks implementations (or both)?

They are largely compatible but have meaningful differences, so providing both spark__ and databricks__ prefixes separately is safest. If you cover only one, run CI against both targets to catch unintended default fallbacks or dialect mismatches.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
dbt

dbt Models: SQL-Defined Transformation Units (2026)

Model fundamentals — SELECT-based definitions, naming, refs,...

dbt

dbt Analytics Engineering Exam: Complete Guide (2026)

Pass the AE Certification — scope, weighting, sample questio...

dbt

dbt Cloud vs dbt Core: Feature & Cost Comparison (2026)

Honest comparison of dbt Cloud vs. dbt Core — IDE, scheduler...

dbt

dbt Project Structure: models/seeds/macros Layout (2026)

Recommended dbt project layout — models, seeds, macros, snap...

dbt

dbt_project.yml Explained: Every Config (2026)

Every dbt_project.yml setting that matters — paths, vars, ma...

Browse all dbt articles (101)
© 2026 NicheeLab All rights reserved.