正如节33.2所说,PostgreSQL 可以扩展为支持新的数据类型。本节描述如何定义新的基本类型,这些类型是那些定义在 SQL 语言之下的数据类型。创建一个新的基本类型要求实现函数低层语言(通常是 C)的类型上操作。
本节的例子可以在源码发布中 src/tutorial 目录的 complex.sql 和 complex.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 = (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)); }
一旦我们写好 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 的(参阅节52.2)。要做到这一点,该类型的内部形式必需遵循变长数据内部形式的标准布局:头四个字节必需是一个包含数据全长(包括长度自身)的 int32 。在该类型上操作的 C 函数必须通过使用 PG_DETOAST_DATUM
小心地解开它们处理的任何"烘烤"过的数值(这些细节通常都可以通过定义类型相关的 GETARG
宏掩盖)。最后,在使用 CREATE TYPE 命令的时候,声明内部长度为 variable 并且选择恰当的存储选项。
更多细节请参阅 CREATE TYPE 命令的描述。