31.11. 用戶定義類型

正如 Section 31.2 所說, PostgreSQL 可以擴展為支援新資料類型。 本節描述如何定義新的基本類型,這些類型是那些定義在 SQL 語言之下的資料類型。 建立一個新的基本類型要求實現函數在一種低層語言的類型上操作,通常是 C。

本節的例子可以在源碼發佈中 src/tutorial 目錄的 complex.sqlcomplex.c 裡找到。

一個用戶定義的類型總是有輸入和輸出函數。 這些函數決定該類型如何在字串裡出現(讓用戶輸入和輸出給用戶)以及類型如何在儲存器裡組織。 輸入函數以一個以空(null)結尾的字元串為參數並且返回該類型的內部(內存裡)的資料表現形式。 輸出類型以該類型的內部資料表現形式為參數並且返回一個以空(null)結尾的字元串。

假設我們要定義一個類型 complex 用來資料表示複數。 通常,我們選用下面的 C 結構來在儲存器裡資料表現複數:

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

對於該類型的外部資料表現形式,,我們選擇形如 (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);
}

您應該把您的輸入和輸出函數做的互為逆(函數)。如果您不這樣做, 您就可能在需要把資料輸出來在裝載回去時碰到很嚴重的問題,當涉及到浮點數時,這是非常普遍的問題。

另外,一個用戶定義類型可以提供二進制輸入和輸出過程。 二進制 I/O 通常更快,但是沒有文本 I/O 移植性好。 因為對於文本 I/O 而言,完全是由您來定義外部的二進制形式是如何的。 大多數內置的資料類型都盡可能提供一個與機器無關的二進制形式。 對於 complex,我們將把二進制 I/O 建立在 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));
}

要定義 complex 類型,我們要在建立該類型前先建立用戶定義的 I/O 函數:

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 的(參閱 Section 49.2)。 要做到這一點, 該類型地內部形式必需遵循變長資料內部形式地標準佈局: 頭四個字元必需是一個 int32,包含資料地全長(包括長度自身)。 在該類型上操作的 C 函數必須透過使用 PG_DETOAST_DATUM 小心地解開它們處理的任何「烘烤」過的數值(這些細節通常都可以透過定義類型相關的 GETARG 宏掩蓋)。 最後,在使用 CREATE TYPE 命令的時候, 聲明內部長度為 variable 並且選擇恰當的儲存選項。

更多的細節請參閱 CREATE TYPE 命令的描述。