PostgreSQL付属ドキュメントの「外部プロジェクト」節にはPL/Rubyの記載がある (ただしリンク切れ)。その意味ではいわゆる車輪の再発明であるが、長らく保守されていない様子であること、適用に当たって細部を修正する必要が生じたことを考慮し、独自に開発することにした。
$ ruby extconf.rb $ make # cp plrb.so /usr/local/pgsql/lib
次にデータベースを生成し、PL/rbハンドラーを登録する。
$ createdb db_plrb $ psql -d db_plrb db_plrb=# CREATE FUNCTION plrb_call_handler() RETURNS language_handler db_plrb-# AS '$libdir/plrb' db_plrb-# LANGUAGE 'c'; db_plrb=# CREATE FUNCTION plrb_inline_handler(internal) RETURNS void db_plrb-# AS '$libdir/plrb' db_plrb-# LANGUAGE 'c'; db_plrb=# CREATE FUNCTION plrb_validator(oid) RETURNS void db_plrb-# AS '$libdir/plrb' db_plrb-# LANGUAGE 'c'; db_plrb=# CREATE LANGUAGE plrb db_plrb-# HANDLER plrb_call_handler db_plrb-# INLINE plrb_inline_handler db_plrb-# VALIDATOR plrb_validator;
ハンドラー函数は3つあるが、手続き言語ハンドラー (plrb_call_handler) が中心である。
有効性検証函数 (plrb_validator) は、「CREATE FUNCTION」実行時に構文検査をおこなう。インライン・ハンドラー (plrb_inline_handler) は、SQLコマンド「DO」で記述した無名コード・ブロックを実行する。
「CREATE EXTENSION」コマンドで登録する方法については、pg_pltemplateカタログの取り扱いが変わる過渡期であるため、保留にしている。PostgreSQL付属ドキュメントの「システム・カタログ」章、「pg_pltemplate」節には、「今後のPostgreSQLのリリースでpg_pltemplateは削除される予定」である旨の記載がある。
CREATE FUNCTION named_operator(num integer, op text) RETURNS text AS $$ case op when nil return "Operator is NULL"; when 'succ' return "#{num} -> #{num + 1}"; when 'pred' return "#{num} -> #{num - 1}"; when 'double' return "#{num} -> #{num * 2}"; else return "Unknown Operator #{op}"; end $$ LANGUAGE 'plrb';
このように、函数の定義部分はSQL、実装部分はrubyの構文で記述する。これで函数が定義されたので、SQL文中に埋め込んで呼び出すことができる。SQLの文字列定数は、二重引用符ではなく単一引用符で括ることに註意する。
SELECT named_operator(7, 'pred') AS result; -- "7 -> 6"
(SQLの) NULLを渡すと、ruby側ではnilになる。
SELECT named_operator(7, NULL) AS result; -- "Operator is NULL"
函数の定義部分にはUTF-8の文字列を記述できる。postgresql.confに、
plrb.script_encoding = 'EUC-JP'
のように記述すれば、このスクリプト・エンコーディングを変更できる。
PostgreSQL側とruby側で引数や戻り値をやり取りする際、次のように型が変換される。
PostgreSQL | ruby | 備考 |
---|---|---|
bool、boolean | FalseClass/TrueClass | |
int2、smallint | Integer | |
int4、integer | ||
int8、bigint | ||
float4、real | Float | |
float8、double precision | ||
numeric、decimal | BigDecimal | |
char | String | |
varchar | ||
text | ||
bytea | ||
date | Date | |
time | Time | Timeの日付部分は無視 |
timestamp | DateTime | |
timestamptz | ||
interval | String | 「15 years 1 mon 2 days」のような書式の文字列 |
bit | String | 「011010」のように「1」と「0」から成る文字列 |
varbit |
ruby側のStringは、エンコーディングがUTF-8になる (例外としてbyteaの場合はASCII-8BIT)。とは言っても何らかの変換を施すわけではなく、String#force_encoding()でエンコーディング情報を変えるだけである。これはpostgresql.confに
plrb.string_force_encoding = 'EUC-JP'
のように記述することにより変更できる。
「time with time zone」型については、あえて考慮していない。この型は機能に問題があるので使用を推奨しない旨、PostgreSQL付属ドキュメントの「日付/時刻データ型」節に記載がある。
rubyのTimeクラスは、1970~2038年の範囲しか扱えない (システム環境によってはもっと広い範囲を受け付けるが保証はない)。したがってtimestamp型やtimesatmptz型に対応づけるには不充分なので、DateTimeクラスを用いることにした。
timestamptz型の値は、内部的には64ビット整数 (2000-01-01 00:00:00 UTCを起点とするμ秒単位の値) として表される。タイムゾーン情報を保持しているわけではない。したがって、例えば「2010-10-10 13:00:00 +0700」というタイムスタンプを入力した場合、「2010-10-10 06:00:00 UTC」に相当する経過μ秒数が保存され、どのタイムゾーンの時刻として入力したか、という情報は失われてしまう。その後、出力の際には、システム標準のタイムゾーン (例えばJST) に変換され、「2010-10-10 15:00:00 +0900」となる (もちろんAT TIME ZONE構文で変換すればそれに従う)。一方、DateTimeクラスは、タイムゾーン情報も保持している。
ruby側の戻り値型と函数定義の戻り値型が違っている場合、(to_i、to_sなどの常識的な) 型変換が起こる。
CREATE FUNCTION int_returns_str(inp text) RETURNS integer AS $$ return inp.downcase; # 戻り値はString $$ LANGUAGE 'plrb'; SELECT int_returns_str('-87ab'); -- -87 (integer) SELECT int_returns_str('NOT NUMBER'); -- 0 (integer)
テーブル名はそのまま複合型の名前として使うことができる。ruby側にはHashの形で渡される。
CREATE TABLE user_tbl ( user_id text PRIMARY KEY, real_name text NOT NULL, privilege integer NOT NULL DEFAULT 0 ); CREATE FUNCTION format_user(user user_tbl) RETURNS text AS $$ if user['privilege'] == 0 return "#{user['user_id']} ==> #{user['real_name']}"; else return "#{user['user_id']} ::: #{user['real_name']}"; end $$ LANGUAGE 'plrb';
「CREATE TYPE」で定義した型も同様に使える。
CREATE TYPE color_type AS ( name text, red integer, green integer, blue integer ); CREATE FUNCTION color_value(col color_type) RETURNS integer AS $$ return col['red'] * 65535 + col['green'] * 256 + col['blue']; $$ LANGUAGE 'plrb';
複合型は戻り値として使うこともできる。
CREATE FUNCTION name_to_color(color_name text) RETURNS color_type AS $$ ## ... 省略 (色の名前からRGB値を調べる処理) return { 'name' => color_name, 'red' => 128, 'green' => 64, 'blue' => 64 }; $$ LANGUAGE 'plrb';
戻り値型が「SETOF ...」である函数の場合、戻り値をArrayの形で返す。
CREATE FUNCTION color_table() RETURNS SETOF color_type AS $$ tbl = [ { 'name' => 'black', 'red' => 0, 'green' => 0, 'blue' => 0 }, { 'name' => 'gray', 'red' => 128, 'green' => 128, 'blue' => 128 }, ## ... 以下省略; ここでは定数の形で記述したが、 ## カラー・テーブルを取得する何らかの処理を記述してもよい ]; return tbl; $$ LANGUAGE 'plrb';
Arrayの代わりに、「重複のないオブジェクトの集まり」を表すSetクラスを用いれば、関係演算との整合性が保てるかも知れない。しかし現実の関係データベースでは、レコードが重複しているテーブルも存在しうる。
戻り値をArrayの形で返す方式は、函数の戻り値型として配列を指定した場合、混乱が生じるおそれがある。しかし、配列型を導入することによる利点よりも、第1正規形が崩れるという問題の方が大きいため、広く普及するには至っていないようである。
初回の呼び出し時にArray以外を返すと、函数が繰り返し呼び出されるようになるので、集合の要素をひとつずつ返す。すべての要素を返した後、nilを返さなければならない。この点を間違えると処理が停止しないことになる。
Pg::call_counterは、何回目の呼び出しか、を表す(0、1、2、...)。次回の呼び出し時に情報を受け渡すために、Pg::user_contextを使うことができる。
CREATE FUNCTION color_table() RETURNS SETOF color_type AS $$ if Pg::call_counter == 0 tbl = ## ... カラー・テーブルを取得する処理 Pg::user_context = tbl; end tbl = Pg::user_context; if Pg::call_counter < tbl.size return tbl[Pg::call_counter]; else return nil; end $$ LANGUAGE 'plrb';
もちろんこの例は、一括モードで記述した方が簡単である。
PostgreSQL側のログ機能を用いて情報を出力するよう記述できる。
CREATE FUNCTION try_elog() RETURNS text AS $$ if ## .... Pg::elog(Pg::LevelNOTICE, "Some Error Message..."); end $$ LANGUAGE 'plrb';
Pg::elog() の第1引数には次のいずれかを指定する。なお、「備考」欄に示した出力先は、postgresql.confの設定 (client_min_messages、log_min_messages、log_min_error_statement) により変更できる。
第1引数 | 備考 |
---|---|
Pg::LevelDEBUG5 | |
Pg::LevelDEBUG4 | |
Pg::LevelDEBUG3 | |
Pg::LevelDEBUG2 | |
Pg::LevelDEBUG1 | |
Pg::LevelLOG | ログ・ファイルに出力 |
Pg::LevelCOMMERROR | ログ・ファイルに出力 |
Pg::LevelINFO | コンソールに出力 |
Pg::LevelNOTICE | コンソールに出力 |
Pg::LevelWARNING | コンソールとログ・ファイルに出力 |
Pg::LevelERROR | 以下は、ログ出力後、呼び出し元に制御が戻らない |
Pg::LevelFATAL | |
Pg::LevelPANIC |
引数なしの函数として宣言するが、「opt」という引数が (暗黙に) 渡される。これを介して、イヴェントの種類など必要な情報を取り出せる。
操作前に発行される行単位のトリガーについては、nilを返す (PostgreSQL側にはNULLが返る) ことにより、当該操作を実行しないよう指示できる。また、挿入/更新される行を変更することも可能である。
CREATE TABLE code_tbl ( code integer, value text ); CREATE FUNCTION cant_insert_bad_code() RETURNS trigger AS $$ if ## opt['RECORD_NEW']['code'] に関する複雑な条件判定 return nil; # INSERT処理を取り消す。 else return opt['RECORD_NEW']; end $$ LANGUAGE 'plrb'; CREATE TRIGGER trig_bad_code_constraint BEFORE INSERT ON code_tbl FOR EACH ROW EXECUTE PROCEDURE cant_insert_bad_code();
こういうことは可能な限り、検査制約 (CHECK) などで実装するべきである。ここでは、検査のために、SQLの機能だけでは実現できない処理を要する状況を想定している。
optを介して渡される情報:
名前 | 値 | 備考 |
---|---|---|
WHEN | BEFORE | AFTER | INSTEAD | |
LEVEL | ROW | STATEMENT | |
EVENT | INSERT | UPDATE | DELETE | TRUNCATE | |
RECORD_OLD | 削除しようとする行、または更新前の行 (Hash) | |
RECORD_NEW | 挿入しようとする行、または更新後の行 (Hash) | |
NAME | トリガー名 | |
TABLE_NAME | トリガーの発行元テーブル名 | |
TABLE_SCHEMA | トリガーの発行元テーブルがあるスキーマの名前 | |
ARGC | トリガー函数の引数の数 | |
ARGV | トリガー函数の引数の配列 |
引数なしの函数として宣言するが、「opt」という引数が (暗黙に) 渡される。これを介して、イヴェントの種類など必要な情報を取り出せる。
CREATE FUNCTION monitor_event_trigger() RETURNS event_trigger AS $$ Pg::elog(Pg::LevelNOTICE, "[Event Trigger] TAG = #{opt['TAG']}."); $$ LANGUAGE 'plrb'; CREATE EVENT TRIGGER monitor_ddl_command_start ON ddl_command_start EXECUTE PROCEDURE monitor_event_trigger();
optを介して渡される情報:
名前 | 値 | 備考 |
---|---|---|
EVENT | ddl_command_start | ddl_command_end | sql_drop | |
TAG | 実行対象イヴェントに関連するコマンド・タグ。"CREATE FUNCTION" など。 |
函数内からSQLコマンドを実行できる。
CREATE TABLE user_tbl ( user_id text PRIMARY KEY, passwd text NOT NULL DEFAULT '' ); CREATE FUNCTION login_authorized(user_id text, passwd text) RETURNS boolean AS $$ sql = "SELECT count(*) AS ct FROM user_tbl WHERE user_id = $1 AND passwd = $2;"; result = Pg::select(sql, user_id, passwd); ## あるいは result = Pg::select_with_type(sql, Pg::TypeTEXT, user_id, Pg::TypeTEXT, passwd); return (result[0]['ct'] == 1); $$ LANGUAGE 'plrb';
函数 | 説明 |
---|---|
Pg::execute(sql, val1, val2, ...); | 「INSERT」、「UPDATE」、「DELETE」などのコマンドを実行する。 |
result = Pg::select(sql, val1, val2, ...); | 「SELECT」コマンドを実行する。 |
result = Pg::funcall(func_name, val1, val2, ...); | (実験的実装) PostgreSQL側に定義した函数を呼び出す。 |
Pg::execute_with_type(sql, type1, val1, type2, val2, ...); | 「INSERT」、「UPDATE」、「DELETE」などのコマンドを実行する。偶数番目の引数はプレースホルダーの型* (下記参照)。 |
result = Pg::select_with_type(sql, type1, val1, type2, val2, ...); | 「SELECT」コマンドを実行する。偶数番目の引数はプレースホルダーの型* (下記参照)。 |
plan = Pg::prepare(sql, type1, type2, ...); | 実行する文を準備する。第2引数以降はプレースホルダーの型* (下記参照)。 |
result = Pg::exec_prepared(plan, val1, val2, ...); | 準備した文を実行する。「SELECT」の場合、戻り値はPg::select() と同様。 |
Pg::free_plan(plan); | 準備した文を解放する。 |
portal = Pg::open_cursor(plan, val1, val2, ...); | 「SELECT」用のカーソルを設定する。 |
record = Pg::fetch_one(portal); | 1行取り出し、カーソルを進める。 |
Pg::close_cursor(portal); | カーソルを閉じる。 |
*に指定できる「型」定数:
定数 | (同じ意味の定数) |
---|---|
Pg::TypeBOOL | Pg::TypeBOOLEAN |
Pg::TypeINT8 | Pg::TypeBIGINT |
Pg::TypeINT2 | Pg::TypeSMALLINT |
Pg::TypeINT4 | Pg::TypeINTEGER |
Pg::TypeNUMERIC | Pg::TypeDECIMAL |
Pg::TypeFLOAT4 | Pg::TypeREAL |
Pg::TypeFLOAT8 | Pg::TypeDOUBLE、Pg::TypeDOUBLE_PRECISION |
Pg::TypeTEXT | |
Pg::TypeBPCHAR | Pg::TypeCHARACTER |
Pg::TypeVARCHAR | Pg::TypeCHARACTER_VARYING |
Pg::TypeBYTEA | |
Pg::TypeDATE | |
Pg::TypeTIME | |
Pg::TypeTIMESTAMP | |
Pg::TypeTIMESTAMPTZ | |
Pg::TypeBIT | |
Pg::TypeVARBIT | Pg::TypeBIT_VARYING |
Pg::TypeINTERVAL |
CREATE TABLE user_tbl ( user_id text PRIMARY KEY, passwd text NOT NULL DEFAULT '', privilege integer NOT NULL DEFAULT 0 ); -- パスワードは、生のままでなく暗号化して格納する。 -- レインボー・テーブル攻撃等に備えるため、ユーザーIDをソルトとして用いる。 CREATE FUNCTION encrypt_passwd(user_id text, passwd text) RETURNS text AS $$ require 'digest/md5'; fixed_seed = '0ZDf7Fl3Dluuk)kjdsZ^sjDFF0S_jJDl'; crypted_passwd = Digest::MD5::hexdigest(passwd || ''); 125.times do crypted_passwd = Digest::MD5::hexdigest(fixed_seed + user_id + crypted_passwd); end return crypted_passwd; $$ LANGUAGE 'plrb'; -- ユーザーを登録しようとすると起動されるトリガー。 -- INSERTコマンドに渡されたパスワードに暗号化を施す。 CREATE FUNCTION trig_func_insert_user() RETURNS trigger AS $$ ret = opt['RECORD_NEW'].dup; # 与えられたパスワードを暗号化して格納する。 sql = "SELECT encrypt_passwd($1::text, $2::text) AS pw;" crypted_passwd = Pg::select(sql, ret['user_id'], ret['passwd'])[0]['pw']; ret['passwd'] = crypted_passwd; return ret; $$ LANGUAGE 'plrb'; CREATE TRIGGER trig_insert_user BEFORE INSERT ON user_tbl FOR EACH ROW EXECUTE PROCEDURE trig_func_insert_user(); -- ユーザー情報を更新しようとすると起動されるトリガー。 -- ユーザーIDをソルトとして使っているため、これを更新する場合は -- パスワードをいったん無効にし、改めて設定する必要がある。 CREATE FUNCTION trig_func_update_user() RETURNS trigger AS $$ sql = "SELECT encrypt_passwd($1::text, $2::text) AS pw;" ret = opt['RECORD_NEW'].dup; if opt['RECORD_OLD']['user_id'] != opt['RECORD_NEW']['user_id'] # user_idが変わるのでパスワードをいったん無効にする。 ret['passwd'] = ''; elsif opt['RECORD_OLD']['passwd'] != opt['RECORD_NEW']['passwd'] # パスワードが更新されるか確認する。 # そのために、与えられたパスワードを暗号化し、保存してある # 旧パスワードと比較する。 user_id = opt['RECORD_NEW']['user_id']; passwd = opt['RECORD_NEW']['passwd']; crypted_passwd = Pg::select(sql, user_id, passwd)[0]['pw']; if opt['RECORD_OLD']['passwd'] != crypted_passwd # パスワードが更新される場合 ret['passwd'] = crypted_passwd; Pg::elog(Pg::LevelNOTICE, "Password updated."); else ret['passwd'] = opt['RECORD_OLD']['passwd']; end end return ret; $$ LANGUAGE 'plrb'; CREATE TRIGGER trig_update_user BEFORE UPDATE ON user_tbl FOR EACH ROW EXECUTE PROCEDURE trig_func_update_user(); -- ユーザーの認証。 -- 与えられたパスワードを暗号化し、user_tblに該当するレコードが -- あるかどうか調べる。 CREATE FUNCTION login_authorized(user_id text, passwd text) RETURNS boolean AS $$ sql = "SELECT encrypt_passwd($1::text, $2::text) AS pw;" crypted_passwd = Pg::select(sql, user_id, passwd)[0]['pw']; sql = "SELECT count(*) AS ct FROM user_tbl WHERE user_id = $1 AND passwd = $2;"; result = Pg::select(sql, user_id, crypted_passwd); return (result[0]['ct'] == 1); $$ LANGUAGE 'plrb';
Pg::funcall()を使って次のように記述すれば若干見やすくなる。
CREATE FUNCTION login_authorized(user_id text, passwd text) RETURNS boolean AS $$ crypted_passwd = Pg::funcall('encrypt_passwd', user_id, passwd); sql = "SELECT count(*) AS ct FROM user_tbl WHERE user_id = $1 AND passwd = $2;"; result = Pg::select(sql, user_id, crypted_passwd); return (result[0]['ct'] == 1); $$ LANGUAGE 'plrb';
PostgreSQL函数の形でこみ入った処理を組み込もうとすると、テストのしにくさが問題になる。そこで、次のような手順で開発を進めるよう推奨する。
Auto-requireは、postgresql.confに次のように記述しておけば、自動的にrequireされる、という機能である。多くの函数にrequireを記述しなければならないような状況でも、Auto-requireならば1回で済む。
plrb.auto_require = 'foo,baa'
複数ある場合はカンマで区切る。Rubyの$LOAD_PATH上にない場合は絶対パスで指定する。
PL/Perlには「plperl.on_init」という似た機能がある。しかし、requireに限らず何でも書ける点が、逆に問題になるおそれがある。
この考え方にもとづいて先の例を書き換えると、たとえば次のようになる。
# user_tbl_helpers.rb require 'digest/md5'; class UserTblHelpers # パスワードは、生のままでなく暗号化して格納する。 # レインボー・テーブル攻撃等に備えるため、ユーザーIDをソルトとして用いる。 def encrypt_passwd(user_id, passwd) fixed_seed = '0ZDf7Fl3Dluuk)kjdsZ^sjDFF0S_jJDl'; crypted_passwd = Digest::MD5::hexdigest(passwd || ''); 125.times do crypted_passwd = Digest::MD5::hexdigest(fixed_seed + user_id + crypted_passwd); end return crypted_passwd; end # ユーザーを登録しようとすると起動されるトリガー。 # INSERTコマンドに渡されたパスワードに暗号化を施す。 def trig_func_insert_user(opt) ret = opt['RECORD_NEW'].dup; # 与えられたパスワードを暗号化して格納する。 crypted_passwd = encrypt_passwd(ret['user_id'], ret['passwd']); ret['passwd'] = crypted_passwd; return ret; end # ユーザー情報を更新しようとすると起動されるトリガー。 # ユーザーIDをソルトとして使っているため、これを更新する場合は # パスワードをいったん無効にし、改めて設定する必要がある。 def trig_func_update_user(opt) ret = opt['RECORD_NEW'].dup; if opt['RECORD_OLD']['user_id'] != opt['RECORD_NEW']['user_id'] # user_idが変わるのでパスワードをいったん無効にする。 ret['passwd'] = ''; elsif opt['RECORD_OLD']['passwd'] != opt['RECORD_NEW']['passwd'] # パスワードが更新されるか確認する。 # そのために、与えられたパスワードを暗号化し、保存してある # 旧パスワードと比較する。 user_id = opt['RECORD_NEW']['user_id']; passwd = opt['RECORD_NEW']['passwd']; crypted_passwd = encrypt_passwd(user_id, passwd); if opt['RECORD_OLD']['passwd'] != crypted_passwd # パスワードが更新される場合 ret['passwd'] = crypted_passwd; Pg::elog(Pg::LevelNOTICE, "Password updated."); else ret['passwd'] = opt['RECORD_OLD']['passwd']; end end return ret; end # ユーザーの認証。 # 与えられたパスワードを暗号化し、user_tblに該当するレコードが # あるかどうか調べる。 def login_authorized(user_id, passwd) crypted_passwd = encrypt_passwd(user_id, passwd); sql = "SELECT count(*) AS ct FROM user_tbl WHERE user_id = $1 AND passwd = $2;"; result = Pg::select(sql, user_id, crypted_passwd); return (result[0]['ct'] == 1); end end
このファイルを /path/to/helpers/user_tbl_helpers.rb として保存したとすれば、postgresql.confに
plrb.auto_require = '/path/to/helpers/user_tbl_helpers'
と記述する (「pg_ctl reload」などで忘れずに設定を再読み込み)。
すると、PostgreSQL側の函数は、次のようにごく簡単なものになる。
CREATE FUNCTION trig_func_insert_user() RETURNS trigger AS $$ helpers = UserTblHelpers.new; return helpers.trig_func_insert_user(opt); $$ LANGUAGE 'plrb'; CREATE TRIGGER trig_insert_user BEFORE INSERT ON user_tbl FOR EACH ROW EXECUTE PROCEDURE trig_func_insert_user(); CREATE FUNCTION trig_func_update_user() RETURNS trigger AS $$ helpers = UserTblHelpers.new; return helpers.trig_func_update_user(opt); $$ LANGUAGE 'plrb'; CREATE TRIGGER trig_update_user BEFORE UPDATE ON user_tbl FOR EACH ROW EXECUTE PROCEDURE trig_func_update_user(); CREATE FUNCTION login_authorized(user_id text, passwd text) RETURNS boolean AS $$ helpers = UserTblHelpers.new; return helpers.login_authorized(user_id, passwd); $$ LANGUAGE 'plrb';
ここではあえて極端に走り、実質的な実装をすべてuser_tbl_helpers.rbに分離してしまう、という書き方を示した。実際には処理内容に応じて、どこで分離するか判断するべきである。
たとえばlogin_authorized()の動作をテストする場合、「SELECT login_authorized('Sally', 'pass_brown');」のようなSQLを実行し、返されたレコードが想定どおりであるかどうか確認することになるであろう。しかし、上記のように実質的な実装コードを分離してしまえば、ruby側のlogin_authorized()にいろいろな引数値を渡して戻り値を確認しても、ほぼ目的を達成できる。たとえば次のようなテスト・プログラムになるであろう。
test_cases = [ [ 'Sally', 'pass_brown', true ], [ 'Sally', 'BAD_PASSWD', false ], ]; helpers = UserTblHelpers.new; test_cases.each do |user_id, passwd, guess_result| result = helpers.login_authorized(user_id, passwd); am_assert_equals(guess_result, result); end
しかし、login_authorized()の実装にはPg::select()が使われているので、このままでは動作しない。
そこでpgx.rbに、テストの際に用いるPg::select()その他の代替実装を用意した。
Pg::execute(sql, val1, val2, ...); | PostgreSQLのクライアント・インターフェイスであるlibpqを使って実装しているが、ほぼ同じように動作する。 |
result = Pg::select(sql, val1, val2, ...); | |
Pg::execute_with_type(sql, type1, val1, type2, val2, ...); | 未実装。 |
result = Pg::select_with_type(sql, type1, val1, type2, val2, ...); | |
plan = Pg::prepare(sql, type1, type2, ...); | |
result = Pg::exec_prepared(plan, val1, val2, ...); | |
Pg::free_plan(plan); | |
portal = Pg::open_cursor(plan, val1, val2, ...); | |
record = Pg::fetch_one(portal); | |
Pg::close_cursor(portal); | |
result = Pg::funcall(func_name, val1, val2, ...); | なし (無意味) |
Pg::connect(conninfo) | データベースへの接続と切断* (下記参照)。 |
Pg::disconnect() | |
Pg::elog(level, message) | levelにかかわらず、標準エラー出力にメッセージを出力する。 |
(*) テストに先立ってデータベースに接続する必要がある。conninfoには、たとえば 'postgresql://local_host/db_plrb' のような文字列を指定する。
このpgx.rbを使ったテスト・プログラム例を以下に示す。
require_relative 'user_tbl_helpers'; require_relative 'pgx'; require_relative 'am_assert'; include AmAssert; # ====================================================================== # class TestUserTblHelpers def setup() Pg::connect('postgresql://localhost/db_plrb'); # データベースへの接続: この節で説明した方式で # 実装し、テストする場合にのみ必要。 end def teardown() Pg::disconnect(); end def test_login_authorized() setup(); # テスト環境の準備: ユーザーを登録する。 @pq = Pq.new('postgresql://localhost/db_plrb'); @pq.execute("DELETE FROM user_tbl;"); @pq.execute("INSERT INTO user_tbl (user_id, passwd) VALUES ($1, $2);", 'Sally', 'pass_brown'); # ---------------------------------------------------- test_cases = [ [ 'Sally', 'pass_brown', true ], [ 'Sally', 'BAD_PASSWD', false ], ]; helpers = UserTblHelpers.new; test_cases.each do |user_id, passwd, guess_result| result = helpers.login_authorized(user_id, passwd); # login_authorized()の実装には # Pg::select()の記述があるが、 # テストの際には代替実装版の # Pg::select()を実行する。 am_assert_equals(guess_result, result); end teardown(); end end # ====================================================================== # if __FILE__ == $0 t = TestUserTblHelpers.new; t.test_login_authorized(); end
Copyright © 2015 KOYAMA Hiro <tac@amris.co.jp>