31.11. Типы, определяемые пользователем

Как описано в Section 31.2, можно расширять PostgreSQL, создавая поддержку новых типов данных. Данный раздел описывает как определить новые базовые типы, которые будут типами данных, определяемыми на более низком уровне, чем язык SQL. Создание нового базового типа требует реализации функций для работы с этим типом на языке низкого уровня, обычно на C.

Примеры, которые используются в этом разделе можно найти в файлах complex.sql и complex.c в подкаталоге src/tutorial, который находится в каталоге с исходными текстами. Инструкцию по запуску этих примеров можно найти в файле README.

Определяемый пользователем тип должен всегда иметь функции для работы с вводом и выводом. Эти функции определяют как данный тип выглядит в строках (для ввода значений типа пользователем и вывода значений пользователю) и как данный тип организован в памяти. Функция ввода в качестве аргумента берёт строку, которая завершается нулевым символом и возвращает внутреннее представление (в памяти) данного типа. Функция вывода берёт в качестве аргумента внутреннее представление данного типа и возвращает строку символов, которая завершается нулём. Если мы хотим выполнять с типом нечто большее, то мы должны просто это написать, мы должны предоставить дополнительные функции для реализации любых операций, которые мы хотим выполнять с данным типом.

Далее мы хотим определить тип complex, который обеспечивает представление комплексных чисел. Естественный способ представления комплексного числа в памяти - это структура на языке C:

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

Нам понадобится сконструировать этот pass-by-reference тип, так как он слишком велик, чтобы поместиться в единичное значение 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);
}

Вы должны быть аккуратны при создании функций ввода и вывода, которые являются противоположными друг другу. Иначе у вас возникнуть проблемы, когда вам понадобится сделать дамп ваших данных в файл, а затем прочитать их обратно. Эта проблема особенно актуальна при использовании сложных чисел с плавающей точкой.

Необязательно, определяемый пользователем тип может предоставлять подпрограммы для двоичного ввода и вывода. Обычно двоичный ввод/вывод быстрее, но менее переносим, чем текстовый ввод/вывод. Как и в случае с текстовым вводом/выводом, вам необходимо определить точное внешнее двоичное представление. Для большинства встроенных типов данных сделана попытка предоставить машинно-независимвое двоичное представление. Для complex, бы будем использовать такие же преобразования ввода/вывода как и для типа 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, вам понадобится создать определяемые пользователем функции ввода/вывода до создания самого типа:

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 автоматически предоставляет поддержку массивов этого типа. По историческим причинам, тип массива имеет такое же имя как и базовый тип с символом подчёркивания (_) вначале.

Как только тип данных станет существовать, вы можете объявить дополнительные функции, чтобы предоставить полезные операции с этим типом данных. Operators can then be defined atop the functions, and if needed, operator classes can be created to support indexing of the data type. Эти дополнительные слои обсуждаются в следующих разделах.

Если размер значений вашего типа данных может превышать несколько сотен байт (во внутренней форме), вы должны сделать ваш тип данных TOAST-абельным (см. Section 49.2). Чтобы сделать это, внутреннее представление должно следовать стандартному порядку для данных переменной длины: первые четыре байта должны быть int32 и содержать полную длину данных в байтах (включая и сами эти четыре байта). Функции на языке C, которые работают с этим типом данных должны аккуратно распаковывать любые toasted значения, с которыми они работают, используя PG_DETOAST_DATUM. (Это подробность сокрыта через определение специфичного для типа макроса GETARG). Затем, когда запускается команда CREATE TYPE, задайте внутреннюю длину как variable и выберите соответствующую опцию хранения.

Дальнейшие подробности см. в описании команды CREATE TYPE.