Пользовательские типы
Эта страница переведена при помощи нейросети GigaChat.
Как описано в разделе «Система типов PostgreSQL», PostgreSQL может быть расширен для поддержки новых типов данных. В этом разделе описывается, как определить новые базовые типы, которые являются типами данных, определенными ниже уровня языка 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
);
Когда происходит определение нового базового типа, PostgreSQL автоматически обеспечивает поддержку массивов этого типа. Тип массива обычно имеет то же имя, что и базовый тип с символом подчеркивания (_
) перед ним.
После того как тип данных определен, можно объявить дополнительные функции для выполнения полезных операций над типом данных. Затем операторы могут быть определены поверх функций, а при необходимости можно создавать классы операторов для поддержки индексации типа данных. Эти дополнительные уровни обсуждаются в следующих разделах.
Если внутреннее представление типа данных является переменной длины, внутреннее представление должно соответствовать стандартной компоновке данных переменной длины: первые четыре байта должны быть полем char[4]
, к которому никогда не осуществляется прямой доступ (обычно называемым vl_len_
). Необходимо использовать макрос SET_VARSIZE()
для сохранения общего размера данных (включая само поле длины) в этом поле и VARSIZE()
для его получения. (Эти макросы существуют потому, что поле длины может быть закодировано в зависимости от платформы.)
Для получения дополнительной информации см. описание команды CREATE TYPE.
Особенности TOAST
Если значения типа данных различаются по размеру (во внутренней форме), обычно для такого типа желательно реализовать поддержку TOAST (см. раздел «TOAST»). Нужно сделать это даже в том случае, если значения всегда слишком малы для сжатия или внешнего хранения, потому что 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 в конечном счете представляет собой просто набор байтов; он не может содержать указатели, например, поскольку его можно скопировать в другое место в памяти. Для сложных типов данных плоский формат может быть довольно дорогим в использовании, поэтому PostgreSQL предоставляет способ «расширения» плоского формата в представление, которое лучше подходит для вычислений, а затем передавать этот формат между функциями типа данных в памяти.
Для использования расширенного хранилища тип данных должен определить расширенный формат, соответствующий правилам, приведенным в 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
.