JavaScriptでSOQLパーサを作る

## はじめに

この記事では、Salesforceのオブジェクト内のレコードデータをクエリするための言語である SOQL（Salesforce Object Query Language） を構文解析し、構造体としての表現（抽象構文木）を取得するための方法とその実装手順について記します。

## なぜSOQLの構文解析が必要になるのか

SOQLではSQLに似た記述形式でSalesforceのデータクエリを記述できますが、一旦文字列表現として確定してしまうとその内容を操作しにくいといった問題があります [^1]。また妥当性の検証や事前のセキュリティチェックをクエリを実行する前に行う必要がある場合にも、データクエリをSOQL文字列のままで扱うことは厳しいものがあります。

[^1]: 文字列結合によるSOQL生成は、カジュアルに利用されることが多いですが、堅牢なアプリケーションにおいてはアンチパターンです。

このため、多くのアプリケーションでは、SOQLを直接記述するのではなく、一旦中間表現として何らかの構造体でクエリ情報を構成した後、最終的なクエリ実行の段階でSOQLとして文字列にシリアライズするといった方針を取っているものも多くあります。

しかしながら、インテグレーション上の制約や、開発者向けのDXといった観点で、生の文字列のSOQLが入力として必要とされることもしばしばあります。このような場合に、アプリケーションが構造体としてクエリ情報を操作できるようにするために、入力として与えられたSOQLを構文解析することが求められてきます。

## 実装方針

SOQLでもなんでもかまわないのですが、特定の文法に従ったテキストを構文解析しようとする場合にはパーサジェネレータ(Parser Generator)を使うのが楽です。パーサジェネレータは何をしてくれるかというと、文法を定義したファイルを入力すると、その文法に従って文字列を解析し何らかの構造体として出力してくれるパーサプログラム自体を生成してくれます。

今回はJavaScriptで動作することを目的としているので、パーサジェネレータ自体もJavaScriptで実装されているものを使うほうがよいでしょう。Pure JavaScriptでのパーサジェネレータ実装はいろいろありますが、今回は[PEG.js](https://pegjs.org/)を利用します。

## ステップ1: シンプルに文法を定義する

SOQLの文法、といっても、「文法」が一体何を意味しているのかよくわからないかもしれません。ここでは『文字列の要素がどういう順番で並べられるかというルールを計算機が理解可能なように記したもの』とざっくりと理解しておきます。PEG.jsではその名の通り[PEG(Parsing Expression Grammar)](https://ja.wikipedia.org/wiki/Parsing_Expression_Grammar)という記述形式を用いて文法を定義します。

通常、オープンな仕様の言語などでは何らかの形で文法についてもオープンにされていることが多いのですが、SOQLについてはリファレンスマニュアルはあるものの、計算機が理解可能なちゃんとした仕様レベルでの文法は公開されていません。[^2] なのでこちらでリファレンス仕様から文法を定義してあげる必要があります。

[^2]: [大昔にBNFが提供されていたこともあります](https://resources.docs.salesforce.com/sfdc/pdf/apex_api.pdf?major=146)が、Spring'07の時点でのSOQLでリレーションクエリも対応してない時の話なので、正直あまり役に立ちません

文法を作るなんて初めてだ、という方でもある程度わかりやすいように、シンプルな例から始めましょう。オブジェクトから幾つかの項目を選択しすべてのレコードを取得するSOQL、`SELECT Field1, Field2 FROM Object1`のようなSOQLのみを解析可能な文法を作成してみます。PEG.jsでは以下のような形で記述できます。

```js:step-1.pegjs
Query =
  SelectClause __ FromClause
  
SelectClause =
  "SELECT"i __ FieldList

FromClause =
  "FROM"i __ ObjectName

FieldList =
  Field _ "," _ FieldList
/ Field

Field = Identifier

ObjectName = Identifier

Identifier = [a-zA-Z][0-9a-zA-Z_]+

__ "whitespaces"
  = [ \t\n\r]+
 
_ "spacer"
  = [ \t\n\r]*
```

この文法を上から順番に見ていきます。

まず最初のルールである`Query`要素は`SelectClause`と`FromClause`を whitespaces(`__`で表記。必ず空白文字を１つ以上含む)で区切られているものとして定義されているのがわかります。これを満たしていることが（今回の）SOQLの構文としての条件となります。

では`SelectClause`および`FromClause`はどのようなものでしょうか。まず`SelectClause`は文字列`SELECT`(大文字小文字区別なし)ではじまり、whitespacesで区切られた後、`FieldList`要素が含まれています。同じように`FromClause`は文字列`FROM`(大文字小文字区別なし)ではじまり、whitespaces、`ObjectName`要素が続いているのがわかります。

さらに`SelectClause`から参照されている`FieldList`はというと、まず文法規則が`/`で区切られているのがわかります。PEGでは、この区切られたルールに対して、まず最初のルールにマッチするかどうかチェックし、マッチしない場合は次のルール、というように解析されていきます。そのため、この要素は、以下のような形で解析されます
 1. `Field`要素で始まり、spacer(`_`で表記。空文字列も含む空白文字列)の後に","(カンマ)文字列、spacer、そして自分自身である`FieldList`が続いていること
 2. (1.が満たされない場合) `Field`要素と同等であること

`Field`および`ObjectName`要素は、`Identifier`要素と同じであり、これは[0-9a-zA-Z_]の文字からなる文字列になります。

## ステップ2: 実際に構文解析（パース）してみる

本来ならパーサジェネレータであるPEG.jsに上記文法を入力としてパーサを生成させるのが正しい手順ですが、実はPEG.jsではオンライン環境でパーサ生成／実行までできてしまいます（これがJavaScript実装のパーサジェネレータの強みですね）。

[https://pegjs.org/online](https://pegjs.org/online)

そのため、めんどうな手順はすっ飛ばして、文法定義を上のサイトの"Write your PEG.js grammar"内にコピー＆ペーストして実行してみます。"Test the generated parser with some input"には何らかのSOQL文を入力してみて下さい。ただし現時点では `SELECT 〜 FROM 〜`のシンプルな形のみで、それ以外はエラーになるのがわかります。

<img width="585" alt="Online_version_»_PEG_js_–_Parser_Generator_for_JavaScript.png" src="https://qiita-image-store.s3.amazonaws.com/0/6495/6a5e85db-0ae7-3155-2957-cef0605b4215.png">

## ステップ3: 解析結果出力の構造を変更する

上記のステップで、文法上妥当なSOQLについて正常にパースが実行できることは確認できましたが、出力として得られた情報はなんだかよくわからない入れ子の配列になっていました。構造化されているとは言えこのままでは少々アプリケーションが直接扱うには荷が重いため、生成する情報をカスタマイズしてみます。

ここでは、解析後の情報が `{ fields: ["Field1","Field2"], object: "Object1" }`のような構造体になるように調整します。

```js:step-3.pegjs
Query =
  fields:SelectClause __ object:FromClause { return { fields:fields, object:object } }
  
SelectClause =
  "SELECT"i __ fields:FieldList { return fields }

FromClause =
  "FROM"i __ object:ObjectName { return object }

FieldList =
  field:Field _ "," _ fields:FieldList { return [field].concat(fields) }
/ field:Field { return [field] }

Field = Identifier

ObjectName = Identifier

Identifier = [a-zA-Z][0-9a-zA-Z_]+ { return text() }

__ "whitespaces"
  = [ \t\n\r]+
 
_ "spacer"
  = [ \t\n\r]*
```

上記では、各ルールの末尾に中括弧("{","}")で囲まれた表記が追加されていますが、PEG.jsではこれをactionといって、解析後の結果をどのような構造で返却するかをJavaScriptの関数で定義できるようになっています。

さらにルールから参照されている要素の先頭には `fields:SelectClause`のように先頭にラベルが振られているものもありますが、actionの中からはこのラベル名を通じて要素の解析結果を変数として参照することができます。

変更した文法定義を同じようにPEG.js Onlineに貼り付けて実行してみましょう。解析後の出力結果が意図されたとおりになっているのがわかります。

<img width="585" alt="Online_version_»_PEG_js_–_Parser_Generator_for_JavaScript.png" src="https://qiita-image-store.s3.amazonaws.com/0/6495/1038ac48-2b24-0217-5836-1638d7f291ae.png">

生成したパーサプログラムは、サイト上からそのままJavaScriptファイルとしてダウンロードできます。ブラウザJavaScriptでもNode.jsでも、関数呼び出しで簡単に利用が可能です

```js
// ダウンロードしたJavaScriptファイルをparser.jsとして保存
const parser = require('./parser.js');

const parsed = parser.parse('SELECT Id, Name FROM Account');
console.log(parsed);
// => { fields: [ 'Id', 'Name' ], object: 'Account' }
```

## ステップ4: 文法ルールの追加

あとはもう少し複雑なクエリ表現でも解析可能なように、文法にルールを追加していきます。

色々説明も面倒なので、[こちら](https://github.com/stomita/soql-parse/blob/master/src/parser.pegjs)に文法定義を置いておきます。

## SOQLパーサの実装ライブラリ

上記のパーサを実装したライブラリを[すでに公開しています](https://github.com/stomita/soql-parse)。対応するSOQLはまだ限られていますが、通常の用途であればほぼ問題なく構文解析できるようになっています。参考までに現在サポートされているSOQLの構文は以下のものです。

- WHERE句
- ORDER BY句
- LIMIT/OFFSET句
- GROUP BY句
- 親/子リレーションクエリ
- 関数(e.g. `toLabel()`/`convertCurrency()`など)
- FROM句のオブジェクト別名表記
- USING SCOPE句
- GROUP BY ROLLUP / GROUP BY CUBE句

TYPEOF句やWITH句やその他のマイナーな構文もちゃんと対応したいところですが、まあ実用上こんなものでしょうか。

## 構文解析結果の活用方法

SOQLが構文解析できるようになったことで、以下のような活用方法が考えられます。

### SOQLの妥当性の検証

今まで、あるSOQLが本当に妥当なものかどうかは、実際にAPIをコールしてクエリを実行するまでわからないのが実情でした。SOQLをオフラインで構文解析するおくことで、少なくとも構文上の間違いを持っているSOQLを検出することが可能になります。

ただし、与えられたSOQLが妥当であるかどうかは、構文解析だけではまだ実はわかりません。構文解析に成功したとしても、項目やオブジェクトなどの情報は環境毎に異なるので、そのSOQLが妥当かどうかは、実際に存在する項目かどうか、そしてその項目のデータ型が何か、に依存しているからです。

これらの妥当性の検査は、実際に接続する環境のオブジェクトや項目などのメタデータ情報を取得した後、解析結果のAST(=Abstract Syntax Tree; 抽象構文木）のデータ構造を辿りながらチェックすることで可能になります。

また、GROUP BY句などには構文解析中には指定できない制約（例： SELECTに指定した非集計関数の項目は必ずGROUP BYにも含まれること）もあります。そのような場合も解析後のASTを元にチェックを行うことが可能です。

### SOQLの補完

もっともパワーを感じさせられるのは補完かもしれません。SQLなどでテーブル名や列名をダイナミックに補完してくれるフロントエンドを使ったことのある人もいるかもしれませんが、それが同様にSOQLでも可能になります。組織内のオブジェクトや関連する項目のリストは簡単にAPI経由で取得できるので、あとはそれを適切にユーザにフィードバックしてやればいいはずです。

補完については、入力中の不完全なSOQL文を相手にすることになるので、実際には構文解析エラーが発生しますが、PEG.jsの場合はSOQL文中のどの位置の文字列が想定されているものと異なっていたか、エラーオブジェクトの中に通知されてきますので、それを利用して不完全なSOQLを構文上妥当な形に直すことができます。あとは構文解析された情報を利用して、現在のカーソル位置の要素を特定し、Salesforceメタデータ情報から適切な補完候補を引っ張ってきてリスト表示する、といった戦略を取ることができます。

なお、SOQLの補完について実装したアプリケーションとして [SOQL Console](https://soql-console.herokuapp.com/)があります。こちらはWebアプリケーションフロントエンドとして、SOQLの補完組み立てからSalesforceに対するクエリ実行までできるアプリケーションです。入力されたSOQLを解析し、ログイン済みのSalesforce組織からオブジェクトの項目情報などのメタデータを動的に取得して、補完候補を画面に表示します。

ただし、こちらのアプリケーションはかなり昔のコード(実装もCoffeeScript)となっており、かつ今回説明したPEG.jsではなく[jison](http://zaa.ch/jison/)というパーサジェネレータ実装を使っています。文法についてもあまりサポートしていません。今後書き直されることもあるかもしれませんが、あまり期待しないでおいて下さい。

https://github.com/stomita/soql-console

### SOQLの変換

ここでは触れていませんでしたが、データ構造としてASTを作ってあげれば逆にSOQLへのシリアライズも可能であるということです。
解析して得られたASTに対して適切なツリー変換を施してあげることで、ダイナミックに条件を挿入したり、合成したりと言ったことが比較的簡単になります。

またSOQLの実行に制約があるようなケース(例：多態関連の項目値による絞り込みなど) についても、ASTから２つのクエリに分解してアプリケーション上で結果を結合する、といった荒業も可能になるかもしれません。


## まとめ

構文解析って一見大変そうに見えますが、最近は文法さえ定義してしまえばいいので、お気楽でいいですね。SOQLにかぎらずいろいろトライしてみてはいかがでしょうか。