🌊

dbt macro tips advent calendar 2022 day 25 - その他のTips

2022/12/25に公開

便利なデータ変換ツールである dbt の中のmacroに関するtipsを書いていく dbt macro tips Advent Calendar 2022 25日目です。 最終日です。

最終日はすごくニッチ過ぎて、めったに使わないdbtで使えるJinjaの機能についてです。

raw block

これは ヒアドキュメントのようなもので、rawで囲った部分はJinja templateとして解釈されません。

例えば以下のようなモデルを作ってみます。

models/raw.sql
{{
    config(
        materialized='table',
    )
}}

{% raw %}
    select '{{ "hogehoge" }}' as raw_text
{% endraw %}
$ dbt build --select raw
09:03:45  Running with dbt=1.3.1
09:03:45  Unable to do partial parsing because config vars, config profile, or config target have changed
09:03:45  Unable to do partial parsing because profile has changed
09:03:46  Found 3 models, 4 tests, 0 snapshots, 0 analyses, 313 macros, 0 operations, 0 seed files, 3 sources, 0 exposures, 0 metrics
09:03:46  
09:03:47  Concurrency: 4 threads (target='dev')
09:03:47  
09:03:47  1 of 1 START sql table model dev_dev.raw ....................................... [RUN]
09:03:47  1 of 1 OK created sql table model dev_dev.raw .................................. [SELECT 1 in 0.17s]
09:03:47  
09:03:47  Finished running 1 table model in 0 hours 0 minutes and 0.60 seconds (0.60s).
09:03:47  
09:03:47  Completed successfully
09:03:47  
09:03:47  Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1
 
$ psql -h 127.0.0.1 -U postgres -p 5432 -d postgres
Password for user postgres: 
psql (14.2, server 14.5 (Debian 14.5-2.pgdg110+2))
Type "help" for help.

postgres=# select * from dev_dev.raw;
     raw_text     
------------------
 {{ "hogehoge" }}
(1 row)

postgres=# 

テンプレートとして解釈されずにそのまま実行されてますね。
場合によっては、一部だけテンプレートの処理を無効化したいときに有効です。
まぁ、めったにそんな場面ありませんけれども…

filter

Jinjaにはfilterと呼ばるものがあります。

https://jinja.palletsprojects.com/en/3.0.x/templates/#list-of-builtin-filters

filterというタグもあり、このタグで囲った中身は指定のフィルタが適用されます。

{{
    config(
        materialized='table',
    )
}}

{%- set value %}
    {%- filter upper | trim | reverse -%}


        This text becomes uppercase {{ "hoge" }}


    {%- endfilter %}
{%- endset %}
select '{{ value }}' as filter_text

やってみれば、わかりやすいですね。 大文字化して、Trimしたあとに鏡文字にしてます。

$ dbt build --select filter                        
09:22:51  Running with dbt=1.3.1
09:22:51  Found 3 models, 4 tests, 0 snapshots, 0 analyses, 313 macros, 0 operations, 0 seed files, 3 sources, 0 exposures, 0 metrics
09:22:51  
09:22:52  Concurrency: 4 threads (target='dev')
09:22:52  
09:22:52  1 of 1 START sql table model dev_dev.filter .................................... [RUN]
09:22:52  1 of 1 OK created sql table model dev_dev.filter ............................... [SELECT 1 in 0.18s]
09:22:52  
09:22:52  Finished running 1 table model in 0 hours 0 minutes and 0.44 seconds (0.44s).
09:22:52  
09:22:52  Completed successfully
09:22:52  
09:22:52  Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1
[~/go/.../macro-tips-advent-calender/macro_tips_advcal]                                                                                                                                                                                                                                                                                                                               *[main]  
$ psql -h 127.0.0.1 -U postgres -p 5432 -d postgres
Password for user postgres: 
psql (14.2, server 14.5 (Debian 14.5-2.pgdg110+2))
Type "help" for help.

postgres=# select * from dev_dev.filter;
           filter_text            
----------------------------------
 EGOH ESACREPPU SEMOCEB TXET SIHT
(1 row)

postgres=# 

caller

普通のmacroを呼ぶときは、そのまま {{ macro_name() }} と呼ぶと思います。

じつは、callタグを使った呼び方もあり、その場合はcallerという無名macroが手に入ります.
百聞は一見にしかずということで。

macros/caller.sql
{%- macro add_hader(header) %}
    -- this is header
    -- {{ header }}
    {{ caller() }}
{%- endmacro %}
models/caller.sql
{{
    config(
        materialized='table',
    )
}}

{%- call add_hader(header='hogehoge') %}
    select '1'
{%- endcall %}

これを compileすると

    -- this is header
    -- hogehoge
    
    select '1'

このようになります。これだけでも面白いのですが、callの呼び方を工夫すると一風変わったmacroが作れます。

{%- macro select_value_unions(values=[]) %}
    {%- if values | length == 0 %}
        {{ exceptions.raise_compiler_error('values length is 0') }}
    {%- endif %}
    {%- for value in values %}
        {{ caller(value) }}
        {%- if not loop.last %} union all {%- endif %}
    {%- endfor %}
{%- endmacro %}
{{
    config(
        materialized='table',
    )
}}
{%- set values_json %}
[
    {"hoge": 1, "fuga": "aaaa"},
    {"hoge": 2, "fuga": "bbbb"}
]
{%-endset %}
{%- call(value) select_value_unions(values=fromjson(values_json)) %}
    select {{ value.hoge }} as hoge, '{{ value.fuga }}' as fuga
{%- endcall %}

このように、callに引数をつけると、その引数を受取るcallerが定義されます。

$ dbt build --select caller                        
09:48:55  Running with dbt=1.3.1
09:48:55  Found 3 models, 4 tests, 0 snapshots, 0 analyses, 314 macros, 0 operations, 0 seed files, 3 sources, 0 exposures, 0 metrics
09:48:55  
09:48:56  Concurrency: 4 threads (target='dev')
09:48:56  
09:48:56  1 of 1 START sql table model dev_dev.caller .................................... [RUN]
09:48:56  1 of 1 OK created sql table model dev_dev.caller ............................... [SELECT 2 in 0.20s]
09:48:56  
09:48:56  Finished running 1 table model in 0 hours 0 minutes and 0.44 seconds (0.44s).
09:48:56  
09:48:56  Completed successfully
09:48:56  
09:48:56  Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1

$ psql -h 127.0.0.1 -U postgres -p 5432 -d postgres
Password for user postgres: 
psql (14.2, server 14.5 (Debian 14.5-2.pgdg110+2))
Type "help" for help.

postgres=# select * from dev_dev.caller;
 hoge | fuga 
------+------
    1 | aaaa
    2 | bbbb
(2 rows)

postgres=# 

callerの例

前に @takimoさん がSSOTの文脈でクラスのmethodみたいなの作れないかな?呟いてたのをPoC的に書いたdbt packageがあります。

https://github.com/mashiike/dbt-methods

実は、このpackageはcallerとconfig組み合わせた例です。

models/sample.sql
{{
    config(
        materialized='view',
    )
}}

{%- call dbt_methods.def("prefix_added", arguments=["prefix"]) %}
    select '{{ dbt_methods.argument("prefix") }}' || name as name
    from {{ this }}
{%- endcall %}

{%- call dbt_methods.def("suffix_added", arguments=["suffix"]) %}
    select name || '{{ dbt_methods.argument("suffix") }}'as name
    from {{ this }}
{%- endcall %}

select 'hoge' as name
union all
select 'fuga'
union all
select 'piyo'
{{
    config(
        materialized='view',
    )
}}

with base as (
    {{ dbt_methods.call('sample', 'prefix_added', prefix='hoge') }}
)

select * from base

こうすると compileした結果が

with base as (
    select 'hoge' || name as name
    from "postgres".public.sample
)

select * from base

こんな感じで書けますよ。 というものです。

このdef macroの中身を覗くと次のようになってます。

https://github.com/mashiike/dbt-methods/blob/main/macros/def.sql#L1-L8

ここで渡されたcallerの中身をparse phaseでconfigに設定してます。

https://github.com/mashiike/dbt-methods/blob/main/macros/def.sql#L14-L23

そして、呼び出し時にそのconfigの中身を取り出して、 render()で描画して返しています。

https://github.com/mashiike/dbt-methods/blob/main/macros/call.sql#L1-L31


dbt macro tips Advent Calendar 2022 いかがでしたでしょうか?

dbtのmacroを書くための細々としたTips的なものを書き続けた25日でしたが楽しんでいただけたでしょうか。

dbtはデータ変換のお供になっているので、今回のシリーズを機会にいろいろなmacroを書くきっかけになったら幸いです。

Discussion