33.10. 用戶定義類型

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

For further details see the description of the CREATE TYPE command in Part VI. 更多的細節請參閱 CREATE TYPE 命令的描述。