Claude Codeにast-grepを使ってSQLの構造見ながら検索してもらう
コーディングエージェントにSQLをいい感じに検索させたい!
Serena(MCPでコーディングエージェントに、セマンティック検索などの機能を提供するツール)ではSQLがサポートされておらず、ぐぬぬと思っています。
しかし先日ast-grepというツールを見つけました。作者の方の記事によると、構造検索という抽象構文木(AST)へのパターンマッチで検索ができるようです。
Claude Codeにast-grepで構造検索したら「SQLでもSerenaっぽい、いい感じの検索ができるのでは?」と思ったので、やります。
解決したいこと
- 「クエリ中に存在する全テーブルを抽出」など、特定の目的にフォーカスした検索をClaude Codeがバシッと決定論的にやれるようにしたい
課題
-
ast-grepは、SQLに標準対応していない
- でも、ast-grepは任意のtree-sitter製パーサを読み込める
- ast-grepにSQLを検索してもらうには、既存のtree-sitter製SQLパーサを借りてくるのが手っ取り早い
今回は手っ取り早く感触を試してみたいので、既存のSQLパーサを使ってast-grepで検索してみます。
セットアップ
既存のSQLパーサを使ってast-grepで検索してみるために、環境を構築してみます。
- ast-grepのインストール
- tree-sitter製のSQLパーサをビルドする
- ビルドしたSQLパーサを、ast-grepにCustom Languageとして読み込ませる
- ast-grepの使い方を、Claude CodeにSkillsとして仕込む
ast-grepのインストール
まずは、ast-grepをインストールします。
Mac環境なので下記コマンドでインストールしました。
brew install ast-grep
$ ast-grep -V
ast-grep 0.40.0
tree-sitter製のSQLパーサをビルドする
ast-grepで標準対応していない言語でも、tree-sitterのパーサがあれば対応できます。
公式ドキュメントの手順に従って、カスタム言語対応をやっていきます。
tree-sitterで実装されたSQLパーサであるtree-sitter-sqlを、ast-grepに読み込ませてみます。
まずはビルドして共有ライブラリ(.so)をつくります。
公式ドキュメントには、tree-sitter cliからもビルドできるっぽいことが書いてあるんですが、なぜか私の環境だとうまく行かなかったので、とりあえずgccでビルドしてみます。
npm install tree-sitter-cli
git clone https://github.com/DerekStride/tree-sitter-sql.git
cd tree-sitter-sql
git checkout gh-pages
gcc -shared -fPIC -fno-exceptions -g -I 'src' -o sql.so -O2 src/scanner.c -xc src/parser.c -lstdc++
※mainではなく、gh-pagesにブランチを切り替える必要がある
sql.so という名前で、tree-sitter-sqlの共有ライブラリがビルドできました。
ast-grepにSQLパーサを読み込ませる
次は、sql.soを使ってast-grepが検索できるように、プロジェクトのルートディレクトリにsgconfig.yml を置いてみます。
# sgconfig.yml
ruleDirs: ["./rules"]
customLanguages:
sql:
libraryPath: sql.so # さっきビルドしたsoファイルのパス
extensions: [sql] # SQLとみなす拡張子
sql.so のパスは適宜修正してください。
ast-grepの使い方をClaude CodeにSkillsとして仕込む
ast-grepは公式でClaude Code向けの構成方法や、Skillsを公開しています。
さっそくこれを借りて使ってみます。Claude Code上でマーケットプレイスからSkillsをインストールしてみます。
/plugin marketplace add ast-grep/claude-skill
/plugin install ast-grep
ただ、ast-grepでSQLを検索できるということを、Claude Codeが必ずしも理解してくれているわけじゃないので、「SQLにも対応しているよ」というようなことをCLAUDE.mdに仕込んでおきます。
CLAUDE.md
You are operating in an environment where ast-grep is installed. For any code search that requires understanding of syntax or code structure, you should default to using ast-grep --lang [language] -p '<pattern>'. Adjust the --lang flag as needed for the specific programming language(ex: sql,yaml,python). Avoid using text-only search tools unless a plain-text search is explicitly requested.
ast-grep's configuration file (sgconfig.yml) has been modified to include a custom language definition for SQL. When performing code searches that involve SQL syntax or structure, ensure to use the --lang sql flag with ast-grep commands.
検索条件を生成するための準備
ast-grepが検索するときに、ymlでASTの検索条件を記載したrulesを作ると検索条件を再利用しやすくなるし、仔細な条件を設定できます。
なので、rulesをClaude Codeに作らせてみようと思います。
ただし前提条件として、「SQLをtree-sitter-sqlでパースしたときにどんなASTが生成されるのか」を知る必要があります。
Claude Codeでも人力でも、ASTの語彙がわからない中で、rulesを作るのは不可能に近いでしょう。
この知識はSonnetやOpusのベース知識の中に含まれているとはあまり思えません。
なので、とりあえずtree-sitter-sqlをPythonでインストールして、Claude Codeにサンドボックスで遊ばせました。
しばらく遊ばせた後、ざっくりとリファレンス用のmarkdownを生成させました。
これを適当に、SQL_AST_QUICK_REF.mdという名前で置いといて、CLAUDE.mdの中に参照しといてね、という感じで追記しておきます。
CLAUDE.md
You must read `SQL_AST_QUICK_REF.md` to understand the SQL AST structure before performing any SQL-related code searches.
これでパーサが解釈した語彙と、実テキストのマッピングが概ねできるので、検索条件の生成ができそうですね。
検索させてみよう
実際に検索してみます。
検索条件:FROMとJOINで参照しているテーブルを探す
FROM句やJOIN句はきっと構文として解析できているはずなので、それらが参照しているテーブル名を検索させてみます。
Claude Codeにrulesを作らせてみて、こんな感じのができました。
id: extract-table-references
language: sql
severity: info
message: "テーブル参照: $TABLE"
rule:
kind: relation
any:
- inside:
kind: from
stopBy: neighbor
- inside:
kind: join
stopBy: neighbor
has:
kind: object_reference
stopBy: neighbor
has:
kind: identifier
pattern: $TABLE
やってみると、確かにちゃんと検索できていそうです。
note[extract-table-references]: テーブル参照: Customer
┌─ example/badquery.sql:16:22
│
16 │ ╭ FROM Customer c4
note[extract-table-references]: テーブル参照: InvoiceLine
┌─ example/badquery.sql:45:10
│
45 │ JOIN InvoiceLine ON i9.InvoiceId = InvoiceLine.InvoiceId
│ -----^^^^^^^^^^^----------------------------------------
例えば、
- aliasを網羅的に検出して(決定論的にast-grepで検索)
- 命名ルールから逸脱した名称のものがないかチェックする(非決定論的にAIがチェック)
みたいなタスクもできそうですね。
検索条件:テーブルの参照時に、aliasが指定されていないテーブルを探す
正規表現だとやり方がかなり難しくなるであろう「◯◯がない」 系の検索を試してみました。ast-grepだとできそうです。
試しに、aliasが指定されていないテーブルを探してみます。
Claude Codeにrulesを作らせてみて、こんな感じのが作れました。
id: relation-without-alias
language: sql
severity: info
message: テーブル参照にエイリアスが指定されていません
rule:
kind: relation
all:
# relationの直接の子としてobject_referenceを持つ (必須)
- has:
kind: object_reference
stopBy: neighbor
# relationの直接の子としてidentifierを持たない (エイリアスなし)
- not:
has:
kind: identifier
stopBy: neighbor
やってみると、確かにちゃんと検索できていそうです。
⎿ note[relation-without-alias]: テーブル参照にエイリアスが指定されていません
┌─ example/badquery.sql:45:10
│
45 │ JOIN InvoiceLine ON i9.InvoiceId = InvoiceLine.InvoiceId
│ ^^^^^^^^^^^
note[relation-without-alias]: テーブル参照にエイリアスが指定されていません
┌─ example/badquery.sql:47:61
│
47 │ AND InvoiceLine.UnitPrice > (SELECT AVG(UnitPrice) FROM InvoiceLine)
│ ^^^^^^^^^^^
必要な記述がないパターンを検知できるので、Linterっぽい振る舞いもやってくれそうです。
最後に
SQL(それ以外の言語も)に対するスマートな検索が、ast-grepならいい感じにできそうだな、と思います。
同じようなことができるツールには、sqlglotとかもあるのですが、こちらは方言対応がガチで色々と融通が利く反面、ガッツリPythonを組ませる必要があって、人間の目視レビューが怠かったりします。
ただ現時点では、ast-grepのrulesを、AIがイージーに作るのはやや難しいかな?という印象です。
理由は、AIのベース知識にはない知識が必要になるからです。
各言語のパーサ固有の構造・語彙の知識を使わなければrulesがうまく定義できず、パーサ固有知識を持つコンテキストを読み込ませないといけません。
でもまぁ、しっかりコンテキストを渡してあげればやや時間はかかるけどなんとか作ってくれています。
あと、rulesをあらかじめ定型的に一定量作り込んだ後にいろいろと作業させたほうが多分いいかなぁ、と思っています。
パーサ固有知識にコンテキストが一定占められちゃうので、検索したあとにやりたい本筋の仕事のノイズになり、集中しづらそうに思います。
(もしかしたらコンテキストを別で持てるサブエージェントとして作りこむとか、やり方はあるような気もする)
また、検索速度は仕事で使用しているコードベースの分量でも十分速く、ストレスになることはなさそうでした。自分でこういうツール作ると全然速度でなかったので、すごいなぁと思いました。
まとめ
- Claude Codeに、ast-grepを使ってSQLを構造検索してもらうことができそう
- SerenaとかではSQL対応していなかったので、うれしい
- rulesを作るのはちょっとむずかしめだが、あらかじめ定型的に作り込んだものを作ったら結構よさそう
- ast-grepは検索速度が速くてすごい
Discussion