🙆‍♀️

SQL を QueryDSL に変換するツールを作っている話

2023/06/03に公開

QueryDSL ってご存知ですか?Elasticsearch(以降 ES)にリクエストする際に使うクエリで json で記述されます。

json 形式なのでクエリによってはかなり長くなって読みたくなくなり、どのような条件のデータが出ているのかもわかりにくいです。

そこでみんな大好き SQL を QueryDSL に変換して気軽に ES のリクエストができるようにするためツールを Go で作りました。

*(23/6/3 現在 v0.1.0 を提供してますが、簡単な全検索 SQL しか変換できないので、実用的ではありません。)
https://github.com/UserKazun/qc

QueryDSL

先述した通り QueryDSL とは ES で検索するためのクエリフォーマットです。
ES は json 形式でデータが格納されますが、クエリフォーマットも json です。

例えば以下のようにリクエストします。

$ curl -XGET http://localhost:{port}/{index_name}/{type_name}/_search -d 
'{
  "query": {
    "match_all": {}
  }
}'

上記は SQL で書くと以下になります。

SELECT * FROM {table_name}

単純に全件数取得するならさほど読みにくくないですが、抽出するカラムを制限したり、以上以下などの条件を組み合わせると QueryDSL はかなり長くなります。

ここまではデメリットっぽいことを述べましたが、逆に json 形式であれば複数の条件を組み合わせた複雑な書き方もできます。

Elasticsearch SQL

そもそも SQL を QueryDSL に変換するプラグインがすでに公式から提供されています。
https://www.elastic.co/jp/what-is/elasticsearch-sql

これは Elasticsearch v6.3.0 からデフォルト化された X-Pack の中に内包されています。
*X-Pack 自体は5系あたりではプラグインとして提供されていました。ES v5.xでも X-Pack を有効にすれば Elasticsearch SQL は使えるはずです。

正直これを使えばツールを作成する必要はありません。ただし世の中の ES を使っている全てのサービスが最新の ES へのアップデートが行き渡っている訳ではないのです。

Go で作る変換器

前置きは以上として早速本題に入っていきます。今回やりたいことは SQL を QueryDSL に変換するです。

そのためまずは SQL を解析して構造体にします。その後その構造体を json に変換して標準出力するように実装します。

ここで必要になってくるのが、字句解析・構文解析です。

字句解析と構文解析

  • 字句解析

字句解析は、コンピュータを用いた自然言語処理でも、プログラミング言語のコンパイルでも行われる[1]。
自然言語の文であれ、プログラムのソースコードであれ、文というのは結局、文字や記号や約物類が多数並んだもの(文字列)であるが、字句解析はそれを、言語的に意味のある最小単位トークン(英: token(s))に分解する処理である。
文を解析してトークンに分解する作業を自動的に行うプログラムを字句解析器(英: lexical analyser)という。

https://ja.wikipedia.org/wiki/字句解析

  • 構文解析

文章(具体的にはマークアップなどの注記の入っていないベタの文字列)を対象として、
自然言語であれば、形態素に切分け、さらにその間の関連(修飾-被修飾など)といったような、統語論的関係を図式化するなどして明確化・解析する手続きである。
プログラミング言語など形式言語の場合は、形式文法に従い構文木を得る手続きである。

https://ja.wikipedia.org/wiki/構文解析

つまり実装としては以下が必要になります。

  • 意味のあるキーワードや単語を表すトークン
  • 文字列から1つのトークンになるように部分文字列を切り出すスキャナー
  • 構文解析を通して得られた構造体を変換

構文解析する前に字句解析する必要があるので、構文解析を実装するには字句解析がほぼ必須です。

SQL parser の参考実装

今回実装するにあたって以下の実装をかなり参考にさせていただきました。
https://github.com/benbjohnson/sql-parser
作者の方が解説ブログもあげているので、こちらもぜひ参考にしてみてください。
https://blog.gopheracademy.com/advent-2014/parsers-lexers/

上記の parser は副問い合わせまで parse しないのでそこは自分で書く必要があります。

トークンとスキャナーの実装

まずはトークンを用意しないと始まりません。
以下を参考にトークンを用意します。
https://cs.opensource.google/go/go/+/refs/tags/go1.20.4:src/go/token/token.go
https://github.com/benbjohnson/sql-parser/blob/master/token.go

token.go
package cli

type Token int

const (
	ILLEGAL Token = iota
	EOF
	WS

	IDENT // fields, table name

	ASTERISK  // *
	COMMA     // ,
	EQUAL     // =
	SEMICOLON // ;

	// Keywords
	SELECT
	FROM
	WHERE
)

自作のクエリ変換器は SQL の副問い合わせも変換することを目指しています。そのため WHERE= などもトークンとして用意しておきます。

次にスキャナーです。

一般に、文字列をなめるような処理をするものをスキャナという。字句解析の場合、文字列から、1個のトークンになるような部分文字列を切り出す部分をスキャナとして分けて考える場合がある。

出典:wikipedia

文字列から1個のトークンになるような部分文字列を切り出す必要があるので、1文字ずつスキャンしていきます。

go で1文字を表すには sampleVar := 'a' とシングルクウォートで囲みます。
この時点で sampleVar の型は rune となります。rune は int32 のエイリアスで実態は unicode です。
https://qiita.com/seihmd/items/4a878e7fa340d7963fee
つまりこのスキャナーでは unicode を比較していきます。

これ以外の方法で、例えば strings.Containes() で文字列の中に指定した文字が含まれるのか検査する方がわかりやすいかもしれません。
ただ上記の方法だと SELECT や WHERE などの決まった文字列の比較は良いかもしれませんが、フィールドやテーブル名など入力値が固定されない文字列の比較には向きません。
さらに strings.Containes() は return が bool 型です。この方法で検査すると if 文が増え続けてコードも冗長になりそうです。
あと一般的にいうスキャナーの仕様も満たしません。

一般に、文字列をなめるような処理をするものをスキャナという。字句解析の場合、文字列から、1個のトークンになるような部分文字列を切り出す部分をスキャナとして分けて考える場合がある。

https://ja.wikipedia.org/wiki/字句解析#字句解析器

今後

次はスキャナの処理を分解してみていきます。
冒頭にも書きましたが、2023/06/03時点では副問い合わせの解析までは行いません。
そのあたりの処理が書けたらまたアップデートしたいと思います。

Discussion