パスワードを忘れた? アカウント作成
678648 journal

Oliverの日記: パーサージェネレータ 2

日記 by Oliver

ちょこっとづづ進んでる大学の研究実習で書いてるRubyなオブジェクトリポジトリ(コードネームVapor)の基本機能がここ1ヵ月の集中でひととおり完成した。XMLなクラス説明からSQL DDLを作成した後、クラスにVapor::Persistableモジュールをincludeするだけで、そのクラスのオブジェクトをDBから出し入れできる。今週末は検索機能を実装した。当然ながら、SQLのWHERE文をそのまま書かせるわけにはいけないので、それにとっても近い独自のとってもシンプルなキュエリー言語を作った。

objects = pmersistence_mgr.query( Foo, "bar = ? AND baz = ?", [ 42, "very"] )

といった感じだ。これからキュエリーを表現するオブジェクトを作成してSQLのWHERE文に変換するわけだ。自力でパーズするのは、前にPostgreSQL Array -> Ruby Array変換ルーチンを書いたときに懲りているし、もちょっと複雑なオートマトンを自分で管理するのは面倒なので、今回はパーザージェネレータを使った。Rubyの定番パーザージェネレータといえばraccだが、RAAを漁ったところ、rockitというのを発見。raccがyacc風のイベント発生型パーザジェネレータならば、rockitはAbstract Syntax Tree(AST)生成機で、文法とアクションが分離できる。今回の文法をそれぞれの形式で書いてみたたものの中核だけ抜粋すると、raccだと

class Vapor::QueryParser

prechigh
left BOOL
preclow

rule

statement : '(' statement ')'{ result = val[1] } | IDENT COMP PLACEHOLDER { result = BasicQueryStatement.new( val[ 0], val[1], @arguments[@ph_count] ); @ph_count += 1 }
| statement BOOL statement { result = ComplexQueryStatement.new( val[0], val[1], val[2] ) }

end
[...] def parse( query )
@tokens = []
query.scan(/\w+|=|\?|\(|\)/).each{ |token|
case token
when /\A\w+/
puch @tokens [:IDENT, $&]
[...]
end
}
do_parse
end

といったところだ。resultを使ったracc固有の約束ごとが必要なのと、アクションが文法に紛れているのが特徴だ。半面、rockitだと

Grammar VaporQuery
Tokens
Blank = /(( )|(\t)|(\v))+/ [:Skip]
Identifier = /\w+/
Comp_Op = /=/
Bool_Op = /AND/
Placeholder = /\?/

Productions
Stmnt -> '(' Stmnt ')' [Stmnt: _,stmnt,_]
| Stmnt Bool_Op Stmnt [ComplexStmnt: stmnt1, op, stmnt2]
| Identifier Comp_Op Placeholder [BasicStmnt: identifier, op, placeholder]

Priorities
left(ComplexStmnt)

----

def query_eval(ast)
case ast.name
when "Stmnt"
query_eval( ast.stmnt )
when "ComplexStmnt"
return ComplexQueryStatement.new( query_eval( ast.stmnt1 ), ast.op[0], query_eval( ast.stmnt2 ) )
[...]
end
end

q = query_eval( QueryParser.parser.parse( query ).compact! )

といったところだ。正直いって、rockitのほうが素敵だと思うが、ランタイムに依存しない形式が作成でき、(今日の時点で)apt-get一発でインストールできるという理由でraccにした。rockitはこれからも要注目だな。

さてと、次はトランザクション処理の実装か。その後に残る課題はバージョン履歴システムとワークフロー支援の長期トランザクション (バージョン権利におけるブランチという構想)だ。暇があれば、なんか認証システムとオブジェクトの読み書きに関する権限システムも考えたい。さらに頑張るとして期末までの2ヵ月で完成するんかい。

ちなみに、最近、Ruby Array -> PostgreSQL Arrayの変換ルーチンも書いた。こっちはパーズしなくてよかったので楽だったが、バックスラッシュに泣いた。エスケープなんて仕組みが必要ない世の中が欲しい。前のパッチと共に再びruby-dbiリストに投げたところ、今度はマージされる可能性が高そうだ。Perl DBIやJDBCのPostgreSQLドライバにすらない高度な機能だもんね、売り文句になる。しかし、PostgreSQLのArrayって誰も使ってないんかな、リテラルの表現以外は便利なのに。

この議論は賞味期限が切れたので、アーカイブ化されています。 新たにコメントを付けることはできません。
  • by G7 (3009) on 2003年06月02日 0時06分 (#327562)
    Query言語を作らない、という選択は駄目な選択だったんでしょうか?
    SyntaxTreeをRubyのObjectとして作って与えるという形態とか。

    Rubyくらい記述力のある言語なら、専用の文を書くようにせず、
    Query Object(?)をその場で組み上げるという形態にしても、
    記述し易さはあまり劣らないんじゃないか?と想像します。想像だけですが。

    #C言語ベースでそれをやらせやがるライブラリに日夜苦しめられてるんでG7

    ふと思ったけど、括弧をIteratorで表現する、とか。

    class Where
        def initialize
            @data=[]
        end
        def eq(name, value)
            @data name.to_s
            @data '='
            @data value.to_s
            self
        end
        def and
            @data 'AND'
            self
        end
        def kakko # block
            @data '('
            yield self
            @data ')'
            self
        end
        def to_s
            @data.join(' ')
        end
    end

    puts Where.new.eq("hoge", 10).and.eq("fuga", "20")
    puts Where.new.kakko{|w| w.eq("hoge", 10)}.and.eq("fuga", "20")

    w2=Where.new
    puts w2.kakko{w2.eq("foo", "bar").and.kakko{w2.eq("zot", "aaa")}}

    うーん。やっぱりちっとも書きやすくないか(笑)
    • by Oliver (4) on 2003年06月04日 17時49分 (#329549) ホームページ 日記
      パーズした結果はQueryを表現するオブジェクトツリーなので、キュエリーを使い回したい、とかいろいろそのツリーを直接作りたい、という要求がでたら、パージングを迂回するインタフェース作るつもりです。いまのところ、唯一のユーザは「いらね」と言ってるので、作ってません。だから今のところは=とANDしかサポートしてなかったり。(笑)

      トランザクションとバージョン管理はまだだけど、現状でも有用と思えるところまできたので、ドキュメントをちょっと掃除したら、一度パブリックリリースしようかな、と思ってます。READMEを書くのが一番大変だ。
      親コメント
typodupeerror

「科学者は100%安全だと保証できないものは動かしてはならない」、科学者「えっ」、プログラマ「えっ」

読み込み中...