35.11. 用户定义类型

正如第 35.2 节所说,PostgreSQL 可以扩展为支持新的数据类型。本节描述如何定义新的基本类型, 这些类型是那些定义在SQL语言之下的数据类型。 创建一个新的基本类型要求实现函数在低层语言(通常是 C)的类型上操作。

本节的例子可以在源码发布中src/tutorial目录的complex.sqlcomplex.c里找到。参见同目录下的README 文件获取关于如何运行例子的指示。

一个用户定义的类型总是有输入和输出函数。 这些函数决定该类型如何在字符串里出现(让用户输入和输出给用户)以及类型如何在内存里组织。 输入函数以一个以空(null)结尾的字符串为参数并且返回该类型的内部(内存里)的表现形式。 输出类型以该类型的内部表现形式为参数并且返回一个以空(null)结尾的字符串。 如果我们想做任何比简单存储它更多的事情,我们必须提供额外的函数来实现我们想要对这个类型的操作。

假设要定义一个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 = psprintf("(%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));
}

一旦我们写好 I/O 函数并将其编译为共享库,就可以定义 SQL 中的complex类型。 我们首先声明其为 shell 类型:

CREATE TYPE complex;

这将作为一个占位符以允许在定义其 I/O 函数时引用该类型。现在我们定义其 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的(参阅第 59.2 节)。 即使数据总是太小以至于被压缩或存储在外部你也应该这样做,因为TOAST 可以通过减少头开销在小数据上节约空间。

要做到这一点,该类型的内部形式必需遵循变长数据内部形式的标准布局: 头四个字节必需是一个从来没有直接访问的 char[4]字段(通常叫 vl_len_)。你必须使用SET_VARSIZE() 来存储这个字段里的数据的长度,使用VARSIZE()来取回这个长度。 在该类型上操作的 C 函数必须通过使用 PG_DETOAST_DATUM 小心地解开它们处理的任何"烘烤"过的数值(这些细节通常都可以通过定义类型相关的 GETARG_DATATYPE_P宏掩盖)。最后, 在使用CREATE TYPE命令的时候,声明内部长度为variable 并且选择恰当的存储选项。

如果对齐是不重要的(不管是只是对于一个特定的函数还是因为数据类型指定字节对齐) 那么避免PG_DETOAST_DATUM的一些开销是可能的。 你可以使用PG_DETOAST_DATUM_PACKED代替(通常通过定义一个 GETARG_DATATYPE_PP宏指令),并且使用宏指令VARSIZE_ANY_EXHDRVARDATA_ANY来使用一个潜在封装的数据。 另外,由这些宏指令返回的数据是不对齐的,即使数据类型定义声明了一个对齐。 如果对齐是重要的,那么你必须通过正规的PG_DETOAST_DATUM接口。

注意: 旧代码经常声明vl_len_为一个int32字段而不是char[4]。 只要结构体的定义中其他字段至少有int32个队列的话这么做是可以的。 但是当使用一个潜在的对齐数据时,使用这样一个结构体定义是危险的; 编译器可能把它当做一个许可来假设数据实际上已经对齐,导致在架构上的核心转储严格对齐。

更多细节请参阅CREATE TYPE命令。