🙌

dbt macro tips advent calendar 2022 day 17 - persist_docs改造

2022/12/17に公開

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


persist_docs改造

皆様、persist_docs という機能はご存知でしょうか?

https://docs.getdbt.com/reference/resource-configs/persist_docs

models:
  <resource-path>:
    +persist_docs:
      relation: true
      columns: true

こうすると、いい感じにDWHのコメントとかにdocs情報を残してくれる機能ですね。
いい感じに残してくれるんですが、残してくれるのはdescriptionだけなんですね。
metaの情報も残してほしかったり、構造化した形で保存してほしかったりしませんか?

ということで、persist_docsを改造したいと思います。

突然ですが、 一度 深淵(Custom Materialization)を覗いたことがある方はご存知かもしれませんが、{{ config(materilaized='table') }} と指定した場合の実際のTableを作るためのコードはいかにあります。

https://github.com/dbt-labs/dbt-core/tree/dev/louisa-may-alcott/core/dbt/include/global_project/macros/materializations

このへんを参考に dbt-coreを眺めると、 persist_docsというmacroを呼び出している部分があります。

https://github.com/dbt-labs/dbt-core/blob/0544b085439b3a635b8ce56adbf56d8e7c7e6839/core/dbt/include/global_project/macros/materializations/models/table/table.sql#L48

dbt 1.3.1時点ではどうやら、persist_docsというmacroがあり、それがこの機能の実態のようです。
もちろんドキュメントには載ってないので、将来的に変わるかもしれませんが・・・

さて、このmacroの実態を探しましょう。

https://github.com/dbt-labs/dbt-core/blob/0544b085439b3a635b8ce56adbf56d8e7c7e6839/core/dbt/include/global_project/macros/adapters/persist_docs.sql#L25-L33

こちらのようです。ふむふむ。なるほど、どうやら alter_column_commentalter_relation_comment を呼び出してコメントを変えているようです。

※ 注意深く読んでいくと、実はこの2つのmacroはadapterの実装側で定義するようになっているようです。例えばBigQueryはAdapter側でこの2つのmacroは中身は空でcreate_table_as macroの一部でOptionsとして設定するようになっているようです。 なので、実はこういうDeep Diveな改変をするときはadapterの実装まで見に行ったほうがいいですね。 閑話休題

では、postgresのadapter側の実装を見ていきましょう。

https://github.com/dbt-labs/dbt-core/blob/0544b085439b3a635b8ce56adbf56d8e7c7e6839/plugins/postgres/dbt/include/postgres/macros/adapters.sql#L176-L189

このように、実装を追っていくと段々とどう動いているのが見えてきましたね。
さて、では実際のコメントをどうするかを考えます。

https://docs.getdbt.com/reference/dbt-jinja-functions/tojson

こういうmacroがありますので、meta情報も含めたjsonでdocsを作るように改造してみましょう。

改造した結果がこちらとなります。

overrides.sql
{%- macro postgres__persist_docs(relation, model, for_relation, for_columns) -%}
    {%- if for_relation and config.persist_relation_docs() and (model.description or model.meta) %}
        {%- set comment_dict = {} %}
        {%- if model.description %}
            {%- do comment_dict.update({'description': model.description}) %}
        {%- endif %}
        {%- if model.meta %}
            {%- do comment_dict.update(model.meta) %}
        {%- endif %}
        {%- set comment = tojson(comment_dict) %}
        {%- do run_query(alter_relation_comment(relation, comment)) %}
    {%- endif %}

    {%- if for_columns and config.persist_column_docs() and model.columns %}
        {%- do run_query(alter_column_comment(relation, model.columns)) %}
    {%- endif %}
{%- endmacro %}

{% macro postgres__alter_column_comment(relation, column_dict) %}
    {%- set existing_columns = adapter.get_columns_in_relation(relation) | map(attribute="name") | list %}
    {%- for column_name in column_dict if (column_name in existing_columns) %}
        {%- set comment_dict = {} %}
        {%- if 'description' in column_dict[column_name] %}
            {%- do comment_dict.update({'description': column_dict[column_name]['description']}) %}
        {%- endif %}
        {%- if 'meta' in column_dict[column_name] %}
            {%- do comment_dict.update(column_dict[column_name]['meta']) %}
        {%- endif %}
        {%- set comment = tojson(comment_dict) %}
        {%- set escaped_comment = postgres_escape_comment(comment) %}
        comment on column {{ relation }}.{{ adapter.quote(column_name) if column_dict[column_name]['quote'] else column_name }} is {{ escaped_comment }};
    {%- endfor %}
{% endmacro %}

試しに使ってみましょう

models/person.sql
{{
    config(
        materialized='table',
        persist_docs={"relation": true, "columns": true},
    )
}}

select 'hoge' as name, 20 as age
models/schema.yml
version: 2

models:
  - name: person
    description: |
      コレは人間です
    meta:
      owner: admin
      sensitive: true
      logical_name: 人間テーブル
      logical_path: /
    columns:
      - name: name
        description: 人間さんの名前です。
        meta:
          logical_name: 名前
          logical_path: /人間
      - name: age
        description: |
          人間さんの年齢です
          10の位で切り捨ててます!!!!
        meta:
          logical_name: 年齢
          logical_path: /人間

さて、実行してみた結果どうなるでしょう。

$ dbt build --select person                        
05:51:17  Running with dbt=1.3.1
05:51:17  Change detected to override macro used during parsing. Starting full parse.
05:51:18  call ref(('my_first_dbt_model',)) from "postgres"."dev"."my_second_dbt_model"
05:51:19  Found 3 models, 0 tests, 0 snapshots, 0 analyses, 292 macros, 0 operations, 0 seed files, 0 sources, 0 exposures, 0 metrics
05:51:19  
05:51:19  Concurrency: 4 threads (target='dev')
05:51:19  
05:51:19  1 of 1 START sql table model dev.person ........................................ [RUN]
05:51:19  1 of 1 OK created sql table model dev.person ................................... [SELECT 1 in 0.24s]
05:51:19  
05:51:19  Finished running 1 table model in 0 hours 0 minutes and 0.53 seconds (0.53s).
05:51:19  
05:51:19  Completed successfully
05:51:19  
05:51:19  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 schemaname, relname as tablename, description                                                                                                                                                                                                                                                                                      from pg_stat_user_tables left join pg_description on pg_description.objsubid = 0 and  pg_stat_user_tables.relid = pg_description.objoid                                                                                                                                                                                                              where schemaname = 'dev' and relname = 'person';
 schemaname | tablename |                                                                                    description                                                                                    
------------+-----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 dev        | person    | {"description": "\u30b3\u30ec\u306f\u4eba\u9593\u3067\u3059\n", "owner": "admin", "sensitive": true, "logical_name": "\u4eba\u9593\u30c6\u30fc\u30d6\u30eb", "logical_path": "/"}
(1 row)

postgres=# select schemaname, relname as tablename, attname as columnname, description                                                                                                                                                                                                                                                               from pg_stat_user_tables inner join pg_attribute on  pg_stat_user_tables.relid = pg_attribute.attrelid inner join pg_description on pg_description.objsubid = pg_attribute.attnum  where schemaname = 'dev' and relname = 'person';
 schemaname | tablename | columnname |                                                                                                   description                                                                                                    
------------+-----------+------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 dev        | person    | name       | {"description": "\u4eba\u9593\u3055\u3093\u306e\u540d\u524d\u3067\u3059\u3002", "logical_name": "\u540d\u524d", "logical_path": "/\u4eba\u9593"}
 dev        | person    | age        | {"description": "\u4eba\u9593\u3055\u3093\u306e\u5e74\u9f62\u3067\u3059\n10\u306e\u4f4d\u3067\u5207\u308a\u6368\u3066\u3066\u307e\u3059!!!!\n", "logical_name": "\u5e74\u9f62", "logical_path": "/\u4eba\u9593"}
(2 rows)

postgres=# 

エンコードされてるのでちょっとわかりにくいですが、ちゃんと入ってそうですね。
ということで、dbt-coreのコードを読むことによって、dbtのシステムの挙動をカスタマイズできるという例でした。


18日目はhookの実装のdeep-diveな話をします。

Discussion