35.11. ユーザ定義の型

項35.2に述べられているように、PostgreSQLは、新しい型をサポートするように拡張することができます。 本節では、SQL言語以下のレベルで定義されるデータ型である基本型を新しく定義する方法について説明します。 新しい基本型の作成には、低レベル言語、通常Cで作成された型を操作する関数の実装が必要です。

本節で使用する例は、ソース配布物内のsrc/tutorialディレクトリにcomplex.sqlcomplex.cという名前で置いてあります。 この例の実行方法についてはディレクトリ内のREADMEを参照してください。

ユーザ定義データ型では必ず入力関数と出力関数が必要です。 これらの関数は、その型が(ユーザによる入力とユーザへの出力のための)文字列としてどのように表現されるかと、その型がメモリ中でどう構成されるかを決定します。 入力関数は引数としてヌル終端文字列を取り、その型の(メモリ中の)内部表現を返します。 出力関数は引数としてその型の内部表現を取り、ヌル終端文字列を返します。 単に格納するだけではなく、その型に操作を加えたいのであれば、その型に持たせたい全ての操作を実装した関数をさらに提供しなければなりません。

例えば、複素数を表現するcomplex型を定義することを考えます。 おそらく、次のようなC構造体で複素数をメモリ中で表現することがごく自然な方法です。

typedef struct Complex {
    double      x;
    double      y;
} Complex;

単一のDatum値で扱うには大き過ぎるので、これは参照渡し型にしなければなりません。

この型の外部文字列表現として(x,y)形式の文字列を使用することを選択します。

入出力関数、特に出力関数を作成することは困難ではありません。 しかし、この型の外部表現文字列を定義する時、その表現のための完全で堅牢なパーサを入力関数として作成しなければなりません。 以下に例を示します。

PG_FUNCTION_INFO_V1(complex_in);

Datum
complex_in(PG_FUNCTION_ARGS)
{
    char       *str = PG_GETARG_CSTRING(0);
    double      x,
                y;
    Complex    *result;

    if (sscanf(str, " ( %lf , %lf )", &x, &y) != 2)
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
                 errmsg("invalid input syntax for complex: \"%s\"",
                        str)));

    result = (Complex *) palloc(sizeof(Complex));
    result->x = x;
    result->y = y;
    PG_RETURN_POINTER(result);
}

出力関数は以下のように簡単にできます。

PG_FUNCTION_INFO_V1(complex_out);

Datum
complex_out(PG_FUNCTION_ARGS)
{
    Complex    *complex = (Complex *) PG_GETARG_POINTER(0);
    char       *result;

    result = (char *) palloc(100);
    snprintf(result, 100, "(%g,%g)", complex->x, complex->y);
    PG_RETURN_CSTRING(result);
}

入出力関数は各々の逆関数になるように注意しなければなりません。 そうしないと、データをファイルにダンプし、それを読み戻そうとする際に、深刻な問題が発生するでしょう。 これは特に浮動小数点数が関係する際によく発生する問題です。

省略することができますが、ユーザ定義型はバイナリ入出力関数を提供することができます。 バイナリ入出力は通常テキスト入出力より高速ですが、テキスト入出力より移植性がありません。 テキスト入出力と同様に、外部バイナリ表現を正確に定義することは作成者の責任です。 ほとんどの組み込みデータ型は、マシンに依存しないバイナリ表現を提供しようとしています。 complex型ではfloat8型のバイナリ入出力コンバータを元にします。

PG_FUNCTION_INFO_V1(complex_recv);

Datum
complex_recv(PG_FUNCTION_ARGS)
{
    StringInfo  buf = (StringInfo) PG_GETARG_POINTER(0);
    Complex    *result;

    result = (Complex *) palloc(sizeof(Complex));
    result->x = pq_getmsgfloat8(buf);
    result->y = pq_getmsgfloat8(buf);
    PG_RETURN_POINTER(result);
}

PG_FUNCTION_INFO_V1(complex_send);

Datum
complex_send(PG_FUNCTION_ARGS)
{
    Complex    *complex = (Complex *) PG_GETARG_POINTER(0);
    StringInfoData buf;

    pq_begintypsend(&buf);
    pq_sendfloat8(&buf, complex->x);
    pq_sendfloat8(&buf, complex->y);
    PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
}

入出力関数を作成し共有ライブラリ内にコンパイルすれば、SQLでcomplex型を定義することができます。 まずシェル型として宣言します。

CREATE TYPE complex;

これは、入出力関数を定義する時にこの型を参照することができるプレースホルダとして動作します。 この後以下のように、入出力関数を定義することができます。

CREATE FUNCTION complex_in(cstring)
    RETURNS complex
    AS 'filename'
    LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION complex_out(complex)
    RETURNS cstring
    AS 'filename'
    LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION complex_recv(internal)
   RETURNS complex
   AS 'filename'
   LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION complex_send(complex)
   RETURNS bytea
   AS 'filename'
   LANGUAGE C IMMUTABLE STRICT;

最後にデータ型の完全な定義を提供することができます。

CREATE TYPE complex (
   internallength = 16,
   input = complex_in,
   output = complex_out,
   receive = complex_recv,
   send = complex_send,
   alignment = double
);

新しい基本型を定義すると、PostgreSQLは自動的にその型の配列のサポートを提供します。 配列型は通常、基本型の名前の前にアンダースコア文字_が付いた名前になります。

データ型が存在するようになると、そのデータ型に対する有用な操作を提供する関数を宣言することができます。 そしてその関数を使用する演算子も定義できます。 また、必要に応じて、そのデータ型用のインデックスをサポートするための演算子クラスも作成することができます。 こうした追加層については後の節で説明します。

データ型の値により(内部形式で)容量が変動する場合、そのデータ型をTOAST可能としなければなりません。 (項55.2を参照してください。) ヘッダのオーバーヘッドを減らすことでTOASTは小さなデータに対しても容量を抑えることができますので、データが常に圧縮し外部に格納するには小さ過ぎる場合でも、これを行わなければなりません。

このためには、内部表現が可変長データの標準レイアウトに従っていなければなりません。 先頭の4バイトはchar[4]フィールドで、直接アクセスされることは決してありません(慣習的にvl_len_と呼ばれます)。 SET_VARSIZE()を使用してデータの容量をこのフィールドに格納し、また、VARSIZE()を使用してこのフィールドを取り出さなければなりません。 そのデータ型を扱うC関数は常に、PG_DETOAST_DATUMを使用して、渡されたTOAST化値を注意深く展開しなければなりません。 (通常、こうした詳細は型独自のGETARG_DATATYPE_Pマクロを定義して隠蔽します。) その後、CREATE TYPEコマンドを実行する際に、内部長をvariableと指定し、また、適当な格納オプションを選択してください。

整列が(単なる特定の関数向けやデータ型が常にバイト単位の整列を規定しているため)重要でない場合、PG_DETOAST_DATUMのオーバヘッドの一部を省くことができます。 代わりにPG_DETOAST_DATUM_PACKEDを使用してください(通常はPG_DETOAST_DATUM_PACKEDマクロを定義することで隠蔽されます)。 そして、VARSIZE_ANY_EXHDRおよびVARDATA_ANYマクロを使用して、圧縮されている可能性があるデータにアクセスしてください。 繰り返しますが、これらのマクロから返されるデータは、たとえデータ型定義で整列を規定していたとしても、整列されません。 整列が重要であれば、通常のPG_DETOAST_DATUMインタフェースを介して実行してください。

注意: 古めのコードではしばしばvl_len_char[4]ではなくint32として宣言しています。 この構造体定義が少なくともint32で整列されたフィールドを持っている限り、これは問題ありません。 しかし、整列されていない可能性があるデータを扱う場合に、こうした構造体定義を使用することは危険です。 データが実際に整列されていると仮定することをコンパイラの規則としているかもしれず、この場合、整列に厳密なアーキテクチャではコアダンプしてしまいます。

詳細についてはCREATE TYPEコマンドの説明を参照してください。