🔍

dbt の SQL と yaml の凡ミスを全部検知したい

2024/11/28に公開

こんにちは、大坪です。
前回の記事の取り組みで品質の単調増加を大事にしているという話をしました。これは外部品質を意図していますが、内部品質も単調増加させられることを期待しています。この記事では内部品質向上の一環として CI で実行しているコードバリデーションスクリプトの全文の公開と解説を行います。

結論を早く見たい人のために全文をここに貼ります。

validate.sh 全文

社内メンバーは GitHub 上でも確認できます

#!/usr/bin/env bash

set -e
set -o pipefail

cd "$(dirname "$0")/.." || exit 1

set +e

if [[ -n "$(git status --porcelain)" ]]; then
  echo "❌ Git に commit されていないファイルがあります。"
  git status
  exit 1
fi

# ./ なしだと GitHub Actions で stdin を読もうとして error になるので明示的につけている
violated_files=$(rg --multiline --files-with-matches --type=sql '(from|join)[ \n\t]+`' ./)
code=$?
set -e

if [ "$code" -eq 1 ]; then
  echo "✅ sql ファイルは正常"
elif [ "$code" -eq 2 ]; then
  echo "rg が失敗しました"
  exit 1
else
  echo "table は直接参照せずに、ref を使ってください"
  echo "参考: https://www.notion.so/medicu/101-e1294de5fbed4c579a43c92b023e0a93?pvs=4#4511327744bc4e32807ef8335a4dd9b5"
  echo ""
  echo " ok 'from {{ ref(\"some_table_name\") }}'"
  echo " ng 'from \`some_table_name\`'"
  echo ""
  echo "違反"
  echo "$violated_files"
  exit 1
fi

find models -type f | while read -r file; do
  if head -n 1 "$file" | grep -q "DO NOT EDIT: This file is generated by"; then
    rm "$file"
  fi
done

rye run python generate.py
if [[ -n "$(git status --porcelain)" ]]; then
  echo "❌ generate の実行し忘れを検知しました"
  echo "   dbt directory 内で下のコマンドを実行してください"
  echo "   \$ rye run python generate.py"
  git status
  exit 1
fi

# aqua の install 部分が $res に入らないように一度 pnpm を遣うことで事前に install を完了させる
echo "✅ prettier version $(pnpm dlx prettier --version)"

set +e
res=$(pnpm dlx prettier 'models/**/*.yml' --check 2>&1 1>/dev/null)
code=$?
set -e

if [ "$code" -eq 0 ]; then
  echo "✅ models/*.yml は正常"
else
  echo "下記の file が正しく format されていません"
  echo "$res"
  echo ""
  echo "format に関しては https://www.notion.so/medicu/code-formatter-2c62c1bb62cb4c868390d4f5fce138f6?pvs=4 を参照してください"
  echo "dbt directory で下記のコマンドで修正してください"
  echo '$ pnpm dlx prettier models/**/*.yml --write'
  exit 1
fi

set +e
res=$(pnpm dlx prettier 'one-icu-normalizer/**/*.ts' --check 2>&1 1>/dev/null)
code=$?
set -e

if [ "$code" -eq 0 ]; then
  echo "✅ one-icu-normalizer/**/*.ts は正常"
else
  echo "下記の file が正しく format されていません"
  echo "$res"
  echo ""
  echo "format に関しては https://www.notion.so/medicu/code-formatter-2c62c1bb62cb4c868390d4f5fce138f6?pvs=4 を参照してください"
  echo "dbt directory で下記のコマンドで修正してください"
  echo '$ pnpm dlx prettier one-icu-normalizer/**/*.ts --write'
  exit 1
fi

set +e
res=$(rye run sqlfmt models --check 2>&1 1>/dev/null)
code=$?
set -e

if [ "$code" -eq 0 ]; then
  echo "✅ models/*.sql は正常"
else
  echo "下記の file が正しく format されていません"
  echo "$res"
  echo ""
  echo "format に関しては https://www.notion.so/medicu/code-formatter-2c62c1bb62cb4c868390d4f5fce138f6?pvs=4 を参照してください"
  echo "dbt directory で下記のコマンドで修正してください"
  echo '$ rye run sqlfmt models'
  exit 1
fi

set +e
res=$(rye run black . --check 2>&1 1>/dev/null)
code=$?
set -e

if [ "$code" -eq 0 ]; then
  echo "✅ black は正常"
else
  echo "下記の file が正しく format されていません"
  echo "$res"
  echo ""
  echo "format に関しては https://www.notion.so/medicu/code-formatter-2c62c1bb62cb4c868390d4f5fce138f6?pvs=4 を参照してください"
  echo "dbt directory で下記のコマンドで修正してください"
  echo '$ rye run black .'
  exit 1
fi

前提など

コードの利用はOK

ここで記載するコードは大いに参考にしていただいて問題ありません。業務システムの本番環境に流用していただいて構いません。利用する場合はこちらのページへのリンクを張っていただけると嬉しいですが細かに何らかの対応を求める意図はありません。ただし当然ながら一切の責任を負いません。

社内リンクを含む

閲覧者が制限されているリンクは社内メンバーにとってのわかりやすさのためにあえて残しています。

MeDiCU は医療者がコードを書く会社

MeDiCU ではもちろんソフトウェアエンジニアリングを専門にする私もドメインロジックの実装をしますが、基本的には医師、薬剤師、医学部生などの医療関係者がコードを書いています。エンジニアは「医療者がドメインロジックに集中できる基盤を整える」ということに重きをおいています。したがって lint や formatter などがそもそも何であるかの理解からサポートすることが重要です。この点は多くの会社と異なる事情かもしれません。

内容

ではこのスクリプトの各セクションの内容を見ていきましょう。

コミットチェック

一番最初に下のように git に commit されていないファイルがないかを確認しています。

if [[ -n "$(git status --porcelain)" ]]; then
  echo "❌ Git に commit されていないファイルがあります。"
  git status
  exit 1
fi

CI 環境で誤ってファイルに変更が入るようなコマンドを実行していても気付けるようにしています。下のようなケースがありえます。

  • 依存の install で誤って lock ファイルを編集してしまっている
  • CI上での generator の実行などで Pull Request に現れない diff が本番適応されてしまう

ref の利用を強制

MeDiCU では dbt と BigQuery を利用しています。多くのケースで BigQuery 上でクエリの挙動を確かめてそれを VSCode などにペーストすることで新規のクエリ変更をコミットすることになります。このときに {{ ref("model_name") }} の形式を用いずに project.dataset.table とベタ書きしてしまうミスがよく起きます。MeDiCU の dataset 名や table は非常に長く手書きする人はいないためほぼ間違いなく backtick がはいる、という性質を活かして下の正規表現でこういったケースを検知しています。

(from|join)[ \n\t]+`

この部分の全文を書くと下のようになります。
rg 自体が失敗するケースがあるので $code2 でないことの確認が必要です

violated_files=$(rg --multiline --files-with-matches --type=sql '(from|join)[ \n\t]+`' ./)
code=$?
set -e

if [ "$code" -eq 1 ]; then
  echo "✅ sql ファイルは正常"
elif [ "$code" -eq 2 ]; then
  echo "rg が失敗しました"
  exit 1
else
  echo "table は直接参照せずに、ref を使ってください"
  echo "参考: https://www.notion.so/medicu/101-e1294de5fbed4c579a43c92b023e0a93?pvs=4#4511327744bc4e32807ef8335a4dd9b5"
  echo ""
  echo " ok 'from {{ ref(\"some_table_name\") }}'"
  echo " ng 'from \`some_table_name\`'"
  echo ""
  echo "違反"
  echo "$violated_files"
  exit 1
fi

自動生成ファイルの検知

まずは自動生成ファイルをすべて消します。ファイル名やディレクトリ名で match して削除する方法のほうが一般的かと思いますが、MeDiCU では「手書きで書いた SQL を少しずつ generator に移行する」という戦略を取っているためファイルの中身を見ないと自動生成されているかわかりません。そこで自動生成コメントを使ってマッチしています。一度全て消しているのは「generator が生成しなくなったファイルは存在してはいけない」という規約を守るためです。

find models -type f | while read -r file; do
  if head -n 1 "$file" | grep -q "DO NOT EDIT: This file is generated by"; then
    rm "$file"
  fi
done

その後自動生成プログラムを実行して実行し忘れを検知しています。ここで diff を取るためスクリプトの全体の実行開始時には diff がない状態を担保しておきたいというのが最初の処理の意図です。

rye run python generate.py
if [[ -n "$(git status --porcelain)" ]]; then
  echo "❌ generate の実行し忘れを検知しました"
  echo "   dbt directory 内で下のコマンドを実行してください"
  echo "   \$ rye run python generate.py"
  git status
  exit 1
fi

formatter の確認

MeDiCU では現在下の formatter を使っています。

言語 formatter
YAML prettier
TypeScript prettier
Python black
SQL sqlfmt

formatter のコードはほとんど同じなので prettier のみ書きます。実装者にフィードバックする方法はシェルスクリプトよりもリッチな方法がたくさんあります。その中でシェルスクリプトを選んでいるのは「医療者がコードを書く」という MeDiCU の特性があるからです。今後初めてコードを書く医療者が社内増えてくるときのストレスを減らし学べることを増やしたいということです。シェルスクリプトを使うことで下のことを狙っています。

  • エラーメッセージに慣れてもらう
    • まずは日本語のシンプルなエラーメッセージを見てもらうことで「エラーメッセージは読める」という実感を得てもらう
  • わかりやすいドキュメントへの誘導
    • コードを書き始めた人にもわかりやすい「formatter とは?」を説明するドキュメントへのリンクをつける
    • これによって「エラーメッセージは読むと便利」という感覚を得てもらう
  • エラーメッセージを自分で変更できる事を知る
    • 「エラーメッセージの意味を教えてほしい」という質問を受けるたびにその疑問に答えることに加えて「そのエラーメッセージをよりわかりやすいものに変える」ことをお願いする
    • これも積み上げの一つになるし、「わかりにくいものはわかりやすくできる」という実感があることはソフトウェア開発に重要


コードフォーマッターの社内向けドキュメントの抜粋

以上のようなことを考えて新しく人が増えても質問があんまり来なくなったある種の完成形が下の実装です。

set +e
res=$(pnpm dlx prettier 'models/**/*.yml' --check 2>&1 1>/dev/null)
code=$?
set -e

if [ "$code" -eq 0 ]; then
  echo "✅ models/*.yml は正常"
else
  echo "下記の file が正しく format されていません"
  echo "$res"
  echo ""
  echo "format に関しては https://www.notion.so/medicu/code-formatter-2c62c1bb62cb4c868390d4f5fce138f6?pvs=4 を参照してください"
  echo "dbt directory で下記のコマンドで修正してください"
  echo '$ pnpm dlx prettier models/**/*.yml --write'
  exit 1
fi

まとめ

以上が MeDiCU で利用している validate.sh の内容でした。これ以外にも様々な静的動的検知の仕組みを導入しているので少しずつこの場で紹介していければと思います。

MeDiCU

Discussion