38.2に述べられているように、PostgreSQLは、新しい型をサポートするように拡張することができます。 本節では、SQL言語以下のレベルで定義されるデータ型である基本型を新しく定義する方法について説明します。 新しい基本型の作成には、低レベル言語、通常Cで作成された型を操作する関数の実装が必要です。
本節で使用する例は、ソース配布物内のsrc/tutorial
ディレクトリにcomplex.sql
とcomplex.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 type %s: \"%s\"", "complex", 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 = psprintf("(%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は自動的にその型の配列のサポートを提供します。
配列型は通常、基本型の名前の前にアンダースコア文字_
が付いた名前になります。
データ型が存在するようになると、そのデータ型に対する有用な操作を提供する関数を宣言することができます。 そしてその関数を使用する演算子も定義できます。 また、必要に応じて、そのデータ型用のインデックスをサポートするための演算子クラスも作成することができます。 こうした追加層については後の節で説明します。
データ型の内部表現が可変長であるなら、内部表現は可変長データの標準配置に従わなければなりません。先頭の4バイトはchar[4]
フィールドで、直接アクセスされることは決してありません(慣習的にvl_len_
と呼ばれます)。
SET_VARSIZE()
マクロを使用してデータの総量をこのフィールドに格納し、また、VARSIZE()
を使用して取り出さなければなりません。
(長さフィールドはプラットフォームに依存してエンコードされるかもしれませんので、このマクロが存在します。)
詳細についてはCREATE TYPEコマンドの説明を参照してください。
データ型の値により(内部形式で)容量が変動する場合、そのデータ型をTOAST可能とすることが通常は望ましいです(70.2を参照してください)。 ヘッダのオーバーヘッドを減らすことでTOASTは小さなデータに対しても容量を抑えることができますので、データが常に圧縮したり外部に格納したりするには小さ過ぎる場合でも、これを行なうことを推奨します。
TOAST格納をサポートするために、そのデータ型を扱うC関数は常に、PG_DETOAST_DATUM
を使用して、渡されたTOAST化値を注意深く展開しなければなりません。
(通常、こうした詳細は型独自のGETARG_DATATYPE_P
マクロを定義して隠蔽します。)
その後、CREATE TYPE
コマンドを実行する際に、内部長をvariable
と指定し、また、plain
以外の適当な格納オプションを選択してください。
データの整列が(単なる特定の関数向けやデータ型が常にバイト単位の整列を規定しているため)重要でない場合、PG_DETOAST_DATUM
のオーバヘッドの一部を省くことができます。
代わりにPG_DETOAST_DATUM_PACKED
を使用してください(通常はGETARG_DATATYPE_PP
マクロを定義することで隠蔽されます)。
そして、VARSIZE_ANY_EXHDR
およびVARDATA_ANY
マクロを使用して、圧縮されている可能性があるデータにアクセスしてください。
繰り返しますが、これらのマクロから返されるデータは、たとえデータ型定義で整列を規定していたとしても、整列されません。
整列が重要であれば、通常のPG_DETOAST_DATUM
インタフェースを介して実行してください。
古めのコードではしばしばvl_len_
をchar[4]
ではなくint32
として宣言しています。
この構造体定義が少なくともint32
で整列されたフィールドを持っている限り、これは問題ありません。
しかし、整列されていない可能性があるデータを扱う場合に、こうした構造体定義を使用することは危険です。
データが実際に整列されていると仮定することをコンパイラの規則としているかもしれず、この場合、整列に厳密なアーキテクチャではコアダンプしてしまいます。
TOASTのサポートにより有効になるもう一つの機能は以下のような可能性です。ディスクに格納されたフォーマットよりも扱うのにより便利な展開されたインメモリデータ表現を持てるかもしれません。 通常のもしくは「単純な」varlena格納フォーマットは結局のところ単なるバイトのblobです。例えば、メモリの別の場所にコピーされるかもしれませんのでポインタを含むことができません。 複雑なデータ型に対しては、単純なフォーマットは扱うのにかなり高価になるかもしれません。そこで、PostgreSQLは計算するのにより適した表現に単純なフォーマットを「展開する」方法を提供し、そのフォーマットをインメモリでそのデータ型の関数から関数へと渡します。
展開された格納を使うためには、データ型はsrc/include/utils/expandeddatum.h
にある規則に従う展開されたフォーマットを定義し、単純なvarlenaの値を展開されたフォーマットに「展開する」関数や展開されたフォーマットを通常のvarlena表現に「戻す」関数を提供しなければなりません。
そのデータ型のC関数はすべてどちらの表現でも確実に受け付けられるようにしてください。おそらく、受け取ったらすぐに一方からもう一方に変換することによって実現することになるでしょう。
これはそのデータ型の既存の関数をすべて一度に修正することを要求するものでありません。なぜなら、PG_DETOAST_DATUM
マクロが展開された入力を通常の単純なフォーマットに変換するために定義されているからです。
そのため、単純なvarlenaフォーマットを扱う既存の関数は、わずかに非効率ではありますが、展開された入力も続けて扱えるでしょう。より良いパフォーマンスが重要になるまで、変更は必要ありません。
展開された表現の扱い方を知っているC関数は典型的には以下の2つに分類されます。展開されたフォーマットのみを扱えるものと、展開されたものも単純なvarlena入力も扱えるものです。
前者は書くのが簡単ですが、全般にあまり効率的ではないかもしれません。なぜなら、一つの関数による使用のために単純な入力を展開された形に変換することは、展開されたフォーマットで操作することで節約されることよりコストが掛かるかもしれないからです。
展開されたフォーマットのみ扱うことが必要であるなら、単純な入力の展開された形への変換は引数を取得するマクロの中に隠すことができます。それゆえ、関数は伝統的なvarlena入力を扱うものよりもより複雑に見えることはありません。
両方の型の入力を扱うためには、外部やショートヘッダや圧縮されたvarlenaの入力はトースト解除をするけれども展開された入力に対してはトースト解除をしないような、引数を取得する関数を書いてください。
そのような関数は、単純なvarlenaフォーマットと展開されたフォーマットの共用体へのポインタを返すよう定義できます。
呼び出し側はどちらのフォーマットを受け取ったのか確定するのにVARATT_IS_EXPANDED_HEADER()
マクロを使えます。
TOAST基盤により、通常のvarlenaの値を展開された値から区別できるようになるだけでなく、展開された値への「読み書き可能」なポインタと「読み取りのみ」のポインタを区別できるようになります。 展開された値を検査することが必要なだけのものや安全で意味論的に不可視の方法で変更するC関数は、受け取ったポインタがどちらの種類であるか気にする必要はありません。 入力値の修正されたバージョンを生成するC関数は、読み書き可能なポインタを受け取ったのであれば展開された入力値をその場で修正できますが、読み取りのみのポインタを受け取ったのであれば入力を変更してはなりません。その場合には、まず値をコピーして、修正するための新しい値を生成しなければなりません。 展開された値を新しく作成したC関数は、必ずそこへの読み書き可能なポインタを返すことを推奨します。 また、読み書き可能な展開された値をその場で修正するC関数は、途中で失敗した場合に気をつけて値を健全な状態のままにしておくことを推奨します。
展開された値を扱う例は、標準配列基盤、特にsrc/backend/utils/adt/array_expanded.c
を見てください。