この記事は、dbt Advent Calendar 2024 シリーズ2 の11日目の記事です。

dbt Advent Calendar 2024
https://qiita.com/advent-calendar/2024/dbt

dbt(Data Build Tool)の自動検証ツールに、
dbt-scoreというものがあり、試してみたので導入手順メモを残しておきます。

dbt-score
https://dbt-score.picnic.tech/

dbt-checkpointよりも用意されているルールは少ないですが、
dbt-scoreの方が、独自のルールが書きやすく導入しやすいかも知れないです。

この手順で試したversion

  • dbt-core==1.9.0rc2
  • dbt-postgres==1.8.2
  • dbt-score==0.8.0

導入手順

dbtのプロジェクトを準備

Gitのリポジトリ用にディレクトリを作ります。

mkdir dbt-score-study && cd $_
git init

Python仮想環境を用意します。

python -m venv venv
. venv/bin/activate
echo "venv" >> .gitignore

dbtをインストールします(ここでは、とりあえずPostgreSQL向けを入れます)。
# 本当は、さらっとsqliteで試したかったのですが、
# dbt-scoreが、dbt-coreのv1.6以上にしか対応しておらず、
# dbt-sqliteはv1.5から更新されていないため。

pip install dbt-postgres
pip freeze > requirements.txt

dbtプロジェクトを初期化します。

dbt init sample
echo "logs" >> .gitignore

profiles.ymlを用意します。

profiles.yml

sample:
  outputs:
    dev:
      dbname: dbt
      host: localhost
      pass: dbt
      port: 5432
      schema: dbt
      threads: 1
      type: postgres
      user: dbt
  target: dev

動作確認します。

DBT_PROJECT_DIR=sample dbt parse

dbt-scoreの導入

dbt-scoreをインストールします。

pip install dbt-score
pip freeze > requirements.txt

dbt-scoreを実行すると、自動検証が行われてスコアが表示されます。

DBT_PROJECT_DIR=sample dbt-score lint --run-dbt-parse
$ DBT_PROJECT_DIR=sample dbt-score lint --run-dbt-parse
🥈 M: my_first_dbt_model (score: 8.0)
    OK   dbt_score.rules.generic.columns_have_description
    OK   dbt_score.rules.generic.has_description
    WARN (low) dbt_score.rules.generic.has_example_sql: The model description does not include an example SQL query.
    WARN (medium) dbt_score.rules.generic.has_owner: Model lacks an owner.
    OK   dbt_score.rules.generic.sql_has_reasonable_number_of_lines

🥈 M: my_second_dbt_model (score: 8.0)
    OK   dbt_score.rules.generic.columns_have_description
    OK   dbt_score.rules.generic.has_description
    WARN (low) dbt_score.rules.generic.has_example_sql: The model description does not include an example SQL query.
    WARN (medium) dbt_score.rules.generic.has_owner: Model lacks an owner.
    OK   dbt_score.rules.generic.sql_has_reasonable_number_of_lines

Project score: 8.0 🥈

既定で適用されたルールについては、以下に説明があります。

Generic | Rules | dbt-score
https://dbt-score.picnic.tech/rules/generic/

ここからコードも確認でき、
独自ルール実装の参考になるので、1つくらい見ておくと良いと思います。

独自ルールの作成

それでは独自のルールを実装してみます。

参考: Create rules | dbt-score
https://dbt-score.picnic.tech/create_rules/

ここでは、dbtのBestPracticesに記載されているものを1つ実装してみます。

How we style our dbt models | dbt
https://docs.getdbt.com/best-practices/how-we-style/1-how-we-style-our-dbt-models

上記ページの、次のルールを実装することにします。

Timestamp columns should be named _at(for example, created_at) and should be in UTC. If a different timezone is used, this should be indicated with a suffix (created_at_pt).

実装は、次のようになります。

dbt_score_rules/custom_rule.py

import re
from dbt_score import Model, RuleViolation, rule

@rule
def column_name_convention_timestamp(model: Model) -> RuleViolation | None:
    """Column name convension."""
    ng_cols = [
        c.name for c in model.columns
        if c.data_type and c.data_type.lower() == "timestamp"
          and not (re.match(".*_at$", c.name) or re.match(".*_at_[^_]*$", c.name))
    ]
    if ng_cols:
        return RuleViolation(message=f"Timestamp columns should be named <event>_at or <event>_at_<tz>: {','.join(ng_cols)}")

試しにschema.ymlを変更して、実行してみます。

sample/models/example/schema.yml ※created_at,create_timeカラムを追加

version: 2

models:
  - name: my_first_dbt_model
    description: "A starter dbt model"
    columns:
      - name: id
        description: "The primary key for this table"
        data_tests:
          - unique
          - not_null
      - name: created_at
        data_type: timestamp
      - name: create_time
        data_type: timestamp

  - name: my_second_dbt_model
    description: "A starter dbt model"
    columns:
      - name: id
        description: "The primary key for this table"
        data_tests:
          - unique
          - not_null
$ DBT_PROJECT_DIR=sample dbt-score lint --run-dbt-parse
🥈 M: my_second_dbt_model (score: 8.3)
    OK   dbt_score.rules.generic.columns_have_description
    OK   dbt_score.rules.generic.has_description
    WARN (low) dbt_score.rules.generic.has_example_sql: The model description does not include an example SQL query.
    WARN (medium) dbt_score.rules.generic.has_owner: Model lacks an owner.
    OK   dbt_score.rules.generic.sql_has_reasonable_number_of_lines
    OK   dbt_score_rules.custom_rule.column_name_convention_timestamp

🥉 M: my_first_dbt_model (score: 6.1)
    WARN (medium) dbt_score.rules.generic.columns_have_description: Columns lack a description: created_at, create_time.
    OK   dbt_score.rules.generic.has_description
    WARN (low) dbt_score.rules.generic.has_example_sql: The model description does not include an example SQL query.
    WARN (medium) dbt_score.rules.generic.has_owner: Model lacks an owner.
    OK   dbt_score.rules.generic.sql_has_reasonable_number_of_lines
    WARN (medium) dbt_score_rules.custom_rule.column_name_convention_timestamp: Timestamp columns should be named <event>_at or <event>_at_<tz>: create_time

Project score: 7.2 🥉

my_first_dbt_modelについて、カラム名が不適切という指摘がされました。

dbt-scoreの設定 (カスタムルールのみ実行・Severityの設定)

続いて、dbt-scoreの設定についてです。

参考: Configuration | dbt-score
https://dbt-score.picnic.tech/configuration/

カスタムで作成したルールのみを実行したい場合は、
rule_namespacesで、先ほど作成したルールのモジュールを指定します。

先ほどの作成したルールは「WARN (medium)」で指摘されており、
これにより、Projectのscoreが低下していました。
このルールが厳守すべきであれば、これをcriticalにします。
するとscoreが0点になり、検証にfailさせることができます。
この場合はruleのseverityを4(critical)にします。

pyproject.toml

[tool.dbt-score]
rule_namespaces = ["dbt_score_rules"]

[tool.dbt-score.rules."dbt_score_rules.custom_rule.column_name_convention_timestamp"]
severity = 4

実行結果は次の通り、検証にfailしexit_codeも1になっています。

$ DBT_PROJECT_DIR=sample dbt-score lint --run-dbt-parse
🥇 M: my_second_dbt_model (score: 10.0)
    OK   dbt_score_rules.custom_rule.column_name_convention_timestamp

🚧 M: my_first_dbt_model (score: 0.0)
    WARN (critical) dbt_score_rules.custom_rule.column_name_convention_timestamp: Timestamp columns should be named <event>_at or <event>_at_<tz>: create_time

Project score: 0.0 🚧

Error: evaluable score too low, fail_any_item_under = 5.0
Model my_first_dbt_model scored 0.0
$ echo $?
1

ルール適用対象の除外

特定のモデルをルールの適用対象から除外したい場合は、フィルターを使います。

参考: Filtering rules | dbt-score
https://dbt-score.picnic.tech/create_rules/#filtering-rules

フィルターの実装は、次のようになります。

dbt_score_rules/custom_filter.py

from dbt_score import Model, rule_filter

IGNORE_MODELS = [
    "my_first_dbt_model",
]

@rule_filter
def ignore_models(model: Model) -> bool:
    """Ignore models in ignore list."""
    return model.name not in IGNORE_MODELS

フィルターを適用したいルールに、フィルターを指定します。
この場合はruleのrule_filter_namesにフィルター名を記載します。

pyproject.toml

[tool.dbt-score]
rule_namespaces = ["dbt_score_rules"]

[tool.dbt-score.rules."dbt_score_rules.custom_rule.column_name_convention_timestamp"]
severity = 4
rule_filter_names = ["dbt_score_rules.custom_filter.ignore_models"]

実行結果は次の通り、my_first_dbt_modelの検証がskipしています。

$ DBT_PROJECT_DIR=sample dbt-score lint --run-dbt-parse
🥇 M: my_second_dbt_model (score: 10.0)
    OK   dbt_score_rules.custom_rule.column_name_convention_timestamp

🥇 M: my_first_dbt_model (score: 10.0)

Project score: 10.0 🥇

以上。