AI・機械学習チームの北川です。 この記事はエムスリー Advent Calendar 2025の17日目の記事です。 16日目は須藤さんのAIに正しく分析してもらうためのテーブル設計戦略でした。

はじめに
以前、BigQuery用のLanguage Server(bqls)を自作した話を書きました。 このbqlsは、GoogleのzetasqlをベースにしたBigQuery専用のLSPで、Web UIでは遅く感じていた補完をNeovimなどのエディタで快適に使えるようにしたものです。
zetasqlの強みは、SQLをパースするだけでなく、カラムの型情報なども一緒に提供してくれる点にあります。 しかし、運用していく中で大きな課題が見えてきました。 それは、SQLにバグがあるとパースできないという点です。
LSPで補完などのリクエストが来る時、たいていそのコードは「壊れています」。 例えば補完を出したいタイミングはユーザーがコードを書いている途中です。 つまり文法的には壊れた状態であることがほとんどです。 前回のブログでは漸次的修正という手法でこの問題に対処しましたが、それでも限界がありました。
この記事では、そんな課題を解決するために導入したtree-sitterと、zetasqlとの適材適所な使い分けについて紹介します。
- はじめに
- zetasqlの限界 - 具体的な問題
- tree-sitterとの出会い
- 2つのパーサーの使い分け設計
- tree-sitter-bigqueryの自作
- 成果と今後の展望
- まとめ
- We are hiring
- 参考リンク
zetasqlの限界 - 具体的な問題
まず、zetasqlでどんな問題が起きていたのか、具体例を見てみましょう。
ケース1: 意味エラーによるパース失敗
次のようなSQLを考えてみます。
SELECT * FROM `table` GROUP BY id
このSQLは文法的には正しいため、zetasqlは問題なくパースできます。
しかし、意味的にはエラーです。GROUP BYを使う場合、SELECT *のように全カラムを取得できず、集約関数を使う必要があります。
問題は、ユーザーが次のように途中まで書いた時点で補完を出したい場合です。
SELECT * FROM `table` GROUP BY | ^ カーソル位置
この時点では、zetasqlの意味解析が引っかかってしまい、うまく補完を提供できません。 zetasqlの強み(意味解析)が、逆に制約となるケースと言えます。
ケース2: JOIN句の途中での補完
次に、JOIN句を書いている途中のケースを見てみましょう。
SELECT * FROM users u JOIN ^ カーソル位置
JOINまで入力した時点で、テーブル名の補完を出したいところです。
しかし、zetasqlはJOINの後にON句がないとエラーとして扱います。
漸次的修正を使っても、「ON句をどう補完すれば良いか」を判断するのが難しく、対応が困難でした。
tree-sitterとの出会い
転機は、2025年11月に初めて参加したVimConf 2025でした。
そこでtreesitter-lsを作成している@Atsushi776さんと話す機会がありました。 「LSP作るのめっちゃ大変ですよね」という話をしている中で、treesitter-lsではどうしているのか相談してみました。 すると、「tree-sitterはエラートレラントパーサーなので、あまりそこで詰まったことはないですよ」という話を聞きました。
確かに、tree-sitterはNeovimなどのエディタでシンタックスハイライトにも使われています。 文法間違いに対してエラーを返していたら、ハイライトが安定しません。 当時、zetasqlだけでLSPを作ることに限界を感じていた僕は、tree-sitterの導入を検討することにしました。 VimConfから関西への帰り道、新幹線の中でbqlsにtree-sitterを導入するコードを書き始めていました。
tree-sitterの魅力は次の点にあります:
- 汎用性の高いパーサー: 様々な言語に対応
- エラー耐性: 文法的に壊れている部分を
ERRORノードとして保持し、壊れていない部分は正しくパースしてくれる - 部分的な情報の活用: 完全に正しくないコードでも、パース可能な部分から情報を取り出せる
ここで1つの疑問が浮かびます。 「zetasqlを捨てて、tree-sitterに乗り換えるのか」
答えは「いや、両方使えばいい」です。
zetasqlの意味解析の強みは活かしたい。でもtree-sitterのエラー耐性も欲しい。 そこで、両者を併用することにしました。
2つのパーサーの使い分け設計
では、具体的にどう使い分けているのでしょうか。
役割分担
現在のbqlsでは、次のように役割を分担しています:
zetasql: カラム名補完など、意味解釈が必要なもの - テーブル参照の解決 - カラムの型情報取得 - より深い意味解析
tree-sitter: キーワード補完など、パースだけで大丈夫なもの - SELECT, WHERE, GROUP BYなどのキーワード補完 - 構文構造の把握 - エラー耐性が重要な場面
tree-sitterによるキーワード補完
tree-sitterを使ったキーワード補完は、次のような流れで実装しています:
- tree-sitterでSQLをパース(エラーがあっても可能な限りパース)
- カーソル位置のノードとその周辺構造を確認
- 文脈に応じて適切なキーワードを提案
例えば、FROM句の後ろであれば、WHERE, GROUP BY, ORDER BYなどを提案できます。
文法的に壊れていても、tree-sitterは部分的な構造を把握してくれるため、これが可能になります。
tree-sitter-bigqueryの自作
tree-sitterを導入するにあたり、まず既存のtree-sitter-sqlを試してみました。 tree-sitter-sqlは活発にメンテナンスされており、一般的なSQLには対応しています。
しかし、BigQueryには独特の方言があるため、tree-sitter-sqlでは対応が難しい部分がありました。 tree-sitter-sql-bigqueryというライブラリもありましたが、メンテナンスが止まっているようでした。
そこで、BigQuery専用のgrammarを自作することにしました
開発プロセス
開発は次のように進めました:
- 基礎知識: Neovimでtree-sitterを使っていたので、ある程度の知識はあった
- Claude Codeの活用: grammar.js作成にClaude Codeを使い、比較的スムーズに進められた
- 段階的な拡張: まずtree-sitter-sqlを使って1週間程度で形にし、その後BigQuery特有の文法に対応
- 機能開発と並行: bqlsで機能を作りながら、必要に応じてtree-sitter-bigqueryを拡張
完璧を目指さず、まず動くものを作ってから育てていくアプローチが功を奏しました。
成果と今後の展望
現在の成果
tree-sitterの導入により、次の改善が実現できました:
キーワード補完の精度向上
- SELECT, WHERE等のキーワード補完が、文法的に壊れた状態でも提供できるように
- zetasqlだけでは対応が難しかった「文法的に正しくない前提」の補完に対応できるようになった
堅牢性の向上
- どんな状態のSQLでも、何かしらの補完を提供できるように
今後の展望
現在は機能ごとに明確に使い分けていますが、今後は次のような拡張を考えています:
カラム名補完へのtree-sitter活用
- より複雑なクエリでもカラム名補完ができるように
抽象化層の設計
- 補完時にどちらのパーサーを使っているか意識しなくて良いように
- zetasqlパース失敗時のフォールバックとしてtree-sitterを活用
課題への対応
- PIVOT等の複雑な構文への対応は引き続き課題
まとめ
この記事では既存のパーサーがある中で、もう1つのパーサーを導入するという選択をした話について書きました。
zetasqlとtree-sitter、それぞれに強みと弱みがあります:
- zetasql: 意味解析の強さ、型情報の提供
- tree-sitter: エラー耐性の強さ、部分的なパース能力
技術選定は「どちらか1つを選ぶ」ものと考えがちですが、実際には「両方使って良いとこ取り」という選択肢もあります。 それぞれの強みを活かし、弱みを補い合う設計にすることで、より良いプロダクトが作れます。 もちろんどちらも導入することによる複雑さは発生するため、それが今後の課題になってきます。
このブログは正直ニッチ過ぎて刺さる人は少ないかもしれませんが、考え方については一般的なものです。 何かの参考になれば幸いです。
We are hiring
AI時代に置いてもプロダクト開発の中でのアーキテクチャの選択は重要です。 そのような選定を共にしてくれる仲間を募集しています。