dbt macro tips advent calendar 2022 day 17 - persist_docs改造
便利なデータ変換ツールである dbt の中のmacroに関するtipsを書いていく dbt macro tips Advent Calendar 2022 17日目です。
persist_docs改造
皆様、persist_docs という機能はご存知でしょうか?
models:
<resource-path>:
+persist_docs:
relation: true
columns: true
こうすると、いい感じにDWHのコメントとかにdocs情報を残してくれる機能ですね。
いい感じに残してくれるんですが、残してくれるのはdescriptionだけなんですね。
metaの情報も残してほしかったり、構造化した形で保存してほしかったりしませんか?
ということで、persist_docsを改造したいと思います。
突然ですが、 一度 深淵(Custom Materialization)を覗いたことがある方はご存知かもしれませんが、{{ config(materilaized='table') }}
と指定した場合の実際のTableを作るためのコードはいかにあります。
このへんを参考に dbt-coreを眺めると、 persist_docs
というmacroを呼び出している部分があります。
dbt 1.3.1時点ではどうやら、persist_docsというmacroがあり、それがこの機能の実態のようです。
もちろんドキュメントには載ってないので、将来的に変わるかもしれませんが・・・
さて、このmacroの実態を探しましょう。
こちらのようです。ふむふむ。なるほど、どうやら alter_column_comment
と alter_relation_comment
を呼び出してコメントを変えているようです。
※ 注意深く読んでいくと、実はこの2つのmacroはadapterの実装側で定義するようになっているようです。例えばBigQueryはAdapter側でこの2つのmacroは中身は空でcreate_table_as macroの一部でOptionsとして設定するようになっているようです。 なので、実はこういうDeep Diveな改変をするときはadapterの実装まで見に行ったほうがいいですね。 閑話休題
では、postgresのadapter側の実装を見ていきましょう。
このように、実装を追っていくと段々とどう動いているのが見えてきましたね。
さて、では実際のコメントをどうするかを考えます。
こういうmacroがありますので、meta情報も含めたjsonでdocsを作るように改造してみましょう。
改造した結果がこちらとなります。
{%- 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 %}
試しに使ってみましょう
{{
config(
materialized='table',
persist_docs={"relation": true, "columns": true},
)
}}
select 'hoge' as name, 20 as age
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