String manipulation in DBT - Jinja - jinja2

I want to develop a macro that will loop rows from a seeding query's result and create a dynamic query for another task.
Let's assume my row would be similar to
<agate.Row: ('product_available', Decimal('0.6'), 'Positive')>
<agate.Row: ('product_quality', Decimal('0.5'), 'Negative')>
I intend to generate an array selection for downstream queries which have my_udf that will take arguments from my seeding query's result. For example,
my_udf("product_available", 0.6, 'Positive')
my_udf("product_quality", 0.5, 'Negative')
The problem that I have is some arguments are actual column names, while others are values. Hence, column names should have double quotes, while value must have single quote. For instance, "product_available" vs 'Positive'
My code is
{% macro generate_list_select_from(seeding_query) %}
{# ... other code to execute my seeding query ... #}
{# loop query goes here #}
{% for i in results_list %}
{% set item = "my_udf( {{ i[0] }} , {{ i[1] }}, '{{ i[2] }}' )" %}
{{items.append(item)}}
{% endfor %}
{{ return(items) }}
{% endmacro %}
Below is output when I use my macro
select foo_column,
my_udf( {{ i[0] }} , {{ i[1] }}, '{{ i[2] }}' ),
my_udf( {{ i[0] }} , {{ i[1] }}, '{{ i[2] }}' )
from foo_table
My question is how to create a such string?
Update:
Tried other way, {% set item = "my_udf(" + {{i[0]}} + ")" %}, I end up with a compilation error expected token ':', got '}'

Don't nest your curlies.
~ is the string concatenation operator in jinja. You could use that to build up your function args, including the single quotes:
{% set item = "my_udf(" ~ i[0] ~ ", " ~ i[1] ~ ", '" ~ i[2] ~ "' )" %}
This is pretty hard to read, though. I'd probably add a macro called quoted:
{% macro quoted(s) %}
'{{ s }}'
{% endmacro %}
And then use the join filter to concatenate the items of a list:
{% set args = [i[0], i[1], quoted(i[2])] %}
{% set item = "my_udf(" ~ args | join(", ") ~ ")" %}
Lastly, if the purpose of this macro is to template SQL, then you don't need to use return(items). You should just have the body of the macro template the string you want (see the quoted macro above). That dramatically simplifies things:
{% macro quoted(s) %}
'{{ s }}'
{% endmacro %}
{% macro generate_list_select_from(seeding_query) %}
{# ... other code to execute my seeding query ... #}
{# loop query goes here #}
{% for i in results_list %}
{% set args = [i[0], i[1], quoted(i[2])] %}
my_udf({{ args | join(", "}}){% if not loop.last %},{% endif %}
{% endfor %}
{% endmacro %}

Related

How to Add a timestamp within dbt macro or log

I want to add a timestamp to my macro log that shows the current date and time of the local place. I tried to place the run_started_at function in and out of the log, but it doesn't work. I prefer to have inside the log. ie,'Operation Completed 01:10:2023 11:30' Below are the works I have done;
{% macro create_local_database(username, reset_environment = false) %}
{% set setup_script1 %}
{%- if reset_environment == false -%}
CREATE DATABASE ANALYTICS_LOCAL_MART_{{username}}
{% do log('Operation Completed', True) %} {{ run_started_at.strftime("%Y-%m-%d") }}
{%- endif -%}
{% endset %}
{% endmacro %}
I would like to have a macro log that includes the message and timestamp.
Just append the timestamp to the string you send to log()
{%- macro logt(msg) -%}
{{ log(this ~ " (" ~ run_started_at.strftime('%Y-%m-%d') ~ "): " ~ msg, true) }}
{%- endmacro -%}

How to dynamically call dbt macros using jinja?

I have a use case where I would like to define the name of a macro and then apply it to one column.
A simplified example could be as follows. I have two macros defined that I want to call dynamically in my model (both take one column as an input):
cast_to_string
convert_empty_string_to_null_value
Now, I want to call them dynamically. See the example below
{%- set macro_names = ["cast_to_string", "convert_empty_string_to_null_value"] -%}
select
{% for macro_name in macro_names %}
-- this should dynamically be evaluated to `{{ cast_to_string(my_column) }}`
-- and `{{ convert_empty_string_to_null_value(my_column) }}`
{{ macro_name(my_column) }}
{% endfor %}
from my_model
However, this will throw an error saying that a string is not callable.
I also tried using {% raw %} {{ {% endraw %} to escape brackets, but that didn’t work either.
So, my question is, if there is a way to dynamically call macros in jinja/dbt?
I think it should work if you remove the quotes :
{%- set macro_names = [cast_to_string, convert_empty_string_to_null_value] -%}
So that jinja doesn't interpret it as string and you can use it as a Callable
I achieve it using this example :
{%- set macro_names = [print_lower, print_upper] -%}
{% for macro_name in macro_names %}
{{ macro_name("test") }}
{% endfor %}
and
{% macro print_lower(string) %}
{{ print(string|lower) }}
{% endmacro %}
{% macro print_upper(string) %}
{{ print(string|upper) }}
{% endmacro %}

Argument into {% for loop %} in jinja2

There is given a list of array results in format:
result_a, result_b, result_c etc.
A foo and bar are some variables, not strings.
There is need to put macro argument into for loop.
{% macro test(foo, bar) %}
{% for item in results_{{foo}} %}
{% if item[0].name == result_{{foo}}[bar] %}
{{ item }} pass test
{% endif %}
{% endfor %}
{% endmacro %}
During calling macro test(a, names) there is error. How to implement argument into {% for block %}.

Jekyll Liquid Errors are puzzling me

I have been trying to create a site with Github and I'm new to the whole thing. I have been struggling with a piece of code that has been giving me some headache:
{% for item in site.static_files %}
{% if item.path contains '/archive' %}
{% if item.path contains 'index.html' == false %}
{% assign split_path = item.path | split: '/' %}
<p>{{item.path}}</p>
{% assign filename = split_path.last %}
<p>{{ filename }}</p>
{% endif %}
{% endif %}
{% endfor %}
This generates the following error:
Error:
Liquid Warning: Liquid Syntax Error (line 5): Expected end_of_string but found comparison in "item.path contains 'index.html' == false" in archive/index.html
Can anyone help?
Replace:
if item.path contains 'index.html' == false
With:
unless item.path contains 'index.html'
Liquid's getting confused.

How to concatenate / append a string to another one in Jekyll / Liquid?

To be clear, assuming:
{% assign my_var = "123" %}
{% assign another_var = "456" %}
I would like to append string to my_var to get something like 123 - 456
What I have tried so far:
{% assign my_var = my_var + " - " + another_var %}
You could use the capture logic tag:
{% capture new_var %}{{ my_var }} - {{ another_var }}{% endcapture %}
It is also possible to use the append filter, as Ciro pointed:
{% assign new_var = my_var | append: ' - ' | append: another_var %}
append: filter
This is more convenient than capture for short concatenations:
{% assign x = 'abc' %}
{% assign y = 'def' %}
{% assign z = x | append: ' - ' | append: y %}
{{ z }}
Output:
abc - def
Tested on jekyll 3.0.4 (github-pages 75).
All the answers so far are correct, but they fail to mention that you can also inline the append instead of having to assign a new variable:
Link