著者: Roberto Mello (<rmello@fslc.usu.edu>)
この節では開発者へオラクルから PostgreSQL へのアプリケーションの移植の手助けとなるようオラクルの PL/SQL と PostgreSQL の PL/pgSQL の違いについて説明します。ここにあるコードの大部分はArsDigita Clickstream moduleにあります。 私は2000年の夏にOpenForce Inc.でインターン実習を行った時に PostgreSQL に移植しました。
PL/pgSQL は 多くの点で PL/SQL に似ています。それはブロックで構成されていて、厳格な言語であるということです。 (全ての変数は宣言されなければいけません。)PL/SQL は PostgreSQL に対応するものより多くの機能があります、しかし PL/pgSQL にも多くの機能があり、それは絶えず改善されています。
Oracle から PostgreSQL に移植するとき注意すべき点がいくつかあります。
PostgreSQL にはデフォルトパラメータがありません。
PostgreSQL では関数をオーバーロードすることができます。 これはしばしばデフォルトパラメータの欠如を補うために使われます。
代入とループと条件文は似ています。
PostgreSQL ではカーソルは必要ありません。 FOR 文内に問い合わせを出力するだけです。 (以下の例をみてください)
PostgreSQL では単一引用符をエスケープする必要があります。Section 19.11.1.1を参照してください。
PostgreSQL では、関数定義内では単一引用符をエスケープさせる必要があります。Example 19-6 のような他の関数を生成するような関数を作成する場合に特に楽しいコードを書くことができます。たくさんの単一引用符をエスケープする際に注意をしてほしいのは、最初と最後の引用符を除き、それ以外のすべては偶数になることです。
Table 19-1 はスクープです。(この小さな図をきっと気に入ってくれることでしょう)
Table 19-1. シングルクォートのエスケープ図
| 番号 | 使い方 | 例 | 結果 | 
|---|---|---|---|
| 1 | 関数本体の最初と最後 | CREATE FUNCTION foo() RETURNS INTEGER AS '...' LANGUAGE 'plpgsql'; | そのまま | 
| 2 | 代入や SELECT 文中で文字列などを囲む場合 | a_output := ''Blah''; SELECT * FROM users WHERE f_name=''foobar''; | SELECT * FROM users WHERE f_name='foobar'; | 
| 4 | 文字列の途中で結果の文字列に二つの単一引用符が必要な時 | a_output := a_output || '' AND name
    LIKE ''''foobar'''' AND ...'' | AND name LIKE 'foobar' AND ... | 
| 6 | 結果として生じる文字列で二重引用符が欲しい場合かつその文字列を終了したい場合 | a_output := a_output || '' AND name
    LIKE ''''foobar'''''' | AND name LIKE 'foobar' | 
| 10 | 結果の文字列に二つの単一引用符が欲しい場合(8個の引用符が必要) かつはその文字列を終了させたい場合(もう 2 つ)。Example 19-6のように)関数を使って他の関数を生成させる場合にだけ必要でしょう。 | a_output := a_output || '' if v_'' ||
    referrer_keys.kind || '' like '''''''''' 
    || referrer_keys.key_string || '''''''''' 
    then return ''''''  || referrer_keys.referrer_type 
    || ''''''; end if;'';  | if v_<...> like ''<...>'' then return ''<...>''; end if; | 
Example 19-5. 簡単な関数
これはオラクルの関数です。
CREATE OR REPLACE FUNCTION cs_fmt_browser_version(v_name IN varchar, v_version IN varchar)
RETURN varchar IS
BEGIN
    IF v_version IS NULL THEN
        RETURN v_name;
    END IF;
    RETURN v_name || '/' || v_version;
END;
/
SHOW ERRORS;
この関数を通じて、PL/pgSQL との違いを見てみましょう。
PostgreSQL には名前付きパラメータはありません。関数内部で明示的に別名を付ける必要があります。
Oracle は IN、OUT、 INOUT というパラメータを関数に渡すことができます。 例えば、INOUT は、パラメータが値を受け取って別の値を返すことを意味します。PostgreSQL では "IN" パラメータしかないため、関数は単一の値しか返せません。
関数プロトタイプ内の RETURN キーワード(関数本体ではありません)は PostgreSQL では RETURNS になります。
PostgreSQL 関数では単一引用符が区切り文字として使用されているので、関数内では単一引用符をエスケープしなければいけません(これが時々いらいらさせるんです。 Section 19.11.1.1を参照してください)。
PostgreSQL には/show errors コマンドはありません。
それではこの関数が PostgreSQL に移植されるとどのようになるかみてみましょう。
CREATE OR REPLACE FUNCTION cs_fmt_browser_version(VARCHAR, VARCHAR)
RETURNS VARCHAR AS '
DECLARE
    v_name ALIAS FOR $1;
    v_version ALIAS FOR $2;
BEGIN
    IF v_version IS NULL THEN
        return v_name;
    END IF;
    RETURN v_name || ''/'' || v_version;
END;
' LANGUAGE 'plpgsql';
Example 19-6. 他の関数を作り出す関数
以下の手続きは SELECT 文からの行をとって、効率のために IF 文で結果を巨大な関数にうめこんでいます。PostgreSQL とはカーソル、FORループ、単一引用符をエスケープする必要があるというという違いに気づくでしょう。
CREATE OR REPLACE PROCEDURE cs_update_referrer_type_proc IS
    CURSOR referrer_keys IS    
        SELECT * FROM cs_referrer_keys 
        ORDER BY try_order;
    a_output VARCHAR(4000); 
BEGIN 
    a_output := 'CREATE OR REPLACE FUNCTION cs_find_referrer_type(v_host IN VARCHAR, v_domain IN VARCHAR, 
v_url IN VARCHAR) RETURN VARCHAR IS BEGIN'; 
    FOR referrer_key IN referrer_keys LOOP 
        a_output := a_output || ' IF v_' || referrer_key.kind || ' LIKE ''' || 
referrer_key.key_string || ''' THEN RETURN ''' || referrer_key.referrer_type || 
'''; END IF;'; 
    END LOOP; 
    a_output := a_output || ' RETURN NULL; END;'; 
    EXECUTE IMMEDIATE a_output; 
END; 
/ 
show errors
この機能を PostgreSQL で記述するとこうなるでしょう。
CREATE FUNCTION cs_update_referrer_type_proc() RETURNS INTEGER AS '
DECLARE
    referrer_keys RECORD;  -- FOR で使用される汎用記憶場所を宣言
    a_output varchar(4000);
BEGIN
    a_output := ''CREATE FUNCTION cs_find_referrer_type(VARCHAR,VARCHAR,VARCHAR) 
                  RETURNS VARCHAR AS '''' 
                     DECLARE 
                         v_host ALIAS FOR $1; 
                         v_domain ALIAS FOR $2; 
                         v_url ALIAS FOR $3;
                     BEGIN ''; 
    -- 
    -- FOR <record> 構文を使用した FOR ループで、問い合わせの結果
    -- をどのようにスキャンしているかに注意して下さい。
    --
    FOR referrer_keys IN SELECT * FROM cs_referrer_keys ORDER BY try_order LOOP
        a_output := a_output || '' IF v_'' || referrer_keys.kind || '' LIKE '''''''''' 
                 || referrer_keys.key_string || '''''''''' THEN RETURN '''''' 
                 || referrer_keys.referrer_type || ''''''; END IF;''; 
    END LOOP; 
  
    a_output := a_output || '' RETURN NULL; END; '''' LANGUAGE ''''plpgsql'''';''; 
    -- これはどの変数も置き換えていないのでうまくいきます。
    -- さもなければ失敗します。関数を実行する他の方法については PERFORM を参照して下さい。
    
    EXECUTE a_output;
END;
' LANGUAGE 'plpgsql';
Example 19-7. たくさんの文字列操作プロシージャと、OUT パラメータ
以下のオラクルの PL/SQL プロシージャは、URL をパースしていくつかの要素(ホスト、パス、問い合わせ)を返します。PL/pgSQL 関数は一つの値のみを返すので一つのプロシージャとなります(Section 19.11.3を参照してください)。PostgreSQL ではこのプロシージャを3つの異なる関数に分割する方法があります。一つはホストを返し、もう一つはパスを返し、もう一つを問い合わせを返します。
CREATE OR REPLACE PROCEDURE cs_parse_url(
    v_url IN VARCHAR,
    v_host OUT VARCHAR,  -- これは呼び出し元に渡されます。
    v_path OUT VARCHAR,  -- これも同様。
    v_query OUT VARCHAR) -- これもです。
is
    a_pos1 INTEGER;
    a_pos2 INTEGER;
begin
    v_host := NULL;
    v_path := NULL;
    v_query := NULL;
    a_pos1 := instr(v_url, '//'); -- PostgreSQL には instr 関数はありません。
    IF a_pos1 = 0 THEN
        RETURN;
    END IF;    
    a_pos2 := instr(v_url, '/', a_pos1 + 2);
    IF a_pos2 = 0 THEN
        v_host := substr(v_url, a_pos1 + 2);
        v_path := '/';
        RETURN;
    END IF;
    v_host := substr(v_url, a_pos1 + 2, a_pos2 - a_pos1 - 2);
    a_pos1 := instr(v_url, '?', a_pos2 + 1);
    IF a_pos1 = 0 THEN
        v_path := substr(v_url, a_pos2);
        RETURN;
    END IF;
    v_path := substr(v_url, a_pos2, a_pos1 - a_pos2);
    v_query := substr(v_url, a_pos1 + 1);
END;
/
show errors;PostgreSQL に移植したのがこのこの手続きです。
CREATE OR REPLACE FUNCTION cs_parse_url_host(VARCHAR) RETURNS VARCHAR AS '
DECLARE
    v_url ALIAS FOR $1;
    v_host VARCHAR;
    v_path VARCHAR;
    a_pos1 INTEGER;
    a_pos2 INTEGER;
    a_pos3 INTEGER;
BEGIN
    v_host := NULL;
    a_pos1 := instr(v_url,''//'');
    IF a_pos1 = 0 THEN
        RETURN '''';  -- 空文字を返します。
    END IF;
    a_pos2 := instr(v_url,''/'',a_pos1 + 2); 
    IF a_pos2 = 0 THEN 
        v_host := substr(v_url, a_pos1 + 2); 
        v_path := ''/''; 
        RETURN v_host; 
    END IF; 
    v_host := substr(v_url, a_pos1 + 2, a_pos2 - a_pos1 - 2 ); 
    RETURN v_host; 
END; 
' LANGUAGE 'plpgsql';
Note: PostgreSQL にはinstr 関数はありませんが、その他の関数を組み合わせて使用することでその動作をさせることできます。私はこれにあきてしまい、 Oracleの関数に似た独自の instr 関数を作成しました(これで今後がより楽になります)。コードはSection 19.11.6 を参照して下さい。
明示的に返り値をもつ必要がありませんので、オラクルのプロシージャは開発者により柔軟性を与えています。 INOUT か OUT パラメータを使うことができます。
以下に例を示します。
CREATE OR REPLACE PROCEDURE cs_create_job(v_job_id IN INTEGER) IS
    a_running_job_count INTEGER;
    PRAGMA AUTONOMOUS_TRANSACTION;(1)
BEGIN
    LOCK TABLE cs_jobs IN EXCLUSIVE MODE;(2)
    SELECT count(*) INTO a_running_job_count 
    FROM cs_jobs
    WHERE end_stamp IS NULL;
    IF a_running_job_count > 0 THEN
        COMMIT; -- ロックを解放します。(3)
        raise_application_error(-20000, 'Unable to create a new job: a job is currently running.');
    END IF;
    DELETE FROM cs_active_job;
    INSERT INTO cs_active_job(job_id) VALUES (v_job_id);
    BEGIN
        INSERT INTO cs_jobs (job_id, start_stamp) VALUES (v_job_id, sysdate);
        EXCEPTION WHEN dup_val_on_index THEN NULL; -- すでに存在していても問題ありません。(4)
    END;
    COMMIT;
END;
/
show errors
このような手続きはINTEGER型を返す PostgreSQL の関数に簡単に変換することができます。これは以下のような事を教えてくれることもあってこの手続きは特におもしろいです。
それでは、このプロシージャを PL/pgSQL に移植することができた方法の1つを見てみましょう。
CREATE OR REPLACE FUNCTION cs_create_job(INTEGER) RETURNS INTEGER AS '
DECLARE
    v_job_id ALIAS FOR $1;
    a_running_job_count INTEGER;
    a_num INTEGER;
    -- PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
    LOCK TABLE cs_jobs IN EXCLUSIVE MODE;
    SELECT count(*) INTO a_running_job_count 
    FROM cs_jobs 
    WHERE end_stamp IS NULL;
    IF a_running_job_count > 0
    THEN
        -- COMMIT; -- ロックを解放します。
        RAISE EXCEPTION ''Unable to create a new job: a job is currently running.'';
    END IF;
    DELETE FROM cs_active_job;
    INSERT INTO cs_active_job(job_id) VALUES (v_job_id);
    SELECT count(*) into a_num 
    FROM cs_jobs 
    WHERE job_id=v_job_id;
    IF NOT FOUND THEN  -- 最後の問い合わせで何も返らなかった場合
        -- このジョブはテーブル内にありませんので、それを挿入します。
        INSERT INTO cs_jobs(job_id, start_stamp) VALUES (v_job_id, sysdate());
        RETURN 1;
    ELSE
        RAISE NOTICE ''Job already running.'';(1)
    END IF;
    RETURN 0;
END;
' LANGUAGE 'plpgsql';
Note: 私はあまりパッケージを使ったことがないので、間違いがありましたら、私に知らせてください。
パッケージはオラクルが、Javaクラスのように、1つの実体に PL/SQL 文と関数をカプセル化するための方法です。 ここではメソッドとオブジェクトの定義をします。 これらはオブジェクト/メソッドに"."(ドット)を使ってアクセスすることができます。ここに ACS 4 のオラクルのパッケージの例があります。 ArsDigita Community System)
CREATE OR REPLACE PACKAGE BODY acs
AS
  FUNCTION add_user (
    user_id     IN users.user_id%TYPE DEFAULT NULL,
    object_type     IN acs_objects.object_type%TYPE DEFAULT 'user',
    creation_date   IN acs_objects.creation_date%TYPE DEFAULT sysdate,
    creation_user   IN acs_objects.creation_user%TYPE DEFAULT NULL,
    creation_ip     IN acs_objects.creation_ip%TYPE DEFAULT NULL,
  ...
  ) RETURN users.user_id%TYPE
  IS
    v_user_id       users.user_id%TYPE;
    v_rel_id        membership_rels.rel_id%TYPE;
  BEGIN
    v_user_id := acs_user.new (user_id, object_type, creation_date,
                creation_user, creation_ip, email, ...
    RETURN v_user_id;
  END;
END acs;
/
show errors
標準的な命令規則を持つ関数としてオラクル・パッケージの異なるオブジェクトを作成することでこれを PostgreSQL に移植します。 PostgreSQL 関数にはデフォルトパラメータが欠乏しているなどのいくつかの細かいことに注意をはらわなければいけません、上記のパッケージはこのようなものになるでしょう。
CREATE FUNCTION acs__add_user(INTEGER,INTEGER,VARCHAR,TIMESTAMP,INTEGER,INTEGER,...)
RETURNS INTEGER AS '
DECLARE
    user_id ALIAS FOR $1;
    object_type ALIAS FOR $2;
    creation_date ALIAS FOR $3;
    creation_user ALIAS FOR $4;
    creation_ip ALIAS FOR $5;
    ...
    v_user_id users.user_id%TYPE;
    v_rel_id membership_rels.rel_id%TYPE;
BEGIN
    v_user_id := acs_user__new(user_id,object_type,creation_date,creation_user,creation_ip, ...);
    ...
    RETURN v_user_id;
END;
' LANGUAGE 'plpgsql';
PostgreSQL の EXECUTEは良くできています。 しかしSection 19.5.4で説明されている quote_literal(TEXT) と quote_string(TEXT) を使うことを覚えておかなければいけません。これらの関数を使用しない限りEXECUTE ''SELECT * from $1'';という構文は動作しません。
PostgreSQL は実行を最適化するために2つの関数生成修飾子があります。 iscachable (同じ引数が与えられた場合常に同じ結果が返します) と isstrict (引数のいずれかに NULL が含まれる場合 NULL を返します). 詳細は CREATE FUNCTION を参照して下さい。
これらの最適化属性を利用するためには、CREATE FUNCTION 文にWITH 修飾子を使用しなければいけません。以下に例を示します。
CREATE FUNCTION foo(...) RETURNS INTEGER AS ' ... ' LANGUAGE 'plpgsql' WITH (isstrict, iscachable);
--
-- Oracle のものと同じ動作をする instr 関数
-- 構文 :instr(string1,string2,[n],[m]) ただし、[]は省略可能なパラメータ
-- 
-- string1 内の n番目の文字からm番目の文字までで string2 を探します。
-- n が負の場合、逆方向に検索します。 mが渡されなかった場合は、1 と 
-- みなします(最初の文字から検索を始めます)。
--
-- 作者 : Roberto Mello (rmello@fslc.usu.edu)
-- 更新 : Robert Gaszewski (graszew@poland.com)
-- GPL v2以降によるライセンス
--
CREATE FUNCTION instr(VARCHAR,VARCHAR) RETURNS INTEGER AS '
DECLARE
    pos integer;
BEGIN
    pos:= instr($1,$2,1);
    RETURN pos;
END;
' LANGUAGE 'plpgsql';
CREATE FUNCTION instr(VARCHAR,VARCHAR,INTEGER) RETURNS INTEGER AS '
DECLARE
    string ALIAS FOR $1;
    string_to_search ALIAS FOR $2;
    beg_index ALIAS FOR $3;
    pos integer NOT NULL DEFAULT 0;
    temp_str VARCHAR;
    beg INTEGER;
    length INTEGER;
    ss_length INTEGER;
BEGIN
    IF beg_index > 0 THEN
       temp_str := substring(string FROM beg_index);
       pos := position(string_to_search IN temp_str);
       IF pos = 0 THEN
	         RETURN 0;
	     ELSE
	         RETURN pos + beg_index - 1;
	     END IF;
    ELSE
       ss_length := char_length(string_to_search);
       length := char_length(string);
       beg := length + beg_index - ss_length + 2;
       WHILE beg > 0 LOOP
           temp_str := substring(string FROM beg FOR ss_length);
	         pos := position(string_to_search IN temp_str);
	         IF pos > 0 THEN
		           RETURN beg;
	         END IF;
	         beg := beg - 1;
       END LOOP;
       RETURN 0;
    END IF;
END;
' LANGUAGE 'plpgsql';
--
-- 作者 : Robert Gaszewski (graszew@poland.com)
-- GPL v2以降によるライセンス
--
CREATE FUNCTION instr(VARCHAR,VARCHAR,INTEGER,INTEGER) RETURNS INTEGER AS '
DECLARE
    string ALIAS FOR $1;
    string_to_search ALIAS FOR $2;
    beg_index ALIAS FOR $3;
    occur_index ALIAS FOR $4;
    pos integer NOT NULL DEFAULT 0;
    occur_number INTEGER NOT NULL DEFAULT 0;
    temp_str VARCHAR;
    beg INTEGER;
    i INTEGER;
    length INTEGER;
    ss_length INTEGER;
BEGIN
    IF beg_index > 0 THEN
        beg := beg_index;
        temp_str := substring(string FROM beg_index);
        FOR i IN 1..occur_index LOOP
            pos := position(string_to_search IN temp_str);
            IF i = 1 THEN
                beg := beg + pos - 1;
            ELSE
                beg := beg + pos;
            END IF;
            temp_str := substring(string FROM beg + 1);
        END LOOP;
        IF pos = 0 THEN
            RETURN 0;
        ELSE
            RETURN beg;
        END IF;
    ELSE
        ss_length := char_length(string_to_search);
        length := char_length(string);
        beg := length + beg_index - ss_length + 2;
        WHILE beg > 0 LOOP
            temp_str := substring(string FROM beg FOR ss_length);
            pos := position(string_to_search IN temp_str);
            IF pos > 0 THEN
                occur_number := occur_number + 1;
                IF occur_number = occur_index THEN
                    RETURN beg;
                END IF;
            END IF;
            beg := beg - 1;
        END LOOP;
        RETURN 0;
    END IF;
END;
' LANGUAGE 'plpgsql';