pg_xnodeを使ってみた

こんにちは。ぬこ@横浜(@nuko_yokoham)です。
このエントリはPostgreSQL Advent Calendar 2012 : ATNDの12/23用として登録しました(23日中に投稿できなかったのは私が怠けていたからです・・・すいません)。

pg_xnodeとは

pg_xnodeとは、簡単に言えばXML型を操作する拡張機能です。
PostgreSQLのコア機能としても(--with-libxml付きでconfigureを行えば)XML型、XML構築関数群、そしてXPath関数は使用できますが、pg_xnodeは幾つかの特徴的な機能を持っています。

  • XML文書だけでなく文書内のノード(サブツリー)を扱うことができる。
  • XML文書内へのノードの追加、XML文書内からのノードの削除が可能。

あと、実装上面白いのは、このEXTENSIONがサードパーティーライブラリに依存していないというところでしょうか。XMLを処理するのであれば、libxml2、あるいはXerces-C、SAXONなどのXMLプロセッサライブラリを使うのが通例でしょうけど、どうやらこのEXTENSIONではXML処理、XPath処理を自前で実装しているようです。

pg_xnodeのビルド

pg_xnodeのサイトへアクセスしてダウンロードページへ移ります。ダウンロードページは現状、2つのリンクが置かれてますが、今回は"Source package of the current release can be downloaded"のほうのリンクを選択して pg_xnode-0.7.2.tar.gz をダウンロードします。
tar.gzファイルをPostgreSQLがインストールされているマシン上の任意の場所で展開し、make USE_PGXS=1でビルドします。

$ ls -l pg_xnode-0.7.2.tar.gz 
-rwxrw-rw-. 1 harada harada 151809 11月  8 02:43 2012 pg_xnode-0.7.2.tar.gz
$ tar xfz pg_xnode-0.7.2.tar.gz 
$ cd pg_xnode-0.7.2
$ make USE_PGXS=1
・・・(makeのログ)
$ make USE_PGXS=1 install
・・・(make installのログ)

うちの環境だと数箇所gccから警告メッセージが出力されましたが、ビルド自体はできたのでとりあえず放置しておきます。;-)
PostgreSQLサーバが起動していれば、make installcheckでテストもできます。

$ make USE_PGXS=1 installcheck
make -C src installcheck
make[1]: ディレクトリ `/home/harada/src/pg_xnode-0.7.2/src' に入ります
/home/harada/pgsql-9.2.2/lib/pgxs/src/makefiles/../../src/test/regress/pg_regress --inputdir=. --psqldir='/home/harada/pgsql-9.2.2/bin'   --user=postgres --dbname=contrib_regression xnode update xnt
(using postmaster on Unix socket, default port)
============== dropping database "contrib_regression" ==============
DROP DATABASE
============== creating database "contrib_regression" ==============
CREATE DATABASE
ALTER DATABASE
============== running regression test queries        ==============
test xnode                    ... ok
test update                   ... ok
test xnt                      ... ok

=====================
 All 3 tests passed. 
=====================

これでpg_xnode EXTENSIONがデータベースにインストール可能になりました。
データベースにpg_xnodeをインストールします。

xdb=# CREATE EXTENSION xnode ;
CREATE EXTENSION
xdb=# \dx
                   List of installed extensions
  Name   | Version |   Schema   |           Description            
---------+---------+------------+----------------------------------
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
 xnode   | 0.7.2   | xml        | Implementation of XML using DOM.
(2 rows)

xdb=# 

XMLデータの格納と参照

まず、pg_xnodeへデータを格納してみます。以下のようにテーブルを定義します。

xdb=# CREATE TABLE test (data1 xml.doc, data2 xml);
CREATE TABLE
xdb=# \d test
     Table "public.test"
 Column |  Type   | Modifiers 
--------+---------+-----------
 data1  | xml.doc | 
 data2  | xml     | 

xdb=# 

data1の型 xml.doc がpg_xnodeが提供するXML文書格納用の型です。今回は比較のためにPostgreSQL組み込みのXML型のカラムも定義してみました。
まずINSERTで挿入して、SELECTで参照してみます。

xdb=# INSERT INTO test VALUES ('<a><b><c>c01</c><d>d01</d></b></a>','<a><b><c>c01</c><d>d01</d></b></a>');
INSERT 0 1
xdb=# INSERT INTO test VALUES ('<ns1:a xmlns:ns1="http://foo.bar"><ns1:b><ns1:c>c01</ns1:c><ns1:d>d01</ns1:d></ns1:b></ns1:a>', '<ns1:a xmlns:ns1="http://foo.bar"><ns1:b><ns1:c>c01</ns1:c><ns1:d>d01</ns1:d></ns1:b></ns1:a>');
INSERT 0 1
xdb=# INSERT INTO test VALUES ('<あ><い><う>う01</う><え>え01</え></い></あ>','<あ><い><う>う01</う><え>え01</え></い></あ>');;
INSERT 0 1
xdb=# \x
Expanded display is on.
xdb=# SELECT * FROM test;
-[ RECORD 1 ]----------------------------------------------------------------------------------------
data1 | <a><b><c>c01</c><d>d01</d></b></a>
data2 | <a><b><c>c01</c><d>d01</d></b></a>
-[ RECORD 2 ]----------------------------------------------------------------------------------------
data1 | <ns1:a xmlns:ns1="http://foo.bar"><ns1:b><ns1:c>c01</ns1:c><ns1:d>d01</ns1:d></ns1:b></ns1:a>
data2 | <ns1:a xmlns:ns1="http://foo.bar"><ns1:b><ns1:c>c01</ns1:c><ns1:d>d01</ns1:d></ns1:b></ns1:a>
-[ RECORD 3 ]----------------------------------------------------------------------------------------
data1 | <あ><い><う>う01</う><え>え01</え></い></あ>
data2 | <あ><い><う>う01</う><え>え01</え></い></あ>

xdb=# 

名前空間ありの文書も日本語の利用も問題はなさそうに見えます(が、後述のように名前空間はきちんと対応できていないかも)。

XPathによる取り出し

次にXPathで中間ノードを取り出してみます。pg_xnodeでは xml.path() を使います。

xdb=# SELECT xml.path('/a/b', data1) FROM test;
            path             
-----------------------------
 <b><c>c01</c><d>d01</d></b>
 
 
(3 rows)
xdb=# 

このパスの場合、最初の行の文書しかヒットしないのですが、残りの2行の文書が空白なのかNULLなのかが分からないので\psetでNULL値を設定して再検証してみます。

xdb=# \pset null (null)
Null display is "(null)".
xdb=# SELECT xml.path('/a/b', data1) FROM test;
            path             
-----------------------------
 <b><c>c01</c><d>d01</d></b>
 (null)
 (null)
(3 rows)

xdb=#

どうやら xml.path() ではヒットしなかった場合、NULLと評価されるようです。これは組み込みのxpath関数とは異なるのでちょっと注意が必要です。組み込みのxpath関数の場合は、ヒットしない場合以下のように空の配列として評価されます。

xdb=# SELECT xpath('/a/b', data2) FROM test;
    xpath     
--------------
 {"<b>       +
   <c>c01</c>+
   <d>d01</d>+
 </b>"}
 {}
 {}
(3 rows)

xdb=# 

別のpath関数

pg_xnodeにはもうひとつ別形式のpath関数があります。
このpath関数は

を引数とし、ベースパス+ベースパスからの相対パスにヒットするノードを配列として返却します。

xdb=# SELECT xml.path('/a/b', '{"c", "d"}', data1) FROM test;
          path           
-------------------------
 {<c>c01</c>,<d>d01</d>}
(1 row)

xdb=# 

ここで注目すべきは結果が1行になっているところです。どうやらベースパスに合致しない文書はレコードとしても生成されない挙動になっているようです。ここも組み込みのxpathとは大きく異るところでしょう。

名前空間

最初に文書を登録したときには気づかなかったのですが、現状のpg_xnodeではまだ名前空間をきちんと扱えないようです。
PostgreSQL組み込みのxpath関数では、第3引数に名前空間接頭辞と名前空間URLの組みを指定できるので、以下のように任意の名前空間接頭辞と名前空間URLを関連づけられます。

xdb=# SELECT xpath('/ns1:a/ns1:b/ns1:c', '<ns1:a xmlns:ns1="http://foo" xmlns:ns2="http://bar"><ns1:b><ns1:c>c01</ns1:c><ns2:c>c02</ns2:c></ns1:b></ns1:a>', ARRAY[['ns1','http://foo'],['ns2','http://bar']]);
        xpath         
----------------------
 {<ns1:c>c01</ns1:c>}
(1 row)

xdb=# SELECT xpath('/foo:a/foo:b/foo:c', '<ns1:a xmlns:ns1="http://foo" xmlns:ns2="http://bar"><ns1:b><ns1:c>c01</ns1:c><ns2:c>c02</ns2:c></ns1:b></ns1:a>', ARRAY[['foo','http://foo'],['bar','http://bar']]);
        xpath         
----------------------
 {<ns1:c>c01</ns1:c>}
(1 row)

xdb=# 

が、pg_xnodeの場合には名前空間の指定がそもそもできないので、上記のような指定はできません。

xdb=# SELECT xml.path('/ns1:a/ns1:b/ns1:c', '<ns1:a xmlns:ns1="http://foo" xmlns:ns2="http://bar"><ns1:b><ns1:c>c01</ns1:c><ns2:c>c02</ns2:c></ns1:b></ns1:a>');
        path        
--------------------
 <ns1:c>c01</ns1:c>
(1 row)

xdb=# SELECT xml.path('/foo:a/foo:b/foo:c', '<ns1:a xmlns:ns1="http://foo" xmlns:ns2="http://bar"><ns1:b><ns1:c>c01</ns1:c><ns2:c>c02</ns2:c></ns1:b></ns1:a>'); 
  path  
--------
 (null)
(1 row)

xdb=# 

まあ、このあたりは今後改善されていくのではないかと思いますが・・・。

おわりに

今回はここまでですが、今後、文書内へのノード挿入・ノード削除、検索結果とテンプレートからXMLを生成する機能なども検証していきたいと思います。

おまけ:pg_xnodeのバイナリ形式

バイナリ形式はどうなっているのかな・・・?と思ってバイナリ入出力関数があるかどうか確認したが

CREATE TYPE doc (
        internallength = variable,
        input = doc_in,
        output = doc_out,
        alignment = int,
        storage = extended
);

残念ながらサポートされていない。なので、自分でバイナリ出力関数を追加してみました。
byteasendを真似て、src/xmlnode.cに以下のようなコードを追加し

PG_FUNCTION_INFO_V1(xmldoc_send);
Datum xmldoc_send(PG_FUNCTION_ARGS) {
    bytea          *vlena = PG_GETARG_BYTEA_P_COPY(0);

    PG_RETURN_BYTEA_P(vlena);
}

SQLファイルに関数登録し、doc型にsend定義を追加。

CREATE FUNCTION doc_send(doc) RETURNS bytea
        as 'MODULE_PATHNAME', 'xmldoc_send'
        LANGUAGE C
        IMMUTABLE
        STRICT;

CREATE TYPE doc (
        internallength = variable,
        input = doc_in,
        output = doc_out,
        send = doc_send,
        alignment = int,
        storage = extended
);

再度EXTENSIONを登録して実験。

xnodedb=# SELECT data, xml.doc_send(data) FROM test;
-[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------
data     | <a><b><c>c01</c><d>d01</d></b></a>
doc_send | \x070063303100020001000663000700643031000002000100076400000200020016086200020001000861000000000100080000002c000000

xnodedb=# 

なんかバイナリ形式が出力されましたねw
"633031"や"643031"あたりがテキストノード"c01","d01"の部分だと思います。ちなみに長さは56バイトなので格納効率的にはそんなに良くはなさそうですが、このあたりのバイナリ解析も今後余裕があれば見ておきたいです。