35.11. 用户定义的类型

Section 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);
}

你应当让输入和输出函数互为彼此的逆函数。如果不这样做,当你需要把数据转储到一个文件并且以后将它重新读入时会遇到很严重的问题。在涉及到浮点数时这是一个特别常见的问题。

可选地,一种用户定义的类型可以提供二进制输入和输出例程。二进制 I/O 通常比文本 I/O 更快但是可移植性更差。与文本 I/O 一样,定义准确的外部二进制表达是你需要负责的工作。大部分的内建数据类型都尝试提供一种不依赖机器的二进制表达。对于complex,我们的工作将建立在为类型float8提供的二进制 I/O 转换器上:

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的(见Section 58.2)。即便数据总是很小不会被压缩或者线外存储你也应该这样做,因为TOAST也能通过减少头部负荷来为小数据减少空间。

要这样做,内部表达必须遵循用于变长数据的标准布局:头四个字节必须是一个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(习惯上通过定义一个GETARG_DATATYPE_PP宏隐藏)并且使用宏VARSIZE_ANY_EXHDR以及VARDATA_ANY来访问一个可能包装过的数据。此外,即使数据类型定义指定了一种对齐方式,这些宏返回的数据也不是对齐过的。如果对齐对你很重要,你必须使用常规的PG_DETOAST_DATUM接口。

Note: 老的代码经常声明vl_len_为一个int32域而不是char[4]。只要结构定义含有其他具有至少int32对齐的域,这就是 OK 的。但是在使用可能未对齐的数据时,使用这样一种结构定义就是危险的,编译器可能会把它当作一个授权来假定数据实际上已经被对齐,在对于对齐很严格的架构上会导致核心转储。

进一步的细节可以参考CREATE TYPE命令的描述。