2015年11月18日水曜日

impylaでqmark置換がうまく動かない

データベースで遊ぶならImpalaどうですか?と言われ使ってみることに。

いずれはPythonでいろいろするつもりだったので、Clouderaが提供しているPythonバインディングであるimpylaモジュールの使い勝手の確認も兼ね、手元のSqlite3データベースから行単位でレコードをコピーしていくPythonプログラムを書いてみました。ちなみに全レコードを単に載せ替えるだけなら、CSVなりに変換して直接HDFSからインポートする方が100万倍(未計測)速いです。

テーブルの構成は単純で、(INT, TIMESTAMP, STRING, STRING) の4カラムのみです。ところが、このレコードをimpyla経由で追加しようとしてもどうしてもうまくいかない。

cursor.execute('insert into table1 values (?,?,?,?)', (intvalue1, datetimevalue1, stringvalue1, stringvalue2))

どこにも間違う要素のない処理なのですが、必ずエラーが発生してしまいます。調べてみると、Impalaへ投げるSQL文を構築する途中で、datetimevalue1で置換されるべき部分が想定しない値になっていることが判明しました。

impylaモジュールは、他のPython用データベースアクセスモジュールと同様、Python DB-API 2.0に準拠しています。execute()の第一引数で渡したSQL文の?が第二引数で渡した値に置き換えられる、という部分はその仕様の一部であり、qmark paramstyleと呼ばれる書式です。SQL文の文字列置換の方式には幾つかあり、%sで指定した場所が置き換えられるformat paramstyle、:1:2など第二引数の位置に応じて置き換えられるnumbered paramstyle、:nameと指定してnameをキーとする辞書を第二引数に渡すnamed paramstyleなどがあります。(もう一つ、pyformatという書式がありますが、これはimpylaでは対応していません)

qmark、format、numbered paramstyleでの置換は、impyla/interfaces.py_replace_numeric_markers()関数で実装されていますが、ここが問題でした。元の関数には以下のような記述があります。

operation = replace_markers('?', operation, string_parameters)
operation = replace_markers(r'%s', operation, string_parameters)
for index in range(len(string_parameters), 0, -1):
    operation = operation.replace(':' + str(index),
                                  string_parameters[index - 1])

operationにはexecute()の第一引数の内容が、string_parametersには第二引数で指定されたタプルを文字列リストに変換した情報が入っています。

?で指定された部分を置き換え、続いて%sで指定された部分を置き換え、さらに:数値で指定された部分を置き換える、という処理を意図したものだと思われますが、明らかに変です。これでは、置換結果に別の書式の置換マークが含まれていた場合に正しい結果が得られません。例えば、qmarkで置換した文字列に%s:1などの部分文字列が含まれていると、その後の置換処理で意図しない置換が起きてしまいます。僕のコードが正しく動作しなかったのも、時刻文字列に:数値が含まれていたため、時刻の一部が再置換されてしまったことが理由でした。

_replace_numeric_markers()にはもう一つ問題があります。関数内部で入れ子定義されているreplace_markers()関数の処理はおおよそ以下のようになっています。

def replace_markers(marker, op, parameters):
    marker_index = 0
    while op.find(marker) > -1:
        op = op.replace(marker, parameters[marker_index], 1)
        marker_index += 1

これも場合によっては正しく動作しません。置換マークをfind()メソッドで検索しているため、置き換えた後の文字列に、置換マークが含まれていると再度マッチしてしまうためです。

上記の問題を修正するためのパッチを作ってpull requestしてみました。改善されるといいなぁ。

ちなみに、named paramstyleを用いた場合は別の関数で処理されるようになっており、そちらは問題なさそうです。