ファイヤープロジェクト
ユーザ定義データ型
2004-01-04T18:00+09:00   matsu
PostgreSQLではユーザがデータ型を定義することができる.そのためにはCでいくつかの関数を作成,登録する必要がある.ということで,データ型を定義してみた.
psqlで\dTとタイプすると,そのDBで使用可能なデータ型の一覧が表示される.試しに実行してみたところ,私の環境では今47個のデータ型がある.PostgreSQLではなんとデータ型を自分で定義して使用することができる.データ型を定義するには最低限以下を行なう必要がある.
データ型の内部表現の決定.
以降の作業を行なうために,データの内部表現を決定する.
データを内部表現から外部表現に変換する関数の定義.
内部表現で格納されたデータをフロントエンドでどのように表現するかの変換方法をPostgreSQLは知らないので,そのための関数(ユーザ定義関数)を定義する.
データを外部表現から内部表現に変換する関数の定義.
外部表現(多くの場合SQL文中の表現)をどのように内部表現に変換するかをPostgreSQLは知らないので,そのための関数(ユーザ定義関数)を定義する.
データ型を定義する.
上記二つの関数とともにデータ型を定義する.
PostgreSQLが事前に知る由もないユーザ定義データ型を扱うために何が必要かを考えると自然と上記の作業が必要となる.また,上記の作業だけでは'='や'+'といったオペレータは使用できない.新しいデータ型の「等しいか否かの判断」とか「足す」などといった方法もPostgreSQLは知らない.このために新しいデータ型に対して使用したいオペレータも定義する必要がある(※).これには以下の手順を踏む.
  1. ユーザ定義関数を作成.
  2. 上記関数とともにオペレータのシンボルを定義.
これで,ユーザ定義データ型と定義したオペレータの組み合わせに対して,定義した関数が適用されて演算結果を返すことができる.
※ もちろん新しく定義したデータ型にオペレータが不要ならオペレータの定義も不要である.
ユーザ定義データ型を作成してみた.あまりよい例が浮かばなかったので,あまり意味がないような気がするが,以下の仕様とした.
データ型名
my_id
外部表現
YYYYMMDDのあとに日付毎のシーケンス番号を10桁ゼロアサプレスなしで付加する.
20040415200000000001
内部表現
struct my_id {
int32 year;
int32 month;
int32 day;
int32 seq;
};
オペレータ=
内部表現構造体の全フィールドが一致するなら真.それ以外は偽.
オペレータ!=
内部表現構造体の1個以上のフィールドが不一致なら真.それ以外は偽.
オペレータ=@
内部表現構造体のseq以外の全フィールドが一致なら真.それ以外は偽.
サンプルコードを以下に示す.
ユーザが定義するデータ型をPostgreSQLが内部でどのように表現して持つかを決定しておく必要がある.例えばTEXT型はCではtext型で表現し,その実体はvarlena構造体であった.同様に新しいデータ型をCでどのように表現するかを決定する.当然ながらこの作業は後の作業である内部表現と外部表現との相互の変換ともかかわる.今回は
struct my_id {
int32 year;
int32 month;
int32 day;
int32 seq;
};
とした.これを前提として外部表現と内部表現の相互の変換関数やオペレータ定義時の関数を作成する.
前節で決定した内部表現でDB内に格納されたデータを,フロントエンドでどのように表現するかを決定し,その変換関数を作成する.作業自体はユーザ定義関数の作成そのものである.サンプルではmy_id_it_to_out関数がこれにあたる.この関数ではPostgreSQLから渡されるデータを
  src_my_id = (my_id *)PG_GETARG_POINTER(0);
として取得している.この関数を定義する.
advanced=# CREATE FUNCTION my_id_in_to_out (opaque)
advanced-# RETURNS my_id
advanced-# AS '/tmp/my_id.so',
advanced-# 'my_id_in_to_out'
advanced-# LANGUAGE 'c'
advanced-# WITH (iscachable);
NOTICE:  ProcedureCreate: type my_id is not yet defined
CREAT
関数の引数としてopaqueを指定してる点に注意.
PostgreSQLはフロントエンドで表現したデータを,DBに格納する際には内部表現に変換する必要がある.そのために外部表現から内部表現に変換する関数を作成する.前節と同様,この作業自体はユーザ定義関数の作成そのものである.サンプルではmy_id_out_to_inがこれに当たる.この関数では返り値を
  return PointerGetDatum(output);
としてDatum型データへのポインタからDatum型データを取り出して返している.
advanced=# CREATE FUNCTION my_id_out_to_in (opaque)
advanced-# RETURNS opaque
advanced-# AS '/tmp/my_id.so',
advanced-# 'my_id_out_to_in'
advanced-# LANGUAGE 'c'
advanced-# WITH (iscachable);
CREATE
引数とRETURNSでopaqueを指定してる点に注意.
内部表現と外部表現との相互の変換をする二つの関数とともにデータ型を定義する.これにはCREATE TYPEを使用する.
advanced=> \h CREATE TYPE 
Command:     CREATE TYPE
Description: define a new data type
Syntax:
CREATE TYPE typename ( INPUT = input_function, OUTPUT = output_function
      , INTERNALLENGTH = { internallength | VARIABLE }
    [ , EXTERNALLENGTH = { externallength | VARIABLE } ]
    [ , DEFAULT = default ]
    [ , ELEMENT = element ] [ , DELIMITER = delimiter ]
    [ , SEND = send_function ] [ , RECEIVE = receive_function ]
    [ , PASSEDBYVALUE ]
    [ , ALIGNMENT = alignment ]
    [ , STORAGE = storage ]
)
パラメータがやたら多い.man create_typeの内容をざっくりまとめてみた.
typename
データ型名.アンダーバー'_'で始まってはならず,30文字以内でなければならない.
input_function
データを外部表現から内部表現へ変換する関数の名前.
output_function
データを内部表現から外部表現へ変換する関数の名前.
internallength
その型のデータの内部表現での長さ.可変長の場合はVARIABLEを指定する.
externallength
その型のデータの外部表現での長さ.可変長の場合はVARIABLEを指定する.
default
デフォルト値.省略するとNULLになる.
element
データ型が内部で配列として表現されている場合に,その要素の型を指定する.普通,この値が意味をなすケースはその配列長が固定長の場合だけである.PostgreSQLはデータ型を定義すると,その配列もデータ型として自動的に定義する.fooというデータ型を定義すると,その配列版として_fooを定義し,フロントエンドでfoo[i]を指定すると_fooに変換する.データ型が内部で配列と表現されている場合,そのデータ型の配列は内部表現では2次元配列である.このときelementを指定しておくと,Cで記述する関数でuser_data_type[n]といった方法で,n番目のユーザ定義型データの値にアクセスすることが可能となる.
delimiter
そのデータ型で配列を定義した際の要素間のデリミタを指定する.
send_function
別のマシンにデータを送信する際に,適切な形式に変換する関数を指定する.
receive_function
別のマシンからデータを受信して内部表現に変換する関数を指定する.
PASSEDBYVALUE
普通Datum型(※)より長いデータは値ではなくポインタで渡される.これをあえてポインタより値で渡すように指定する際に指定する.
alignment
バウンダリ処理をどのアライメントで行なうかを指定する.int2,int4,doubleから選択する.デフォルトはint4.
storage
データ格納方法に関する指定.以下から選択する.固定長のデータ型の場合はplainのみが許される.デフォルトはplain.
plain
TOAST不可.圧縮されない.
external
データが長ければメインテーブル列の外に移動する.ただし圧縮はしない.
extended
TOASTの機能をすべて使用可能.システムは最初に長いデータ値を圧縮しようとする.それでもまだ長ければメインテーブル列の外に移動する.
main
データの圧縮を許可し,できるだけメインテーブル列の外には移動しない.ただし他に方法がなければ移動する.
サンプルのデータ型定義を以下のように行なった.
advanced=# CREATE TYPE my_id (
advanced(# internallength = 18,
advanced(# input = my_id_out_to_in,
advanced(# output = my_id_in_to_out);
CREATE
これでmy_idを新しいデータ型として使用することができる.
advanced=> CREATE TABLE hoge (
advanced(> id my_id,
advanced(> val INTEGER);
CREATE
advanced=> INSERT INTO hoge
advanced-> VALUES ('200401040000000001', 1);
INSERT 42348 1
advanced=> SELECT * FROM hoge;
         id         | val 
--------------------+-----
 200401040000000001 |   1
(1 row)
※ Datum型は大抵4バイトだが,一部のシステムでは8バイト.
ここまでの作業だけでは,新しく作成したデータ型に対して'='は使用できない.
advanced=> SELECT * FROM hoge
advanced-> WHERE id = '200401040000000001';
ERROR:  Unable to identify an operator '=' for types 'my_id' and 'unknown'
        You will have to retype this query using an explicit cast
新しいデータ型my_idをオペランドにとるオペレータ'='を定義していないためである(※).そこでオペレータを定義する必要がある.オペレータを定義するためには,オペレータが指定されたときに実行する関数を作成する必要がある.この関数の作成はユーザ定義関数と同様である.サンプルでは三つのオペレータ'=','!=','@='のための関数を定義した.
advanced=# CREATE FUNCTION my_id_eq (my_id, my_id)
advanced-# RETURNS bool
advanced-# AS '/tmp/my_id.so',
advanced-# 'my_id_eq' 
advanced-# LANGUAGE 'c'
advanced-# WITH (iscachable);
CREATE

advanced=# CREATE FUNCTION my_id_ne (my_id, my_id)
advanced-# RETURNS bool
advanced-# AS '/tmp/my_id.so',
advanced-# 'my_id_ne'
advanced-# LANGUAGE 'c'
advanced-# WITH (iscachable);
CREATE

advanced=# CREATE FUNCTION my_id_ymd_eq (my_id, my_id)
advanced-# RETURNS bool
advanced-# AS '/tmp/my_id.so',
advanced-# 'my_id_ymd_eq'
advanced-# LANGUAGE 'c'
advanced-# WITH (iscachable);
CREATE
これらの関数を使用してオペレータを定義した.
advanced=> CREATE OPERATOR = (
advanced(> LEFTARG = my_id,
advanced(> RIGHTARG = my_id,
advanced(> COMMUTATOR = =,
advanced(> procedure = my_id_eq);
CREATE

advanced=> CREATE OPERATOR != (
advanced(> LEFTARG = my_id,
advanced(> RIGHTARG = my_id,
advanced(> COMMUTATOR = !=,
advanced(> procedure = my_id_ne);
CREATE

advanced=> CREATE OPERATOR @= (
advanced(> LEFTARG = my_id,
advanced(> RIGHTARG = my_id,
advanced(> COMMUTATOR = @=,
advanced(> procedure = my_id_ymd_eq);
CREATE
これでmy_idをオペランドにとるオペレータ'=','!=','@='を使用することができる.
advanced=> SELECT * FROM hoge
advanced-> WHERE id = '200401040000000001';
         id         | val 
--------------------+-----
 200401040000000001 |   1
(1 row)

advanced=> SELECT * FROM hoge
advanced-> WHERE id @= '200401040000000002';
         id         | val 
--------------------+-----
 200401040000000001 |   1
(1 row)

advanced=> SELECT * FROM hoge
advanced-> WHERE id != '200401040000000002';
         id         | val 
--------------------+-----
 200401040000000001 |   1
(1 row)
※ オペレータはそのシンボルと左右のオペランドの組み合わせで識別される.
オペレータはDROPを使用して削除する.
advanced=> \h DROP OPERATOR
Command:     DROP OPERATOR
Description: remove a user-defined operator
Syntax:
DROP OPERATOR id ( lefttype | NONE , righttype | NONE )
以下実行例.
advanced=> DROP OPERATOR = (my_id, my_id);
DROP
advanced=> DROP OPERATOR != (my_id, my_id);
DROP
advanced=> DROP OPERATOR @= (my_id, my_id);
DROP
ユーザ定義データ型はDROPで削除する.
advanced=# \h DROP TYPE
Command:     DROP TYPE
Description: remove a user-defined data type
Syntax:
DROP TYPE typename [, ...]
これはDBMasterで実行する必要があるようだ.以下実行例.
advanced=# DROP TYPE my_id;
DROP
matsu(C)
Since 2002
Mail to matsu