SOQLのクエリをER図として可視化するツールを作ってみた
はじめに
私は現在、Salesforceで使われているSOQLというクエリ言語のオープンソースとしての再実装に取り組んでいます。
以前はTypeScriptで実装を行いましたが、直近ではGoによりパーサーを書き直し、実行エンジンの再作成をしています。
Goによるクエリパーサーが主要なクエリへの対応を完了したので、クエリをER図として可視化するツールを作ってみました。
ツールで使用したもの
フロントエンドはVanilla JavaScriptで作っています。
ER図のレンダリングにはMermaidを使用しました。本当はPlantUMLの方が好きなのですが、クライアントだけで何とかしたいのでMermaidを選びました。
SOQLのパーサーははじめに記載した通り、自作のものを使用しています。
パーサーはパーサーコンビネーターとして構築されており、パーサーライブラリも自作のものを使用しています。 (Tinyではない方の)Goでwasmにビルドしています。バイナリサイズは3.1MB(gzip圧縮後863KB)です。大きいですが、こういう目的ならば、まあいいかと思います。SOQLパーサーから得られる情報
SOQLパーサーは字句解析、構文解析により各句の情報を構造体に入れた後、後処理として主に以下のことを行っています。
- オブジェクト名、フィールド名への名前空間の付与とエイリアス名の解決 (オブジェクトグラフの完全修飾名にします)
- オブジェクト毎のクエリ構築 (項目の抽出とオブジェクト内で完結する検索条件の決定)
- 結合方法の決定 (1:0-1の結合の右側に検索条件がある場合は内部結合にします)
- オブジェクトグラフ、クエリグラフの作成 (クエリのネストに基づく親子関係を出力します)
以下の構造体に情報は格納されます。
type SoqlViewGraphLeaf struct {
Name string `json:"name"` // Name
ParentViewId int `json:"parentViewId"` // View id of parent object on object graph
QueryId int `json:"queryId"` // Query unique id
Depth int `json:"depth"` // Depth on object graph
QueryDepth int `json:"queryDepth"` // Query depth
Many bool `json:"many,omitempty"` // True if it is one-to-many relationship (subquery)
InnerJoin bool `json:"innerJoin,omitempty"` // Inner join to parent view id
NonResult bool `json:"nonResult,omitempty"` // True if it is subquery on conditions (where | having clause)
Object *SoqlObjectInfo `json:"-"` // Object
Query *SoqlQuery `json:"-"` // Query
}
type SoqlQueryGraphLeaf struct {
ParentQueryId int `json:"parentQueryId"` // Query id of parent query
Depth int `json:"depth"` // Depth on query graph
IsConditional bool `json:"isConditional"` // Query is part of filter (where/having) condition or not
Query *SoqlQuery `json:"-"` // Query
}
type SoqlQueryMeta struct {
Version string `json:"version,omitempty"` // format version
Date time.Time `json:"date,omitempty"` // compiled datetime
ElapsedTime time.Duration `json:"elapsedTime,omitempty"` // time taken to compile
Source string `json:"source,omitempty"` // source
MaxQueryDepth int `json:"maxQueryDepth,omitempty"` // max depth of query graph
MaxViewDepth int `json:"maxViewDepth,omitempty"` // max depth of object graph
NextQueryId int `json:"nextQueryId,omitempty"` // next query id (a number of queries)
NextViewId int `json:"nextViewId,omitempty"` // next view id (a number of views)
NextColumnId int `json:"nextColumnId,omitempty"` // next column id (a number of columns)
QueryGraph map[int]SoqlQueryGraphLeaf `json:"queryGraph,omitempty"` // query graph (child -> parent)
ViewGraph map[int]SoqlViewGraphLeaf `json:"viewGraph,omitempty"` // object graph (child -> parent)
Functions map[string]struct{} `json:"functions,omitempty"` // functions
Parameters map[string]struct{} `json:"parameters,omitempty"` // parameters
DateTimeLiterals map[string]struct{} `json:"dateTimeLiterals,omitempty"` // datetime literals
}
type SoqlQuery struct {
Fields []SoqlFieldInfo `json:"fields,omitempty"` // Select clause fields; possibly null
From []SoqlObjectInfo `json:"from,omitempty"` // From clause objects; has at least one element
Where []SoqlCondition `json:"where,omitempty"` // Where clause conditions; possibly null; Not used in the execution planning phase.
GroupBy []SoqlFieldInfo `json:"groupBy,omitempty"` // Group by clause fields; possibly null; Not used for "PerObjectQuery"
Having []SoqlCondition `json:"having,omitempty"` // Having clause conditions; possibly null; Not used for "PerObjectQuery"
OrderBy []SoqlOrderByInfo `json:"orderBy,omitempty"` // Order by clause fields; possibly null
OffsetAndLimit SoqlOffsetAndLimitClause `json:"offsetAndLimit,omitempty"` // Offset and limit clause
For SoqlForClause `json:"for,omitempty"` // For clause
Parent *SoqlQuery `json:"-"` // Pointer to parent query; Not used for "PerObjectQuery"
IsAggregation bool `json:"isAggregation,omitempty"` // It is an aggregation result or not; Not used for "PerObjectQuery"
IsCorelated bool `json:"isCorelated,omitempty"` // Co-related query if true
PostProcessWhere []SoqlCondition `json:"postProcessWhere,omitempty"` // Post-processing conditions (Conditions to apply after being filtered in the query for each object)
QueryId int `json:"queryId,omitempty"` // Query unique id
Meta *SoqlQueryMeta `json:"meta,omitempty"` // Meta information
}
ER図の描画
ER図への描画は、前章の構造体をシリアライズすると失われる情報(キャッシュとして持っている各句の構造体へのポインタ)があるので、Go側でクエリをパースした後、引き続いてMermaidの定義を文字列として出力しています。
Where句・Having句内のサブクエリの結合先オブジェクトについては、現在検討している実行計画では直接知る必要が無い[1]のでパース結果に持っていません。可視化ライブラリ内で探すようにしました。
さいごに
今年は引き続きSOQL再実装プロジェクトを進めていきたいと考えています。
もし関心をお持ちいただけるなら、スターやフィードバックをいただけると幸いです。
-
条件句内のサブクエリは現在のクエリにおいて結果セットとなるクエリに先んじて行われる、また、クエリ結果はスタックマシンで評価されるので結合先を求める必要が無い。 ↩︎
Discussion