pass a macro as a parameter jinja dbt

Question:

{{ today_date_milliseconds() }} – is my macro in the project. How to redirect this macro as a parameter, so it will be by default and I could in yml write another macro?

{% test valid_date(model, column_name, exclude_condition = '1=1') %}

    SELECT {{ column_name }}
    FROM {{ model }}
    WHERE (CAST( {{ column_name }} AS BIGINT) < {{ today_date_milliseconds() }}
    AND {{ exclude_condition }}

{% endtest %}

In yml it will look like

        - name: date_3
          description: column for third date
          tests:
          - valid_date:
                lower_bound: 'name of another macro'

Asked By: Aleksandra

||

Answers:

I love this question — I just learned something looking into it (and I’ve wanted to do this in another project, so I’m glad I did!).

First, a caveat that this is undocumented and probably not encouraged, and could probably break at any time.

The good news is that unbound macros act like python functions, so you can assign them to variables in jinja and execute them later. As an example, I have two macros that just log a word to stdout:

-- log_one.sql
{% macro log_one() %}
{{ log("one", info=True) }}
{% endmacro %}

-- log_two.sql
{% macro log_two() %}
{{ log("two", info=True) }}
{% endmacro %}

In a model, I can assign one of these two macros to a variable, and then execute the variable, like this:

-- this model will print `one` to the console when it is built
{% if execute %}
{% set bound_macro = log_one %}
{{ bound_macro() }}
{% endif %}
select 1

Note that there are no quotes around log_one, so I’m passing an actual reference to the macro into the variable called bound_macro, which is why this works.

But this isn’t enough for your use case, since your config is going to enter the jinja context as a string, not a reference to the macro. In Python, you can use eval() to evaluate a string as code, but jinja doesn’t allow that.

(This is the undocumented part that could break in the future but works on dbt v1.2) Fortunately, in every dbt macro, you have access to a global object called context. context quacks like a dictionary, and you can access all of the macros, built-ins, etc., with the context’s get method, which works just like a Python dict’s get. So you can use a macro’s name as a string to get a reference to the macro itself by using context.get("macro_name"). And just like with a dict, you can provide a default value as a second argument to get, if the first argument isn’t present in the context.

-- this will print `two` when the model is built
{% if execute %}
{% set macro_name = "log_two" %}
{% set bound_macro = context.get(macro_name, log_one) %}
{{ bound_macro() }}
{% endif %}
select 1
-- this will print `one` when the model is built, since macro_name is not defined
{% if execute %}
{% set bound_macro = context.get(macro_name, log_one) %}
{{ bound_macro() }}
{% endif %}
select 1

Edit for clarity:
context will only be populated after parsing, so any time you access context, it should be wrapped in an {% if execute %} block, as in the examples above.

For your specific example, I would add an argument called lower_bound to your test, and give it a default value (of a string!), and then use context to retrieve the right macro. To be a little safer, you could also provide a default arg to get, although that might make it harder to debug typos in the config:

{% test valid_date(model, column_name, exclude_condition = '1=1', lower_bound="today_date_milliseconds") %}

{% set lower_bound_macro = context.get(lower_bound, today_date_milliseconds) %}

    SELECT {{ column_name }}
    FROM {{ model }}
    WHERE (CAST( {{ column_name }} AS BIGINT) < {{ lower_bound_macro() }}
    AND {{ exclude_condition }}

{% endtest %}
Answered By: tconbeer
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.