dbtでRecceを使うといい感じに修正の検証ができて良い

# dbtでのデプロイの検証について

dbtに限らずETLプロセスの修正をデプロイする際には、修正の影響がある指標・属性について以下のような検証を行う必要がある (と思っている)

- 修正前後で集計結果に差異がないか
- 修正前後で集計結果に差異は発生しているが、発生原因は説明がつくものか [^1]

dbt(v1.8~)で用意されているテストとして以下の2つがあるが、これらのテストだけでは、上記が問題ないことを保証しきることは難しいと思っている

- Data tests : 主にデータの品質確認のために実施する
- Unit tests : 主にモデルのロジックが正しいかを実施する

https://docs.getdbt.com/docs/build/unit-tests#when-to-add-a-unit-test-to-your-model

`Recce`を使えば、dbtの修正の検証が容易にでき、かつ検証の自動化まで行えそうなことが分かったため、使用方法やどんな感じのことができるかをまとめたいと思う

# Recceについて

以下は公式サイトからの引用

> Recce is a data-change validation tool for dbt projects. Recce provides a convenient platform to perform cross-environment diffs as data modeling validations. Use these validations in your pull request (PR) comment to demonstrate proof-of-correctness of your modeling changes and speed up thew QA process of PR review.

https://datarecce.io/docs/

`Recce`はdbtの異なる環境間を比較検証するためのプラットフォームで、プルリクで`Recce`を使うことで、検証のプロセスを加速させることができるよ、というようなことを書いていると思う

# 使い方

説明の都合上、どのようにすれば使えるかを先に説明する

デモ用のリポジトリを用意しているので、こちらを使えばRecceを手元で確認できると思う

https://github.com/ni4shi4/jaffle_shop_duckdb_with_recce

## 手順

前提として、環境が2つ以上必要となる。以下はDuckDBにつなぐ際のprofiles.ymlの例である [^2]

```yml
jaffle_shop:
  target: dev
  outputs:
    dev:
      type: duckdb
      path: 'jaffle_shop.duckdb'
      schema: main
    prod:
      type: duckdb
      path: 'jaffle_shop.duckdb'
      schema: prod
```

`Recce`をインストールする

```shell
pip install -U recce
```

`Recce`の機能をフルに使うために、package.ymlに以下の2つのパッケージを追加したうえで、`dbt deps`を実行する

```yml
packages:
  - package: dbt-labs/audit_helper
    version: 0.11.0
  - package: data-mie/dbt_profiler
    version: 0.8.1
```

`Recce`はdbt docsと同様に`Recce`サーバーをlocalにホストして、Webブラウザ上で使うことになるが、`Recce`サーバーをホストするために以下の2つのアーティファクトが必要となる

- manifest.json
- catalog.json

そのため、これらのアーティファクトを比較元(prod環境)と比較先(dev環境)で用意する必要がある。さらに、アーティファクトの保存先を以下のように環境ごとに分ける必要がある

| 環境 | アーティファクトの保存ディレクトリ |
|:-:|:-:|
| prod環境  |  `target-base/` |
| dev環境  |  `target/` (デフォルトの保存先) |

比較元(prod環境)でのアーティファクト作成は以下のように行う

```shell
git switch main # mainブランチがprod環境と同期していると仮定

dbt build -t prod
dbt docs generate -t prod --target-path target-base/
```


比較先(dev環境)でのアーティファクトの作成は以下のように行う

```shell
git switch feature/my-awesome-feature # 修正を行っているブランチに移動する

dbt build
dbt docs generate
```

最後に、`Recce`サーバーを起動する。サーバーへはブラウザからアクセスできる

```shell
recce server
```

# Recceの特徴

`Recce`は以下のような機能を含んでいる

- WebUIによる修正の検証
- CLIによる修正の検証・検証の自動化
- 検証結果をstateファイルとして保存・共有する
- 定常的に行う比較をpresetとして登録・コード管理し、自動的に検証できるようにする

## WebUIによる修正の検証

Webブラウザからは以下の3つのページが確認できる

- Lineage
- Query
- Checks

### Lineageページ

Recceで基本となるところ(だと思っている)で、リネージグラフ上で修正があったモデルのみが確認できる。非常に見やすい (修正があったモデル以外も含めて表示することも可能)

![lineage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2226432/6d084a8d-4498-8f1c-ceb6-20575b68d8f0.png)


リネージグラフ上の各モデルをクリックすると、モデルごとの比較結果が確認できる(画像では、ordersをクリックしている)

![orders.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2226432/67a1f8ea-c45c-640c-6a13-f0290939743f.png)

デフォルトでは、スキーマの比較画面とカラム数の比較(`99 rows`のところ)が見られる。上の`</> Diff`、下の`Advanced Diffs`をクリックすることで、そのほかの比較が確認できる

`</> Diff`をクリックするとモデルごとのコードのDiffが確認できる。リネージグラフからたどり着いているので、GitHub上で確認するよりもコードの理解度が高くなると思う

![codediff.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2226432/6519abb8-67dd-3e4b-8cf9-6b6f480f1982.png)

`Advanced Diffs`からは現状4種類の比較ができる

| 項目 | 比較内容 | 特に有用なカラム |
|:-:|:-|:-:|
| Profile Diff  | カラムごとに基礎統計量を比較する  | any |
| Value Diff  | カラムごとにどのくらい値が一致しているかを比較する  | any |
| Top-K Diff  | 選択したカラムについて、取りうる値ごとに値の件数を比較する。占有割合が大きい順にソートされて表示される  | カテゴリカラム |
| Histogram Diff  | 選択したカラムを基にヒストグラムを作成し、比較する  | 数値カラム |

実際の画面は以下のようになる

- Profile Diff

![profile.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2226432/5651cda5-314c-4c3f-3ca8-f34a8a26f7d9.png)

- Value Diff 

![value_diff.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2226432/1a4d4400-2462-de56-6343-60a494b82f44.png)

- Top-K Diff

![top_k.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2226432/74596d4e-4462-56ab-0d11-9d8fd450ff5a.png)

- Histogram Diff

![histogram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2226432/c810096a-71a1-0034-b62a-880317f2098c.png)

`Add check`や`Add to checklist`をクリックすると後述するChecksページに比較結果を追加できる

### Queryページ

クエリを修正前後のモデルに対して発行して、クエリ結果を簡単に比較できる。クエリにはJinjaテンプレートを使用できる(`ref`以外のマクロも使えると思う)

![query.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2226432/29f3ad48-eb3b-4f36-2dc7-dc6428eac1c6.png)

UIは以下のようになっている
- `Run`ボタンもしくはcommand(ctrl)+Enterでクエリを実行可能。`Run diff`で修正前後の両方のモデルに対してクエリを実行し比較できる
- 画面右の`Primary key`が修正前後を比較する際のキーになる(なくても比較はできる)
- 画面右下の`Changed only`をオンにすれば、差異があったレコードだけに絞れる
- クエリ結果右上の`+`から、Checksページに比較結果を追加できる

### Checksページ

今までで追加した比較結果ともともとpresetとして設定されていた比較結果を確認できる。
各比較結果にはチェックをつけることが可能

![preset.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2226432/07e1513c-840b-7dbb-d7a0-f94037786a86.png)

### 比較結果のコピー・保存

比較結果のコピーと保存は以下のように簡単にできる

- 比較結果を画像としてコピー
    - Lineageページでいうと、下画像右端の赤枠で囲ったコピーマークをクリックすると、リネージグラフのコピーができ、プルリクのコメント等にそのまま貼り付けることができる([貼り付けた結果](https://github.com/ni4shi4/jaffle_shop_duckdb_with_recce/pull/1))。ほかの画面でもワンクリックでコピーができる
- 比較結果をstateファイルとして保存
    - 画面右上のエクスポートボタンを押すと、Checksで新たに追加した比較結果を含めてstateファイルとして保存ができる。stateファイルについては後述する


![save.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2226432/51aa864d-7a10-9600-131c-8b1d257319fe.png)

## CLIによる修正の検証・検証の自動化

CLIコマンドとしては以下の2つが用意されている

| コマンド | 処理内容 |
|:-:|:-|
| `recce run` | 比較結果をstateファイル(recce-state.json)に出力  |
| `recce summary`  | stateファイルのサマリをmd形式で表示  |

`recce summary`コマンドによって生成される結果は、githubのコメント用に最適化されている([表示例](https://github.com/ni4shi4/jaffle_shop_duckdb_with_recce/pull/1#issuecomment-2139595295))ので、例えば、Github Actions内で、
1. recce runでステートファイル作成
1. recce summaryでサマリのテキスト作成
1. ghコマンド等でサマリのテキストをプルリクのコメントとして追加

とすることで、CI/CDに組み込むことができる

上記を含む内容をすでに公式は用意しているので、詳しい実装に興味がある人は以下リンクを確認してほしい

https://datarecce.io/docs/guides/scenario-ci/

## 検証結果をstateファイルとして保存・共有する

WebUIやCLIの説明ですでに登場しているが、比較・検証結果をstateファイルというjsonファイルに保存できる

作成したstateファイル(=`recce-state.json`)を元に以下コマンドを使うと、レビューモードで`Recce`サーバーを起動できる

```sh
recce server --review recce-state.json
```

stateファイルは以下の情報から構成される

| 項目 | JSONキー | 内容 |
|:-:|:-:|:-:|
| Checks  | `"checks"`  |  Checksページに表示されている比較の設定 |
| Runs  | `"runs"`  | Checksページに表示されている比較の実行結果  |
| Environments' Artifacts  | `"artifacts"`  | 比較元と比較先のアーティファクト(=manifest.json, catalog.json)  |
| Runtime Information  | `"git"`  | gitのブランチの情報など  |

例えば、プルリクのレビュープロセスにおいて、プルリク作成者がstateファイルを保存し、レビュアーに共有することができる。レビュアーは、レビューモードでReccaサーバーを起動することで、

- 比較の実行結果(=Runs)が保持されているので、レビュアーはクエリを実行することなく比較・検証結果を確認できる
- アーティファクト情報が保持されているので、レビュアーはdbt docs generateなどの追加のコマンドなしに、`Recce`サーバー上で比較クエリを発行することができる

```plantuml
@startuml
<style>
root {
  Padding 0
  Margin 0
}
footer {
  FontColor black
  FontSize 50
}
</style>
footer 公式サイトより拝借した図
title <img:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2226432/2af273ff-4905-a1d5-4501-decb7a9f8f1a.png>
@enduml
```

## 定常的に行う比較をpresetとして登録・コード管理する

dbtプロジェクトのルートディレクトリに以下のような内容を、recce.ymlとして保存することで、`Recce`サーバーのChecksページ上にpresetとして登録することができる
また、`recce run`コマンドでも、presetとして登録された比較が実行されるので、比較を自動化する際には必須の設定となる

```yml
checks:
  - name: Query diff of customers
    description: |
        This is the demo preset check.

        Please run the query and paste the screenshot to the PR comment.
    type: query_diff
    params:
        sql_template: select * from {{ ref("customers") }}
    view_options:
        primary_keys:
          - customer_id
        changed_only: true
```

現在は設定項目に関するドキュメントはないので、WebUIからコードを取得するしかない(下画像参照)が、検証の自動化のためには強力なツールとなる

![preset_code.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2226432/906e6044-241c-2655-08be-64341821ea6a.png)

## 読み方

Recceは「レーキー」と読むらしい(/ˈrɛki/: 'reconnaissance'の略)

https://www.britannica.com/dictionary/eb/audio?file=recce001&format=mp3&word=recce&pron=%CB%88r%C9%9Bki

# 注意事項

## stateファイルについて

前述したように、stateファイルはクエリの実行結果を含むので、クエリ結果に個人情報が含まれないように気を付ける必要がある。そのため、例えば以下のような対策が必要になってくると思われる

- カラムレベルマスキング等でクエリを行っても個人情報が表示されないようにする
- CI/CDで自動化する際に生成されるstateファイルを定期的に削除する

## Snowflakeでの使用について

Snowflakeをよく使うので、Snowflakeで使用するうえでの注意点をまとめておく

Snowflake上で実行する場合の`profiles.yml`は以下のようになると思われる
```yml
jaffle_shop:
  target: dev
  outputs:
    dev:
      type: snowflake
      account: <orgname>-<account_name>
      role: dev_role
      database: dev_db
      schema: public
      warehouse: dev_wh
      user: "{{ env_var('DBT_USER') }}"
      password: "{{ env_var('DBT_PASSWORD') }}"
    prod:
      type: snowflake
      account: <orgname>-<account_name>
      role: prod_role
      database: prod_db
      schema: public
      warehouse: prod_wh
      user: "{{ env_var('DBT_USER') }}"
      password: "{{ env_var('DBT_PASSWORD') }}"
```

### `Recce`の挙動

`recce server`を実行する際の挙動は以下のようになっている

| 参照 | profiles.ymlの設定項目 |
|:-:|:-:|
| 常に環境ごとの設定値が参照されるもの  | `database`  |
| コマンド実行時に指定した環境の設定値が参照されるもの  | `warehouse`, `role`  |

`profiles.yml`に記載した通り、`database`は比較元と比較先で異なるものを設定していても大丈夫だが、ウェアハウスとロールはコマンド実行のものとなる (おそらくユーザーも)
つまり、上記の`profiles.yml`に記載した通りの設定を行った場合、下表のようになる

| 実行コマンド |  実際に使用されるロール | 実際に使用されるウェアハウス |
|:-:|:-:|:-:|
| `recce server`  | `dev_role`  | `dev_wh`  |
| `recce server -t prod` [^3] | `prod_role`  | `prod_wh`  |

例えば、`dev_role`に`prod_db`へのアクセス制限がない状態で`recce server`を実行すると、比較を行うクエリが権限不足で失敗してしまうので、例えば以下のいずれかで対応する必要がある

- `prod_role`に`dev_db`への読み取り権限を与え、`recce server -t prod`と実行するようにする
- `prod_db`と`dev_db`の両方に読み取り権限を持つロール`recce_role`を追加して、recce用のprofileを作る。実行は`recce server -t recce`とする
    ```yml
        recce:
          type: snowflake
          account: <orgname>-<account_name>
          role: recce_role
          database: dev_db
          schema: public
          warehouse: recce_wh # recce用のウェアハウス
          user: "{{ env_var('DBT_USER') }}"
          password: "{{ env_var('DBT_PASSWORD') }}"
    ```

~~いずれも方法においても、レビューモードでは注意が必要~~
レビューモードでも同様にターゲットを指定して実行することが可能になっている [^4]

### その他細かい注意点

- [プリセット](#定常的に行う比較をpresetとして登録コード管理する)で説明したrecce.ymlのprimary_keysだが、Snowflakeの場合は通常大文字で指定してやる必要がある
    - `Recce`内部では、primary_keyで指定したものが、データベース上に存在するか確認しているっぽい
    - 現状その照合で大文字と小文字を区別しているので、小文字にすると、primary_keyがないとエラーになる

    ```yml
            primary_keys:
              - CUSTOMER_ID
    ```

## できないこと

- 参照先のDBが異なる場合は利用できない(devをDuckDB、prodをSnowflakeにする等)
    - そのため、dbtの基盤を別DBに移行するというシナリオでは使用できないと思われる
- dbtでの検証を対象にしているため、DWHの移行で既存のETLをdbtにリプレイスする際の検証にも使用できない

# 最後に

見つけた勢いのまま書いてしまったが、いい感じのパッケージなので、今後積極的に使っていきたいと思った(いい感じを多用している)

# 参考

- 公式ドキュメント

https://datarecce.io/docs/

- ここで、`Recce`の紹介がされていた

https://medium.com/inthepipeline/from-zero-to-dbt-how-to-analyze-and-build-data-models-from-spotifys-million-playlist-data-241c3d8c9b5d

- ベストプラクティス

https://datarecce.io/docs/guides/best-practices-prep-env/

- 公式のプルリクのデモ(いい感じ)

https://github.com/DataRecce/jaffle_shop_duckdb/pull/1


[^1]: 返品した商品を売上から除外するように修正したため、売上は修正前に比べて小さくなったとか
[^2]: pathについて、環境ごとに違うファイルを指定することは可能だが、この場合は、`Recce`はエラーとなってしまう
[^3]: `recce server`時に、`--target`もしくは`-t`を指定することで、実行時の環境が選べるようになっている
[^4]: レビューモードの場合は、targetオプションが効かず、デフォルトの環境を参照するようになっていると思われる。バグっぽいので、Issueで問い合わせ中 → 問い合わせた結果、すぐに修正してもらうことができた ([v0.20.1](https://github.com/DataRecce/recce/releases/tag/v0.20.1)) :relaxed:
ただ期待されていた挙動としては、stateファイル作成時の環境で実行されることっぽいので、将来的には、stateファイルにtargetの情報が含まれる + レビューモードではtargetの指定が不要( → stateファイルのtargetの情報が使用される)みたいになるかもしれない