36.12. Пользовательские типы данных#

36.12. Пользовательские типы данных

36.12. Пользовательские типы данных

Как описано в Раздел 36.2, Tantor SE может быть расширен для поддержки новых типов данных. В этом разделе описывается, как определить новые базовые типы, которые являются типами данных, определенными ниже уровня языка SQL. Создание нового базового типа требует реализации функций для работы с типом на низкоуровневом языке, обычно на C.

Все примеры в этом разделе можно найти в файлах complex.sql и complex.c в каталоге src/tutorial дистрибутива исходного кода. См. файл 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 type %s: \"%s\"",
                        "complex", 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);
}

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

По желанию, пользовательский тип данных может предоставлять бинарные процедуры ввода и вывода. Бинарный ввод-вывод обычно работает быстрее, но менее переносим, чем текстовый ввод-вывод. Как и в случае с текстовым вводом-выводом, вам нужно определить точное внешнее бинарное представление. Большинство встроенных типов данных пытаются предоставить машинно-независимое бинарное представление. Для типа 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 в SQL. Сначала мы объявляем его как оболочку:

CREATE TYPE 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
);

При определении нового базового типа, Tantor SE автоматически предоставляет поддержку массивов этого типа. Тип массива обычно имеет то же имя, что и базовый тип, с символом подчеркивания (_) в начале.

После создания типа данных мы можем объявить дополнительные функции для выполнения полезных операций с этим типом данных. Затем можно определить операторы на основе этих функций, и, при необходимости, можно создать классы операторов для поддержки индексирования типа данных. Эти дополнительные уровни обсуждаются в следующих разделах.

Если внутреннее представление типа данных является переменной длины, то внутреннее представление должно соответствовать стандартной структуре для данных переменной длины: первые четыре байта должны быть полем char[4], которое никогда не используется напрямую (обычно называется vl_len_). Вы должны использовать макрос SET_VARSIZE() для сохранения общего размера данных (включая само поле длины) в этом поле и макрос VARSIZE() для его извлечения. (Эти макросы существуют, потому что поле длины может быть закодировано в зависимости от платформы).

Для получения дополнительной информации см. описание команды CREATE TYPE.

36.12.1. Рассмотрение TOAST

Если значения вашего типа данных имеют различный размер (во внутреннем формате), обычно желательно сделать тип данных TOAST-способным (см. Раздел 71.2). Вы должны сделать это даже если значения всегда слишком малы для сжатия или хранения внешне, потому что TOAST также может экономить место на небольших данных, уменьшая издержки на заголовок.

Для поддержки хранения TOAST, функции на языке C, работающие с данным типом, всегда должны быть осторожны при распаковке любых упакованных значений, которые им передаются, с помощью функции PG_DETOAST_DATUM. (Эта деталь обычно скрыта путем определения макросов GETARG_DATATYPE_P, специфичных для типа). Затем, при выполнении команды CREATE TYPE, укажите внутреннюю длину как variable и выберите подходящий вариант хранения, отличный от plain.

Если выравнивание данных не важно (либо только для конкретной функции, либо потому что тип данных уже указывает выравнивание по байтам), то можно избежать некоторых издержек PG_DETOAST_DATUM. Вместо этого можно использовать PG_DETOAST_DATUM_PACKED (обычно скрытый с помощью определения макроса GETARG_DATATYPE_PP) и использовать макросы VARSIZE_ANY_EXHDR и VARDATA_ANY для доступа к потенциально упакованным данным. Опять же, данные, возвращаемые этими макросами, не выровнены, даже если определение типа данных указывает на выравнивание. Если выравнивание важно, вам необходимо использовать обычный интерфейс PG_DETOAST_DATUM.

Примечание

Старый код часто объявляет vl_len_ как поле int32, а не char[4]. Это допустимо, пока определение структуры имеет другие поля, которые имеют выравнивание, по крайней мере, int32. Однако использование такого определения структуры при работе с потенциально невыровненными данными является опасным; компилятор может считать, что данные фактически выровнены, что может привести к сбоям ядра на архитектурах, которые строго следуют правилам выравнивания.

Другая функция, которая включается с поддержкой TOAST, это возможность иметь расширенное представление данных в памяти, которое удобнее для работы, чем формат, хранящийся на диске. Обычный или плоский формат хранения varlena в конечном итоге является просто блобом байтов; он не может, например, содержать указатели, так как он может быть скопирован в другие места в памяти. Для сложных типов данных плоский формат может быть довольно дорогим для работы, поэтому Tantor SE предоставляет способ расширить плоский формат в представление, которое лучше подходит для вычислений, а затем передать это представление в памяти между функциями типа данных.

Для использования расширенного хранения, тип данных должен определить расширенный формат, который следует правилам, указанным в src/include/utils/expandeddatum.h, и предоставить функции для расширения плоского значения varlena в расширенный формат и сжатия расширенного формата обратно в обычное представление varlena. Затем убедитесь, что все функции на языке C для данного типа данных могут принимать любое из представлений, возможно, преобразуя одно в другое непосредственно при получении. Это не требует исправления всех существующих функций для данного типа данных сразу, потому что стандартная макрос PG_DETOAST_DATUM определена для преобразования расширенных входных данных в обычный плоский формат. Поэтому существующие функции, которые работают с плоским форматом varlena, будут продолжать работать, хотя и несколько неэффективно, с расширенными входными данными; их необходимо преобразовывать только в случае, если важна лучшая производительность.

Функции на языке C, которые умеют работать с расширенным представлением, обычно можно разделить на две категории: те, которые могут обрабатывать только расширенный формат, и те, которые могут обрабатывать как расширенные, так и плоские входные данные varlena. Первые проще написать, но могут быть менее эффективны в целом, потому что преобразование плоского входа в расширенную форму для использования одной функцией может стоить больше, чем экономия от работы с расширенным форматом. Если нужно обрабатывать только расширенный формат, преобразование плоских входных данных в расширенную форму может быть скрыто внутри макроса получения аргументов, так что функция будет выглядеть не более сложной, чем функция, работающая с традиционным входом varlena. Чтобы обрабатывать оба типа входных данных, напишите функцию получения аргументов, которая будет разархивировать внешние, короткие заголовки и сжатые входные данные varlena, но не расширенные входные данные. Такую функцию можно определить как возвращающую указатель на объединение плоского формата varlena и расширенного формата. Вызывающие функции могут использовать макрос VARATT_IS_EXPANDED_HEADER() для определения формата, который они получили.

Инфраструктура TOAST позволяет не только различать обычные значения varlena от расширенных значений, но также различает указатели чтение-запись и только чтение на расширенные значения. Функции на языке C, которым нужно только изучить расширенное значение или изменить его только безопасным и незаметным образом, не обязаны обращать внимание на тип указателя, который они получают. Функции на языке C, которые создают измененную версию входного значения, могут изменять входное расширенное значение на месте, если они получают указатель на чтение-запись, но не должны изменять входное значение, если они получают указатель только на чтение; в этом случае они должны сначала скопировать значение, чтобы создать новое значение для изменения. Функция на языке C, которая создала новое расширенное значение, всегда должна возвращать указатель на чтение-запись на него. Кроме того, функция на языке C, которая изменяет расширенное значение на месте с использованием указателя на чтение-запись, должна обеспечить, чтобы значение оставалось в состоянии, пригодном для использования, если она не завершится полностью.

Для примеров работы с расширенными значениями смотрите стандартную инфраструктуру массивов, в частности файл src/backend/utils/adt/array_expanded.c.