経緯
今のプロジェクトで、もともとOracle Databaseを使っていたものをAmazon Aurora PostgreSQLに移行することになりました。
そのためOracle Database用のSQL*Loaderを、PostgreSQL用のものに変える必要があります。
もちろんPostgreSQLにも似た機能をもつものはありますが、拡張機能のインストールが必要なようであり、残念ながらAmazon Aurora PostgreSQLではインストールできなかったため、やむを得ずローダツールを自作することになりました。
要件
- UTF8、SJIS両方のCSVを受け入れられること
- CSVそのままではなく、ロードする前にSQL*Loaderと同様関数などにかけられること
実装
最初はPythonで書く予定だったのですが、どうもうまくいかなかったのでTypeScriptで書きました。
実際に作成したツールの大まかな流れは以下のようになります。
- CTLファイルを読み出し、オプションやロード先のテーブル、各カラム毎のロード時の関数を取得
- CSVファイルを256MB毎にバイト文字で読み出す
- バイト文字をstringにデコード、関数を入れ子の内側から実行する
- psqlの\copyコマンドでロード実行
- CSVのEOFまで2に戻る
3でかける関数もTypeScriptで実装しています。
そこだけは同じ新卒の匿名希望さんに書いてもらったのですが、Oracle Databaseに実装されている全ては不可能なので使うことになりそうないくつかだけ実装してもらいました。
詰まりポイント
このツールではとにかく文字化けに悩まされました。
大きく分けて詰まった文字化けは2種類あり、CSVの文字化けと出力の文字化け、そして文字化け以外には構文解析もどきでも詰まりました。
CSVの文字化け
まずはCSVが文字化けする問題です。
なぜ実装セクションの2でわざわざバイト文字で読み出しているかというと、最初はUTF8でエンコードされているCSVをS3経由でStreamを使って少し(64KB)ずつロードするという手法を取らざるをえなかったためです。
なので、何も考えずにTextDecorderでデコードするとマルチバイト文字の途中で途切れてしまい、文字化けするという問題が発生しました。
しかも、文字化けしたことを検出してもう一度バイト文字にエンコードし直し、結合してからデコードしても元に戻らず文字化けしたままでした。
そのため読み出したバイト文字の末尾を一時的に変数に保存(キャッシュ)し、次の64KBの頭とくっつける方法を取りました。
ここで次に問題になるのは、どこからをキャッシュするかです。
文字列で考えたときに、どこからをキャッシュするかはすんなり決めることができました。
実装セクションの3はレコード毎に行っているので、CSVの最後1行がキャッシュするのに過不足無い範囲です。つまり改行で切ればいいわけですね。
ということで、Streamの最後のバイト文字で改行を示す10から後ろをキャッシュすることにしました。
しかし、ここでもう1つ問題が発生します。
それはStream内でレコードが途切れなかった場合とレコードの最初、もしくは最後のカラムで文字化けしない途切れ方をした場合です。
この場合、2レコードをまとめて1レコードとして扱ってしまったりしました。
おかげで文字化けはないのに明らかにレコード数が少なくなっており、原因の究明にはかなり苦労しました。
原因としてはキャッシュに入れるかどうかの判断をキャッシュが空かどうかと、デコードしたレコードのカラム数で行っており、カラム数があっていれば正しくデコードできたと扱ったためでした。
なので、キャッシュに入れる、キャッシュから取り出すときにStreamの最後、最初であることをチェックするようにしました。
最終的にはローカルのCSVを参照するため、いきなりstringで読み取ろうと思ったのですが、UTF8かShift_JISかが判別できなかったためにバイト文字から識別するようになっており無駄にならない処理になりました。
出力の文字化け
次に出力が文字化けする問題です。
このツールでは\copyやTRUNCATEをpsqlを使って行うのですが、TypeScriptからpsqlを実行するのにchild_processモジュールを使用しています。
Windowsで実行しているためpsqlの実行結果はShift_JISでデコードされた文字列で返されるのですが、残念ながらchild_processモジュールはShift_JISをサポートしていないため、console.logなどで出力すると全角文字が文字化けしてしまいます。
仕方ないので文字化けしないbase64で出力し、そこからShift_JISでデコードする羽目になりました。
構文解析もどき
最後に構文解析もどきです。
関数が入れ子になっている都合上、単純に前から読んでいって合致する関数を実行するみたいなことができません。
最初は大学のコンパイラの授業を思い出しながら地道に構文解析してたのですが、コードは長くなるしそもそもあまりうまく動きませんでした。
そこで、コンパイラに要求されるほど構文のパターンが多くもないことも相まって、正規表現と再帰で内側から実行することができました。
しかし、その正規表現も簡単にはできず紆余曲折の後、型名、関数名の部分、第1引数の部分、第2引数以降の部分の4か所にマッチするような正規表現を作成し、第2引数を一旦置いて、第1引数の部分だけをもう一度再帰で正規表現にかけるという工夫でなんとか期待通りの動作になりました。
まとめ
CSVの文字化けだけやけに長くなってしまいましたが、それだけ苦労した部分だということでご了承ください。
デバッグモードでひたすらバイト文字とにらめっこするのはなかなか辛いところがありましたね。
似たツールが存在することをわかっていながら車輪の再発明をすることになりましたが、なんとか使い物にはなるものができて少し達成感も感じています。