r7km/s

r7kamura per second.

Scheman

2014-07-10

Schemanという、Ruby製のSQLパーサをつくった。

文章で説明するより見たほうが早いだろうということで、例を用意した。

require "scheman"
require "yaml"

parser = Scheman::Parsers::Mysql.new

schema = parser.parse(<<SQL)
CREATE TABLE `users` (
  `id` INTEGER(11) NOT NULL PRIMARY KEY AUTO INCREMENT,
  `name` VARCHAR(255) NOT NULL
);
SQL

puts schema.to_hash.to_yaml

構文解析結果はHash, Array, Symbol, Stringの組合せで表現される (※可読性のためにYAML形式で表示した)

---
- :create_table:
    :name: users
    :fields:
    - :field:
        :name: id
        :type: integer
        :qualifiers:
        - :qualifier:
            :type: not_null
        - :qualifier:
            :type: primary_key
        - :qualifier:
            :type: auto_increment
        :values:
        - '11'
    - :field:
        :name: name
        :type: varchar
        :qualifiers:
        - :qualifier:
            :type: not_null
        :values:
        - '255'
    :indices: []

構文解析

構文解析にはParsletというライブラリを利用している。 Parsletは PEG という再帰下降構文解析用の文法を利用したライブラリで、 Parsletを使うことでRubyのDSLで文法を定義して構文解析を行える。 Parsletを利用した文法定義と構文解析の例を示そう。 この例では、CREATE DATABASEだけが利用できる小さな言語を定義する。

require "parslet"

class Parser < Parslet::Parser
  root(:statements)

  rule(:statements) do
    statement.repeat(1)
  end

  rule(:statement) do
    create_database.as(:create_database)
  end

  rule(:create_database) do
    str("CREATE ") >> (str("DATABASE") | str("SCHEMA")) >> space >> database_name >> str(";")
  end

  rule(:database_name) do
    match("[^;]").repeat(1).as(:database_name)
  end

  rule(:space) do
    str(" ").repeat(1)
  end
end

parser = Parser.new
parser.parse("CREATE DATABASE test;")
#=> [{:create_database=>{:database_name=>"test"@16}}]

Parslet::Parserを継承したクラスをつくり、.ruleメソッドを幾つか実行して文法規則を定義している。 このクラスのインスタンスをつくり、構文解析させたい文字列を引数に .parseメソッドを実行する。 .parseメソッドは、定義された文法規則を利用して渡された文字列を全て消費しようと試みる。 もし全て消費できた場合、即ちこの文字列を受理できた場合、結果が返される (受理できなかった場合はここで例外が発生する)。

.parseの戻り値は、Array, Hash, String (実際にはStringではない) で構成されたオブジェクトだ。 文法規則の定義の中で実は重要な箇所に名前を与えているのだが、 Parslet::Parser#parse は名前を与えた部分のみを集約して戻り値として返すようになっている。 解析結果から取り出したい部分、つまり意味のある部分には名前を与える必要があるということだ。 今回の例であれば、CREATE DATABASE文が呼ばれたことを表す部分、そのときに利用されるDB名にそれぞれ名前を付けている。

今回の例で定義した文法は次の図のようなものだ。 まず初めにCREATEを消費し、次にDATABASEまたはSCHEMAを消費する。 CREATE DATABASEの代わりにCREATE SCHEMAも使えるということだ。

str("CREATE ") >> (str("DATABASE") | str("SCHEMA")) >> space >> database_name >> str(";")

次にSPACEを消費する。SPACEは1つ以上の半角スペースからなる規則で、 .repeat(min, max) により1回以上の繰り返しを表現している。

str(" ").repeat(1)

次にDATABASE_NAMEを消費する。 DATABASE_NAMEの次に来るのが「;」なので、 ここでは「;」以外の文字が登場するまで繰り返し消費し続ける規則を定義している。 .matchを使うと正規表現が利用できる (但し連続する複数の文字に一致する正規表現は渡せないという制約がある)。

match("[^;]").repeat(1).as(:database_name)

.as(:database_name) の呼び出しが、前述した名前を与える行為に該当する。 Parslet::Parserの文法規則では、全てのパターンを個々にオブジェクトとして扱い、 メソッドチェーンによってパターンを拡張していくという形になっている。 この部分部分のパターンに名前を与えておくことで、 「XXXという名前の付いたパターンをYYYという文字列が消費した」 という情報が解析結果に含まれるようになるのだ。

変換器

あとで書く: 解析結果を使いやすい形に変換する技術

スキーマ

あとで書く: このライブラリにおける「スキーマ」の定義

差分抽出

あとで書く: このライブラリにおける「差分」の定義、2つのスキーマから差分を抽出する方法

SQL生成

あとで書く: 差分をSQLとして出力する方法

活用方法

あとで書く: Webアプリ開発等で活用する方法について

展望

あとで書く: 今後の可能性など