Пользовательские типы
Как описано в разделе Система типов QHB, QHB может быть расширена для поддержки новых типов данных. В этом разделе описывается, как определить новые базовые типы, то есть типы данных, определенные ниже уровня языка SQL. Создание нового базового типа требует реализации функций для работы с этим типом на языке низкого уровня, как правило, на нативном языке.
Пользовательский тип должен всегда иметь функции ввода и вывода. Эти функции определяют, как тип будет отображаться в строках (при вводе и выводе для пользователя) и как этот тип расположен в памяти. Функция ввода принимает в качестве аргумента строку символов, заканчивающуюся нулем, и возвращает внутреннее (в памяти) представление типа. Функция вывода принимает в качестве аргумента внутреннее представление типа и возвращает строку символов, заканчивающуюся нулем. Если мы хотим сделать с типом что-то большее, нежели просто сохранить его, то для реализации любых операций, которые мы хотели бы иметь для этого типа, нужно предоставить дополнительные функции.
Предположим, мы хотим определить тип 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 'имя_файла'
LANGUAGE C IMMUTABLE STRICT;
CREATE FUNCTION complex_out(complex)
RETURNS cstring
AS 'имя_файла'
LANGUAGE C IMMUTABLE STRICT;
CREATE FUNCTION complex_recv(internal)
RETURNS complex
AS 'имя_файла'
LANGUAGE C IMMUTABLE STRICT;
CREATE FUNCTION complex_send(complex)
RETURNS bytea
AS 'имя_файла'
LANGUAGE C IMMUTABLE STRICT;
Наконец мы можем предоставить полное определение типа данных:
CREATE TYPE complex (
internallength = 16,
input = complex_in,
output = complex_out,
receive = complex_recv,
send = complex_send,
alignment = double
);
Когда вы определяете новый базовый тип, QHB автоматически обеспечивает поддержку массивов этого типа. Тип массива обычно имеет то же имя, что и базовый тип, с добавлением символа подчеркивания (_) в начале.
Теперь, когда тип данных существует, мы можем объявить дополнительные функции для выполнения полезных операций с этим типом. Поверх этих функций можно определить операторы и при необходимости создать классы операторов для поддержки индексации этого типа. Эти дополнительные уровни рассматриваются в следующих разделах.
Если внутреннее представление типа данных имеет переменную длину, оно должно соответствовать стандартной схеме данных переменной длины: первые четыре байта должны занимать поле char[4], к которому никогда не следует обращаться напрямую (традиционно называемое vl_len_). Чтобы сохранить в этом поле общий размер элемента (включая само поле длины), нужно использовать макрос SET_VARSIZE(), а чтобы получить его — VARSIZE(). (Эти макросы существуют, потому что поле длины может кодироваться по-разному в зависимости от платформы.)
Более подробную информацию см. на справочной странице команды CREATE TYPE.
Особенности TOAST
Если значения вашего типа данных сильно различаются по размеру (во внутреннем представлении), обычно желательно сделать это тип подходящим для TOAST (см. раздел TOAST). Стоит это сделать даже в том случае, когда значения всегда слишком малы для сжатия или внешнего хранения, поскольку TOAST может сэкономить пространство и с данными малого размера, уменьшая издержки в заголовке.
Чтобы поддерживать хранение TOAST, функции на C/RUST, работающие с этим типом данных, должны позаботиться о распаковке любых сжатых значений, которые передаются им с помощью макроса 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, в конечном счете, является простым набором байтов; например, он не может содержать указатели, так как может быть скопирован в другие области памяти. Для сложных типов данных может быть довольно дорого работать с плоским форматом, поэтому QHB предоставляет способ «развернуть» плоский формат в представление, более подходящее для вычислений, а затем передавать этот формат в памяти между функциями, работающими с этим типом данных.
Чтобы использовать развернутое хранилище, тип данных должен определять развернутый формат, который следует правилам, и предоставить функции для «разворачивания» плоского значения varlena в этот формат и «сворачивания» его обратно в обычное представление varlena. Затем добейтесь того, чтобы все функции на C/RUST для этого типа данных могли принимать любое представление, возможно, путем преобразования одной в другую сразу при получении. Это не требует одновременного исправления всех существующих функций для этого типа данных, поскольку стандартный макрос PG_DETOAST_DATUM способен преобразовать развернутые входные данные в обычный плоский формат. Таким образом, существующие функции, работающие с плоским форматом varlena, продолжат работать, хотя и не очень эффективно, с развернутыми входными данными; их не нужно преобразовывать до тех пор, пока не понадобится повысить производительность.
Функции на C/RUST, которые знают, как работать с развернутым представлением, обычно делятся на две категории: те, которые могут обрабатывать только развернутый формат, и те, которые могут обрабатывать как развернутые, так и плоские входные данные varlena. Первые легче написать, но они могут быть менее эффективными в целом, потому что преобразование плоских входных данных в развернутую форму для использования одной-единственной функцией может стоить больше, чем экономится при работе с развернутым форматом. Когда требуется обрабатывать только развернутый формат, преобразование плоских входных данных в развернутую форму можно скрыть внутри макроса, извлекающего аргументы, чтобы функция была не более сложной, чем та, которая работает с традиционными входными данными varlena. Чтобы обработать оба типа входных данных, напишите функцию извлечения аргументов, которая будет распаковывать входные данные varlena с коротким заголовком, а также внешние и сжатые, но не развернутые. Такую функцию можно определить как возвращающую указатель на объединение плоского формата varlena и развернутого формата. Чтобы определить, какой именно формат был получен, можно воспользоваться макросом VARATT_IS_EXPANDED_HEADER().
Инфраструктура TOAST не только позволяет отличать обычные значения varlena от развернутых значений, но также различает указатели «для чтения-записи» и «только для чтения» на развернутые значения. Функции на C/RUST, которым нужно проверять только развернутое значение или которые будут изменять его только безопасными и не видимыми семантически способами, нет необходимости обращать внимание на то, какой тип указателя они получают. Функциям на C/RUST, которые выдают измененную версию входных данных, разрешено изменять развернутые входные данные на месте при получении указателя для чтения-записи, но не когда получают указатель только для чтения; в этом случае они должны сначала скопировать это значение, создав новое значение, подлежащее изменению. Функция на C/RUST, которая создала новое развернутое значение, всегда должна возвращать указатель на него для чтения-записи. Кроме того, функция на C/RUST, которая изменяет развернутое значение для чтения-записи на месте, должна позаботиться о том, чтобы оставить его в нормальном состоянии, если во время ее работы произойдет сбой.