象と戯れ

2012-12-09

PL/v8は最もリッチなプロシージャ言語に。

23:44 | PL/v8は最もリッチなプロシージャ言語に。 - 象と戯れ を含むブックマーク はてなブックマーク - PL/v8は最もリッチなプロシージャ言語に。 - 象と戯れ

この記事はPostgreSQL Advent Calendar 2012の9日目だと思います。

PL/v8のことはもうご存知でしょうか。JavaScript実行エンジンのV8を組み込んだプロシージャ言語で、JavaScriptPostgreSQLを拡張することができます。PostgreSQL9.2ではJSON型がビルトインとしてサポートされたこともあり、PostgreSQL本体のリリース文の中に言及があるなど、こちらの知らないところで宣伝されていたりしてこっちがびっくりするときもしばしば。先日日本で開催されたHeroku Meetup #7に来ていたはずのWillもあっちこっちでPL/v8の宣伝してますね。

さて、つい先ほどPGXNにて1.3.01.2.1をリリースしました。1.2.1は1.2.0からのバグフィックス、1.3.0は新機能を盛り込んだバージョンになります。あまり日本語で機能全体を紹介したことがないので概要を公式ドキュメントからの抜粋で紹介します。

スカラユーザ定義関数

いわゆる普通のユーザ定義関数をサポートします。SELECT foo() FROM bar;といった使い方ですね。下記のような感じです。

CREATE FUNCTION plv8_test(keys text[], vals text[]) RETURNS text AS $$
    var o = {};
    for(var i=0; i<keys.length; i++){
        o[keys[i]] = vals[i];
    }
    return JSON.stringify(o);
$$ LANGUAGE plv8 IMMUTABLE STRICT;

SELECT plv8_test(ARRAY['name', 'age'], ARRAY['Tom', '29']);
SELECT plv8_test(ARRAY['name', 'age'], ARRAY['Tom', '29']);
         plv8_test        
---------------------------
 {"name":"Tom","age":"29"}
(1 row)

行を返す関数

Set Returning Functionというやつです。generate_series()の類いですね。この場合はplv8.return_next()によって行を返します。

CREATE TYPE rec AS (i integer, t text);
CREATE FUNCTION set_of_records() RETURNS SETOF rec AS
$$
    // plv8.return_next() stores records in an internal tuplestore,
    // and return all of them at the end of function.
    plv8.return_next( { "i": 1, "t": "a" } );
    plv8.return_next( { "i": 2, "t": "b" } );

    // You can also return records with an array of JSON.
    return [ { "i": 3, "t": "c" }, { "i": 4, "t": "d" } ];
$$
LANGUAGE plv8;

SELECT * FROM set_of_records();
 i | t
---+---
 1 | a
 2 | b
 3 | c
 4 | d
(4 rows)

トリガのサポート

トリガもかけます。PL/pgSQLと同様の変数を利用することができます。

CREATE FUNCTION test_trigger() RETURNS trigger AS
$$
    plv8.elog(NOTICE, "NEW = ", JSON.stringify(NEW));
    plv8.elog(NOTICE, "OLD = ", JSON.stringify(OLD));
    plv8.elog(NOTICE, "TG_OP = ", TG_OP);
    plv8.elog(NOTICE, "TG_ARGV = ", TG_ARGV);
    if (TG_OP == "UPDATE") {
        NEW.i = 102;
        return NEW;
    }
$$
LANGUAGE "plv8";

CREATE TRIGGER test_trigger
    BEFORE INSERT OR UPDATE OR DELETE
    ON test_tbl FOR EACH ROW
    EXECUTE PROCEDURE test_trigger('foo', 'bar');

インライン関数のサポート

9.0以降でDO文で呼び出す関数をサポートしています。

DO $$ plv8.elog(NOTICE, 'this', 'is', 'inline', 'code') $$ LANGUAGE plv8;

データベースのデータ型に応じて自動的にJSのデータ型に変換

引数と戻り値のデータ型に応じて適当なJSのデータ型に変換します。

  • oid
  • bool
  • int2
  • int4
  • int8
  • float4
  • float8
  • numeric
  • date
  • timestamp
  • timestamptz
  • bytea
  • json (>= 9.2)

timestamptzまでは想像に難くないと思います。1.3.0ではbyteaとJSONを追加し、byteaは後ほど紹介するTypedArrayによって符号なし1バイト整数配列に変換されます。9.2以降のJSON型についてはJSON.parse()/stringify()を呼び出してJavaScriptのオブジェクトに変換されます。

プリペアドステートメントとカーソルを含むSPI周りのサポート

PL/pythonやPL/perlと同様にSPIによるデータベースアクセスが利用できます。基本的にはplv8.execute()によりSQLが発行できるのと、plv8.prepare()によってPlanオブジェクトを取得、fetch()やcursor()を呼び出す方法があります。

var json_result = plv8.execute( 'SELECT * FROM tbl' );
var num_affected = plv8.execute( 'DELETE FROM tbl WHERE price > $1', [ 1000 ] );
var plan = plv8.prepare( 'SELECT * FROM tbl WHERE col = $1', ['int'] );
var rows = plan.execute( [1] );
var sum = 0;
for (var i = 0; i < rows.length; i++) {
  sum += rows[i].num;
}
plan.free();

return sum;

サブトランザクション

SPI関連でサブトランザクションが自動的に発行されます。関数の途中でplv8.execute()が失敗しても、try/catchで復帰してそれまでのデータベースへの変更をロールバックした後に処理を継続することができます。

try{
  plv8.subtransaction(function(){
    plv8.execute("INSERT INTO tbl VALUES(1)");  -- should be rolled back!
    plv8.execute("INSERT INTO tbl VALUES(1/0)");-- occurs an exception
  });
} catch(e) {
  ... do fall back plan ...
}

ユーティリティ関数

plv8オブジェクト経由で各種ユーティリティ関数が提供されています。

  • plv8.elog(elevel, msg1[, msg2, ...])
  • plv8.quote_literal(str)
  • plv8.nullable(str)
  • plv8.quote_ident(str)

こちらはほぼ標準てきな内容です。さらに、plv8.find_functionを利用して別のユーザ定義関数をJS関数としてロードすることができます。

CREATE FUNCTION callee(a int) RETURNS int AS $$ return a * a $$ LANGUAGE plv8;
CREATE FUNCTION caller(a int, t int) RETURNS int AS $$
  var func = plv8.find_function("callee");
  return func(a);
$$ LANGUAGE plv8;

PL/v8はtrusted languageであるため、データベース外のリソース(ファイルシステム、ネットワーク)へのアクセスは制限されていますので、こうしたコードの再利用はfind_functionを利用すると便利です。

ウィンドウ関数API

1.3.0よりウィンドウ関数APIを利用できるのでユーザ定義ウィンドウ関数をPL/v8で作成することができます。ウィンドウ関数内ではplv8.get_window_object()を呼び出してウィンドウオブジェクトを取得し、APIを呼び出すことができます。

var winobj = plv8.get_window_object();
return winobj.get_current_position() + 1;

TypedArray

V8には変態的な機能があって、メモリの内容を直に配列化することができます。これはもともとブラウザのCanvasのサポートでピクセルメモリを高速に処理するために用意されているのですが、PL/v8ではこれを利用してbytea及び一部の配列をJSの配列に変換します。byteaの列に任意のバイナリデータを保存して、面倒なCユーザ定義関数を書く変わりにJSで処理してしまうことも可能です。また、int2/int4/float4/float8についてはドメイン型plv8_int2array/plv8_int4array/plv8_float4array/plv8_float8arrayが用意されており、これらを引数として与えると一次元でNULLを含まない配列に対して高速なメモリアクセスが可能です。

create table foo(i, ary) as
  select i, ary from(select array_agg(a) ary
  from generate_series(1, 100000)a)a, generate_series(1, 100)i;

create or replace function int4sum(ary plv8_int4array) returns int8 as $$
  var sum = 0;
  for (var i = 0; i < ary.length; i++) {
    sum += i;
  }
  return sum;
$$ language plv8 immutable strict;

select int4sum(ary) from foo;
Time: 48.882 ms

-- plv8_int4arrayのかわりにint4[]を使った場合
Time: 820.785 ms

もうC言語関数いらないかもしれませんね。

リモートデバッガ

1.3.0からはd8を利用したデバッガを利用することができます。make時にENABLE_DEBUGGER_SUPPORTを定義するとデバッガ用のポートを開きます。デフォルトは35432番で待っています。

$ d8 --remote-debugger --debugger-port=35432

Node.jsなどではお馴染みのようです。

実行環境とユーザの隔離について

その他のPLでも同様の問題があるように、V8についてもユーザを切り替えた際に実行環境が混じらないように、それぞれのユーザごとにV8実行環境が作成されます。SET ROLEコマンドを使って同一接続中にユーザを切り替えた場合はグローバル変数などが別に定義されます。

スタートアップ処理

plv8.start_procに定義済みのJS関数名を記述することにより、毎回のJSエンジン初期化時にその関数が呼び出されます。これはグローバル変数/関数の準備などをするために有用です。

SET plv8.start_proc = 'plv8_init';
SELECT plv8_test(10);

JS方言のサポート

現在のところ、CoffeeScriptとLiveScriptが利用可能です。それぞれ別のExtensionになっていますので、利用したい場合はCREATE EXTENSIONを呼び出してください。特にLiveScriptについては、かのPugsを開発者Audrey Tangからパッチをもらっています。

----

さていかがでしょうか。PostgreSQLに標準でついてくるPL/pythonなどに比べても機能的に足りないところはなく、ウィンドウ関数やデバッガの機能などは寧ろ勝っていると言えるでしょう。またPL/v8の重要な特性の一つとしてtrusted languageがあり、セキュリティ上の理由からPL/pgSQLしか利用できなかった場面でもPL/v8が利用できます。特にPaaSなどではここが重要になるかもしれません。しかしPL/pgSQLに対して処理速度は圧倒的でありますので、よほどの理由(Oracleポーティングとか?)でないかぎりPL/pgSQLでないとだめという理由は見当たりません。強いて言えばJSには標準ライブラリ的なものはないので特有の問題を既存のライブラリで解決するなどのときはPL/pythonの方がいいとは思います。

というわけで、外様PLながらPL/v8は最もリッチなプロシージャ言語になってしまいました。本当はFDWとかも書けるようにしたいのですが、いかんせんネットワーク処理はできないのであまり有用ではないと思われます。その他、他に不足する部分があればどしどしIssueListにご応募ください。

Enjoy!