Claude Media
Claude CodeでSQLをsqlfluffで整える — dbtプロジェクトのlintをhookに入れる

Claude CodeでSQLをsqlfluffで整える — dbtプロジェクトのlintをhookに入れる

Claude Codeが.sqlを編集した直後にsqlfluff fixを走らせるPostToolUse hookと、dbtプロジェクトでjinjaとdbtのtemplaterを使い分ける設定をまとめます。

sqlfluffをhookに載せると何が固定されるか

SQLFluffは、方言ごとの違いに対応するSQLのリンターです。sqlfluff lintで指摘を出し、sqlfluff fixで直せる分を書き換えます。これをClaude CodeのPostToolUse hookに載せると、Claudeが.sqlファイルを書くたびに、空白・インデント・大文字小文字といった整形規約が機械的にそろいます。

規約をCLAUDE.mdに文章で書いても、守られるかどうかはモデル任せです。hookなら、守られなかった分を編集の直後に直します。直せない指摘だけをClaudeへ返せば、人間のレビューに回る前に潰せます。

dbtプロジェクトでは、もう一つ判断が要ります。SQLFluffにはJinjaとdbtの2種類のtemplater(テンプレートを展開する仕組み)があり、hookに向くのは前者です。この記事では、動作確認、.sqlfluffの設定、hookスクリプト、templaterの使い分け、直らなかった指摘の返し方を順に扱います。言語を問わないLint自動修正の考え方はPostToolUse hookのLint自動修正の早見表にまとめてあります。

まず手元でlintの出力を読む

SQLFluffはpipで入ります。公式のGetting Startedにある確認手順をそのまま実行します。

pip install sqlfluff
echo "  SELECT a  +  b FROM tbl;  " > test.sql
sqlfluff lint test.sql --dialect ansi

公式ページに載っている出力は、次のとおりです。

== [test.sql] FAIL
L:   1 | P:   1 | LT01 | Expected only single space before 'SELECT' keyword.
                        | Found '  '. [layout.spacing]
L:   1 | P:   1 | LT02 | First line should not be indented.
                        | [layout.indent]
L:   1 | P:   1 | LT13 | Files must not begin with newlines or whitespace.
                        | [layout.start_of_file]

(以降のLT01、LT12の行は省略しています。)1行が「行番号 | 桁 | ルールコード | 内容 | ルール名」の並びです。LTはlayoutグループのコードで、ルール名のlayout.spacingのようなドット表記でも指定できます。この形式なら、Claudeに返す指摘としてもそのまま読めます。

lint・fix・formatの違い

SQLFluffのCLIには、hookで使い分けられる3つのコマンドがあります。

コマンド働きhookでの位置づけ
lint働き指摘を出すだけで、ファイルは書き換えないhookでの位置づけ修正後の再確認、CIでの検査
fix働き直せる違反をファイルに書き込むhookでの位置づけ編集直後の自動修正
format働き安定したルールの一部に絞ってfixを強制適用するhookでの位置づけ整形だけでよいときの軽い選択肢

formatは有効にしたルールの指定を無視し、除外指定だけに従います。rulesで絞った規約検査をしたいならfixを使います。

fixは3.0以降、確認の入力待ちを挟まずにファイル単位で修正を適用します。以前の-f(--force)オプションは非推奨で、付けなくても同じ動きです。編集直後に非対話で走らせるhookに向いています。

.sqlfluffでチームの規約を1か所にまとめる

hookが守らせる中身は.sqlfluffに書きます。新規プロジェクトの設定ファイルは最小に保ち、チームの判断を記録する役目を持たせる、というのが推奨です。dbtプロジェクトの出発点は次のような形です。

[sqlfluff]
dialect = snowflake
templater = dbt
encoding = utf-8
exclude_rules = ambiguous.column_count, structure.column_order
max_line_length = 120
processes = -1
 
[sqlfluff:templater:dbt]
project_dir = ./
 
[sqlfluff:templater:jinja]
apply_dbt_builtins = True
 
[sqlfluff:rules:capitalisation.keywords]
capitalisation_policy = lower

dialectは使っているデータウェアハウスに合わせて変えます。選べる方言はsqlfluff dialectsで確認できます。exclude_rulesの2つは、新規プロジェクト例が「議論の分かれやすいルール」として挙げているものです。外すかどうかはチームで決めます。

大文字小文字のルールは、既定がconsistentで、ファイルの残りから自動判定します。新規プロジェクトではlowerやupperに固定した方が、Claudeが書いたファイルの揺れを拾えます。

encoding = utf-8は、dbt templaterを使うときの推奨設定です。sqlfluff fixが書いたファイルをdbtがあとで読むときに、文字コードの自動判定に頼らずに済み、UnicodeDecodeErrorを避けられます。dbt_packages/やtarget/はlintの対象から外します。

cat > .sqlfluffignore <<'EOF'
target/
dbt_packages/
macros/
EOF

dbt_modules/は1.0.0より前のdbt向けです。1.0.0以降はdbt_packages/を指定します。

hookの設定とスクリプト

PostToolUseは、Claudeがツールを成功させた直後に動くイベントです。Edit|Writeにマッチさせ、スクリプトに編集したファイルのパスを渡します。.claude/settings.jsonは次のとおりです。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/sqlfluff-fix.sh",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

${CLAUDE_PROJECT_DIR}はセッションを始めたプロジェクトのルートを指します。スクリプトはstdinのJSONからtool_input.file_pathを取り出し、.sql以外なら何もせず終わります。

mkdir -p .claude/hooks
cat > .claude/hooks/sqlfluff-fix.sh <<'SH'
#!/bin/bash
file_path=$(jq -r '.tool_input.file_path // empty')
case "$file_path" in
  *.sql) ;;
  *) exit 0 ;;
esac
[ -f "$file_path" ] || exit 0
 
output=$(sqlfluff fix --templater jinja \
  --show-lint-violations "$file_path" 2>&1)
status=$?
 
if [ "$status" -ne 0 ]; then
  echo "$output" >&2
  exit 2
fi
exit 0
SH
chmod +x .claude/hooks/sqlfluff-fix.sh

--show-lint-violationsはfixのオプションで、直せなかった違反だけを一覧にします。--templater jinjaは、.sqlfluffにtemplater = dbtと書いてあっても、このhookの実行だけJinjaに切り替えます。理由は次の節で説明します。jqが無いとパスを取り出せず、hookは何もせずに終わります。

dbtプロジェクトではtemplaterをどう選ぶか

dbtのSQLは{{ ref('stg_orders') }}のようなJinjaを含むため、SQLFluffは先にテンプレートを展開してから検査します。展開を担うのがtemplaterで、公式は使い分けの目安を次のように示しています。

使う場面向くtemplater理由
CI/CD(速度より展開の正確さ)向くtemplaterdbt理由ほぼすべてのマクロが動く
IDEやgit hook(応答の速さ)向くtemplaterjinja理由dbtの複雑さを持ち込まず、速い

dbt templaterは、コンパイル時にデータベースへ問い合わせるモデルがあると、検査にもデータベース接続が要ります。接続先は本番でなくテスト用で構いません。実行も遅くなります。1ファイルを編集するたびに走るhookには重い構成です。

そこで、hookではJinja templaterを使い、ref・source・var・is_incremental()といったdbtの組み込み関数はapply_dbt_builtins = Trueで補います。この設定が前掲の.sqlfluffにある[sqlfluff:templater:jinja]です。dbt templaterを使う検査は、CIのsqlfluff lintで別に回します。

dbt templaterを使う側の準備は次のとおりです。アダプターの選択は方言とそろえます。

pip install dbt-snowflake sqlfluff-templater-dbt

templaterの設定は、実行ディレクトリの.sqlfluffにだけ書けます。サブディレクトリの.sqlfluffでは変更できません。hook側の切り替えにCLIの--templaterを使うのは、この制約を避ける意味もあります。

既存プロジェクトでは規約を絞って始める

すでにあるモデルにいきなり全ルールをかけると、Claudeが触っていない行まで指摘が並びます。rulesとexclude_rulesで対象を絞れます。どちらもカンマ区切りで、ルールコード(LT01)、ルール名(layout.indent)、グループ名(layout、capitalisation)を混ぜて書けます。rulesが空なら全ルールが対象で、exclude_rulesはその後に差し引かれます。

注意点は継承です。子ディレクトリの.sqlfluffでrulesかexclude_rulesを書くと、親の値は合成されず丸ごと置き換わります。推奨されるのは、プロジェクトのルートでrulesを1回だけ決め、サブディレクトリではexclude_rulesだけを使う運用です。hookを追加する前に、この方針で設定を整理しておくと、Claudeが編集する場所によって規約が変わる事態を避けられます。

特定のファイルだけ例外にしたいときは、ファイル冒頭に-- sqlfluff:indentation:tab_space_size:2のようなコメントを置く方法もあります。ファイル全体にかかる設定なので、1ファイルで完結する例外に限るのが前提です。

直らなかった指摘をClaudeに返す

PostToolUseのhookは、終了コードが0なら標準エラー出力をClaudeに見せません。終了コード2にすると、ツールはすでに実行済みですが、標準エラーの内容がClaudeに届きます。前掲のスクリプトは、sqlfluff fixが0以外で終わったときだけexit 2にしています。

sqlfluff fixは、直せない違反(unfixable)が残ると0以外で終わります。空白のような機械的に直せる違反はfixが書き換え、書き換えでは解けない違反だけが残って、Claudeに返ります。

Claudeが指摘を消すために-- noqaを書き足すことがあります。-- noqaは全ルール、-- noqa: CP02,CP03のようにコードを付けると特定ルールだけを無視する書き方です。便利な一方で、規約を回避する手段にもなります。次のCLAUDE.mdの断片で、使い方に歯止めをかけます。

## SQL
- 規約の正は `.sqlfluff`。モデルを書いたら hook の出力を読み、指摘を直す
- `-- noqa` で指摘を消さない。除外したいルールは理由を添えて提案する
- `target/` と `dbt_packages/` のSQLは編集しない

書き換えの範囲にも注意します。sqlfluff fixは、テンプレートの展開エラーや構文エラーがあるファイルでは、既定で何も直しません。noqaや--ignoreで表示を消していても同じです。--FIX-EVEN-UNPARSABLEで直せますが、公式は、SQLを壊す可能性があるため、適用後に差分を確認するよう警告しています。hookには付けない構成が無難です。

コミット時にもう一段かける

ClaudeがBashのリダイレクトなどでSQLを書き換えた場合、PostToolUseのEdit|Writeには届きません。この抜けは、PostToolUse hookのLint自動修正の早見表でも扱っています。コミット直前の検査を足すと、取りこぼしを拾えます。

SQLFluffは公式にpre-commitのフックを2つ用意しています。sqlfluff-lintは指摘を返し、sqlfluff-fixは違反を直します。dbtの検査をかけたいときは、additional_dependenciesにアダプターとsqlfluff-templater-dbtを足します。

repos:
  - repo: https://github.com/sqlfluff/sqlfluff
    rev: <使うバージョンのタグ>
    hooks:
      - id: sqlfluff-lint
        # additional_dependencies: ['dbt-snowflake', 'sqlfluff-templater-dbt']

pre-commitは変更したファイルだけをsqlfluff lint a.sql b.sqlの形で渡すため、全体を走査せずに済みます。.sqlfluffignoreと併用するときは、pre-commit側にもexcludeを設けると、無視設定を上書きした旨の警告が出なくなります。

dbtモデルそのものの実行やリネージはhookの役目ではなく、dbt MCPサーバーの使い方で扱うMCP経由の操作が受け持ちます。

よくあるつまずき

  • hookが動いた形跡がない: ${CLAUDE_PROJECT_DIR}/.claude/hooks/sqlfluff-fix.shに実行権限があるか、jqとsqlfluffがhookを起動するシェルのPATHにあるかを確認します。
  • dbtのrefでテンプレートエラーになる: hookで--templater jinjaを付けているか、apply_dbt_builtins = Trueが.sqlfluffにあるかを見ます。
  • マクロのディレクトリが大量に指摘される: macros/は.sqlfluffignoreに入れます。dbt向けの設定例にも含まれています。
  • dbt templaterの検査だけ落ちる: コンパイル時にテーブルを参照するマクロが原因のことがあります。dbt_skip_compilation_errorは既定でTrueのため、エラーが隠れています。原因を見たいときはFalseにします。
  • 実行が遅い: processes = -1はCPUコア数から1を引いた数のプロセスで並列に検査する設定です。複数ファイルをまとめて検査するCIで効きます。1ファイルずつ走るhookでは、遅さを左右するのはtemplaterの選択です。

まとめ

SQLの整形規約は、.sqlfluffに1か所、hookに1か所だけ書けば、Claudeの編集のたびに自動で守られます。hookはJinja templaterで速く回し、dbt templaterによる厳密な検査はCIとpre-commitに任せる分担が、templaterの使い分けの目安にも沿っています。直せない指摘は終了コード2でClaudeに返し、-- noqaによる回避は、CLAUDE.mdで一言歯止めをかけます。

この記事を共有:XはてブLinkedIn