mruby Advent Calendar 2015 の12日分の記事です。(12日に投稿したとは言っていない)
PostgreSQLでmrubyを用いたユーザー定義関数の実装を可能にするplmrubyの開発を進めています。plmrubyによってデータベース内での実行が必要となる複雑なロジックを、mrubyの豊かな表現力を用いて記述することが可能になります。
plmrubyはWork In Progressであり安定版はリリースされていませんが、プロシージャ言語としての基本的な機能はある程度実装済みなので、以下でその機能を紹介します。
実装済み機能
スカラ関数
一般的な関数です。複数の値を引数として、一つの値を返します。
CREATE FUNCTION plmruby_test(keys text[], vals text[]) RETURNS text AS
$$
h = Hash.new
keys.zip(vals).each do |k,v|
h[k] = v
end
JSON.stringify(h);
$$
LANGUAGE plmruby IMMUTABLE STRICT;
SELECT plmruby_test(ARRAY['name', 'age'], ARRAY['Tom', '29']);
plmruby_test
---------------------------
{"name":"Tom","age":"29"}
(1 row)
SETOF関数
PostgreSQLでは一回の関数で呼び出しで複数の行を返すことができます。標準ではgenerate_seriesなどが提供されています。
plmrubyはこのような集合を返す関数(SETOF関数)を定義することができます。
CREATE TYPE rec AS (i integer, t text);
CREATE FUNCTION set_of_records_array() RETURNS SETOF rec AS
$$
[
{i: 1, t: "a"},
{i: 2, t: "b"},
{i: 3, t: "c"},
]
$$
LANGUAGE plmruby;
SELECT * FROM set_of_records_array();
i | t
---+---
1 | a
2 | b
3 | c
(3 rows)
配列以外にもeachが実装していればそれを利用するようにしているので、eachを実装したクラスやEnumeratorで実装することもできます。
CREATE FUNCTION set_of_records_enumerable() RETURNS SETOF rec AS
$$
cls = Class.new do
ARY = [
{i: 1, t: "a"},
{i: 2, t: "b"},
{i: 3, t: "c"},
]
def each
ARY.each {|e| yield e}
end
end
cls.new
$$
LANGUAGE plmruby;
SELECT * FROM set_of_records_enumerable();
i | t
---+---
1 | a
2 | b
3 | c
(3 rows)
CREATE FUNCTION set_of_records_enumerator() RETURNS SETOF rec AS
$$
Enumerator.new do |y|
ARY = [
{i: 1, t: "a"},
{i: 2, t: "b"},
{i: 3, t: "c"},
]
ARY.each {|e| y << e}
end
$$
LANGUAGE plmruby;
SELECT * FROM set_of_records_enumerator();
i | t
---+---
1 | a
2 | b
3 | c
(3 rows)
トリガ関数
トリガで実行される内容をmrubyで実装できます。
CREATE TABLE test_tbl (i int4, s text);
INSERT INTO test_tbl VALUES(1, 'abc');
CREATE VIEW test_view AS SELECT i FROM test_tbl;
CREATE FUNCTION test_trigger_arguments() RETURNS trigger AS
$$
elog(NOTICE, "new = ", JSON.stringify(new))
elog(NOTICE, "old = ", JSON.stringify(old))
elog(NOTICE, "tg_name = ", tg_name)
elog(NOTICE, "tg_when = ", tg_when)
elog(NOTICE, "tg_level = ", tg_level)
elog(NOTICE, "tg_op = ", tg_op)
#elog(NOTICE, "tg_relid = ", tg_relid)
elog(NOTICE, "tg_table_name = ", tg_table_name)
elog(NOTICE, "tg_table_schema = ", tg_table_schema)
elog(NOTICE, "tg_argv = ", tg_argv)
$$
LANGUAGE plmruby;
-- BEFORE
CREATE TRIGGER test_trigger_arguments_before
BEFORE INSERT OR UPDATE OR DELETE
ON test_tbl FOR EACH ROW
EXECUTE PROCEDURE test_trigger_arguments('foo', 'bar');
INSERT INTO test_tbl VALUES(100, 'ABC');
NOTICE: new = {"i":100,"s":"ABC"}
NOTICE: old = null
NOTICE: tg_name = test_trigger_arguments_before
NOTICE: tg_when = :before
NOTICE: tg_level = :row
NOTICE: tg_op = :insert
NOTICE: tg_table_name = test_tbl
NOTICE: tg_table_schema = public
NOTICE: tg_argv = ["foo", "bar"]
インライン関数
PostgreSQLでは9.0からDO構文によって、事前に定義することなく関数を実行することが可能です。plmrubyではこれに対応しています。
DO $$ elog(NOTICE, 'this', 'is', 'inline', 'code') $$ LANGUAGE plmruby;
NOTICE: this is inline code
型の自動変換
PostgreSQLの型とmrubyの型を、関数を呼び出す時と返り値を戻す時にある程度自動的にいい感じに変換します。これによってPostgreSQLでの型をそれ程意識することなくmruby側で利用することが可能です。数値や文字列、タイムスタンプをもちろん、配列やレコード型についても対応しており、さらにJSON型についても、mattn/mruby-jsonを利用することでmruby内ではパース済みの状態で扱えるようにしています。
具体的な型の対応はREADMEを参照してください。
-- array
CREATE FUNCTION plmruby_array_in(v int4[]) RETURNS void AS $$
elog(INFO, v)
$$ LANGUAGE plmruby IMMUTABLE STRICT;
SELECT plmruby_array_in(ARRAY[1,2,3]::int4[]);
INFO: [1, 2, 3]
plmruby_array_in
------------------
(1 row)
CREATE FUNCTION plmruby_anyarray_in(v anyarray) RETURNS void AS $$
elog(INFO, v)
$$ LANGUAGE plmruby IMMUTABLE STRICT;
SELECT plmruby_anyarray_in(ARRAY['a','b','c']::text[]);
INFO: ["a", "b", "c"]
plmruby_anyarray_in
---------------------
(1 row)
CREATE FUNCTION plmruby_array_out() RETURNS int4[] AS $$
[1,2,3]
$$ LANGUAGE plmruby IMMUTABLE STRICT;
SELECT plmruby_array_out();
plmruby_array_out
-------------------
{1,2,3}
(1 row)
-- json
CREATE FUNCTION plmruby_json_in(v json) RETURNS void AS $$
elog(INFO, v)
$$ LANGUAGE plmruby IMMUTABLE STRICT;
SELECT plmruby_json_in('{"a":1, "b":"c"}'::json);
INFO: {"a"=>1, "b"=>"c"}
plmruby_json_in
-----------------
(1 row)
SELECT plmruby_json_in('[1,2,3]'::json);
INFO: [1, 2, 3]
plmruby_json_in
-----------------
(1 row)
CREATE FUNCTION plmruby_json_out() RETURNS json AS $$
JSON.parse('{"a":1, "b":"c"}')
$$ LANGUAGE plmruby IMMUTABLE STRICT;
SELECT plmruby_json_out();
plmruby_json_out
------------------
{"a":1,"b":"c"}
(1 row)
ユーザー毎のVMの分離
効率を考えると関数の実行ごとにVMを立ち上げるのではなく、一度立ち上げたVMを再利用することになります。その場合VM内に状態を持つことになるので、セキュリティを考えると異なるユーザー間でVMの状態が共有されることは望ましくありません。その為、ユーザー毎にVMを立ち上げて利用するようにしています。
また、実行ごとの環境もなるべく分離できるように、関数の定義毎に以下の様なクラスをVM内に定義し、実行時にインスタンス化しcallメソッドを呼び出します。インスタンスはトランザクションの終了時に廃棄され使いまわされません。
class PLMRUBY_<fn_oid>
def call(<arg1 ,...>
<prosrc>
end
end
Why mruby?
なぜmrubyでプロシージャ言語を提供するかについては、以下のような理由が挙げられます。
- 組み込みが容易
- 実装がシンプルになりメンテナンスしやすい
- 複数のVMの立ち上げも簡単
- プロシージャ言語はそもそも複雑なロジックを記述する際に利用したいものなので、言語自体の記述力が高いものが好ましい
- mrbgemによって拡張をsoに組み込める
- gemやpipのような仕組みはプロシージャ言語としてDB内に組み込む場合には利用しづらい。機能が後から追加できてしまうのはセキュリティ的にも問題。
TODO
機能としては以下のようなものは提供したいなと考えています。
- SPIのサポート
- Window関数
- サブトランザクション
- Trusted Language
しかし、リリースに向けては機能の拡充よりも以下のようなものが必要になってくると思います。気が向いたら誰か手伝ってください。
- 安定化
- ドキュメントの充実