Conforme descrito em O sistema de tipos de dados do PostgreSQL, o PostgreSQL pode ser estendido para dar suporte a novos tipos de dados. Essa seção descreve como definir novos tipos de dados base, que são tipos de dados definidos abaixo do nível da linguagem SQL. A criação de um novo tipo de dados base requer a implementação de funções para operar no tipo de dados em uma linguagem de baixo nível, geralmente C.
Os exemplos mostrados nessa seção podem ser encontrados nos arquivos
complex.sql e
complex.c no diretório src/tutorial
da distribuição do código-fonte.
Veja o arquivo README
nesse diretório para
obter instruções sobre como executar os exemplos.
Um tipo de dados definido pelo usuário deve ter sempre funções de entrada e de saída. Essas funções determinam como o tipo de dados aparece em cadeias de caracteres (para entrada do usuário, e saída para o usuário), e como o tipo de dados é organizado na memória. A função de entrada usa uma cadeia de caracteres terminada em nulo como seu argumento, e retorna a representação interna (em memória) do tipo de dados. A função de saída usa a representação interna do tipo de dados como argumento, e retorna uma cadeia de caracteres terminada em nulo. Se for desejado fazer algo a mais com o tipo de dados do que apenas armazená-lo, deve-se fornecer funções adicionais para implementar quaisquer outras operações que se deseje ter para esse tipo de dados.
Suponha que se deseje definir o tipo de dados complex
,
representando números complexos.
Uma maneira natural de representar um número complexo na memória
seria a seguinte estrutura na linguagem C:
typedef struct Complex { double x; double y; } Complex;
É necessário tornar essa estrutura um tipo de dados passado por
referência, já que é muito grande para caber em um único valor
Datum
.
A representação externa escolhida do tipo de dados terá a forma da
cadeia de caracteres (x,y)
.
Geralmente, as funções de entrada e saída não são difíceis de serem escritas, especialmente a função de saída. Mas ao definir a representação externa do tipo de dados por uma cadeia de caracteres, lembre-se de que pode ser necessário escrever um analisador completo e robusto como função de entrada para essa representação. Por exemplo:
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("sintaxe de entrada inválida para o tipo de dados %s: \"%s\"", "complex", str))); result = (Complex *) palloc(sizeof(Complex)); result->x = x; result->y = y; PG_RETURN_POINTER(result); }
A função de saída pode ser só:
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); }
Deve-se tomar o cuidado de tornar as funções de entrada e saída inversas uma da outra. Se não for assim, haverá sérios problemas quando for necessário fazer uma cópia de segurança dos dados em arquivo e depois lê-los novamente. Esse é um problema muito comum quando estão envolvidos números de ponto flutuante.
Opcionalmente, o tipo de dados definido pelo usuário pode fornecer
rotinas de entrada e saída binárias.
A E/S binária é geralmente mais rápida, mas menos portável, do que
a E/S textual.
Assim como na E/S textual, cabe a quem define o tipo de dados
especificar exatamente qual é a representação binária externa.
A maioria dos tipos de dados nativos tenta fornecer uma representação
binária independente de máquina.
Para o tipo de dados complex
, são usados os conversores
de E/S binários para o tipo de dados 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)); }
Após escrever as funções de E/S e as compilar em uma biblioteca
compartilhada, pode ser definido o tipo de dados complex
no SQL.
Primeiro, se declara
CREATE TYPE complex;
que serve de abrigo (shell) para permitir fazer referência ao tipo de dados ao definir suas funções de E/S e, em seguida, pode-se então definir suas funções de E/S:
CREATE FUNCTION complex_in(cstring) RETURNS complex AS 'nome_do_arquivo
' LANGUAGE C IMMUTABLE STRICT; CREATE FUNCTION complex_out(complex) RETURNS cstring AS 'nome_do_arquivo
' LANGUAGE C IMMUTABLE STRICT; CREATE FUNCTION complex_recv(internal) RETURNS complex AS 'nome_do_arquivo
' LANGUAGE C IMMUTABLE STRICT; CREATE FUNCTION complex_send(complex) RETURNS bytea AS 'nome_do_arquivo
' LANGUAGE C IMMUTABLE STRICT;
Por fim, é possível fornecer a definição completa do tipo de dados:
CREATE TYPE complex ( internallength = 16, input = complex_in, output = complex_out, receive = complex_recv, send = complex_send, alignment = double );
Quando é definido um novo tipo de dados base, o
PostgreSQL oferece suporte automaticamente
para as matrizes desse tipo de dados.
O tipo de dados da matriz tem geralmente o mesmo nome do tipo de dados
base com o caractere de sublinhado (_
) anexado.
Depois que o tipo de dados existe, é possível declarar funções adicionais para fornecer operações úteis no tipo de dados. Podem então ser definidos operadores sobre as funções e, se necessário, podem ser criadas classes de operador para oferecer suporte à indexação do tipo de dados. Essas camadas adicionais são discutidas nas próximas seções.
Se a representação interna do tipo de dados for de comprimento variável,
a representação interna deve seguir o modelo padrão para dados de
comprimento variável: os primeiros quatro bytes devem ser um campo
char[4]
, que nunca é acessado diretamente
(normalmente chamado de vl_len_
).
Deve ser usada a macro SET_VARSIZE()
para armazenar
o tamanho total do datum
(incluindo o próprio campo de comprimento) nesse campo,
e VARSIZE()
para recuperá-lo.
(Essas macros existem porque o campo de comprimento pode estar
codificado dependendo da plataforma.)
Para mais detalhes veja a descrição do comando CREATE TYPE.
Se os valores do tipo de dados variarem em tamanho (na forma interna), é geralmente desejável tornar o tipo de dados possível de ser armazenado usando TOAST (a técnica de armazenamento de atributos superdimensionados) (veja a TOAST). Isso deve ser feito mesmo se os valores forem sempre muito pequenos para serem comprimidos ou armazenados externamente, porque o TOAST também pode economizar espaço em dados pequenos, reduzindo a sobrecarga do cabeçalho.
Para dar suporte ao armazenamento TOAST, as funções
C que operam no tipo de dados devem ter sempre o
cuidado de expandir quaisquer valores TOAST
entregues usando a função PG_DETOAST_DATUM
.
(Esse detalhe é normalmente escondido pela definição das macros
GETARG_DATATYPE_P
específicas do tipo de dados.)
Em seguida, ao executar o comando CREATE TYPE
,
deve ser especificado o comprimento interno como
variable
, e selecionada alguma opção de
armazenamento apropriada diferente de plain
.
Se o alinhamento de dados não for importante (só para uma função
específica, ou porque o tipo de dados especifica o alinhamento de
bytes de qualquer maneira), então é possível evitar parte da
sobrecarga de PG_DETOAST_DATUM
.
Em vez disso pode ser usado
PG_DETOAST_DATUM_PACKED
(normalmente escondido pela definição da macro
GETARG_DATATYPE_PP
),
e usada as macros VARSIZE_ANY_EXHDR
e
VARDATA_ANY
para acessar um
datum
potencialmente comprimido.
Novamente, os dados retornados por essas macros não são alinhados,
mesmo que a definição do tipo de dados especifique um alinhamento.
Se o alinhamento for importante, deve ser usada a interface normal
PG_DETOAST_DATUM
.
Os códigos mais antigos frequentemente declaram
vl_len_
como um campo int32
,
em vez de char[4]
.
Tudo bem desde que a definição da struct
tenha
outros campos que tenham pelo menos o alinhamento int32
.
Mas é perigoso usar essa definição de struct
ao trabalhar com dados potencialmente não alinhados;
o compilador pode tomar a licença de assumir que os dados estão
alinhados, levando a descargas (dumps)
de memória em arquiteturas que são rígidas quanto ao alinhamento.
Outro recurso, habilitado pelo suporte TOAST,
é a capacidade de ter uma representação de dados na memória
expandida, que é mais amigável para trabalhar
do que o formato armazenado em disco.
O formato de armazenamento varlena
padrão ou
“simples” (flat) é,
em última análise, apenas um “conjunto de bytes”;
por exemplo, ele não pode conter ponteiros, porque pode ser copiado
para outros locais na memória.
Para tipos de dados complexos, o formato simples pode ser muito custoso
para trabalhar, por isso o PostgreSQL
fornece uma maneira de “expandir” o formato simples
em uma representação mais adequada para ser usada e, em seguida,
passar esse formato na memória entre funções do tipo de dados.
Para usar o armazenamento expandido, o tipo de dados deve definir um
formato expandido que siga as regras dadas no arquivo
src/include/utils/expandeddatum.h
,
e fornecer funções para “expandir” o valor
varlena
simples
(flatten) no formato expandido, e
“simplificar” o formato expandido de volta à
representação de varlena
padrão.
Em seguida, certifique-se de que todas as funções C
para o tipo de dados possam aceitar qualquer uma das representações,
se possível convertendo uma na outra imediatamente após o recebimento.
Isso não requer a correção de todas as funções existentes para o tipo
de dados de uma vez, porque a macro PG_DETOAST_DATUM
padrão é definida de forma a converter entradas expandidas no formato
simples padrão.
Por causa disso, as funções existentes que funcionam com o formato
simples de varlena
continuarão a funcionar, embora
com menos eficiência, com entradas expandidas; elas não precisam ser
convertidas até, e a menos, que seja importante um melhor desempenho.
As funções C que sabem como trabalhar com a
representação expandida, geralmente se enquadram em duas categorias:
aquelas que só sabem lidar com o formato expandido, e aquelas que
sabem lidar com entradas varlena
expandidas ou simples.
As primeiras são mais fáceis de escrever, mas podem ser menos eficientes
no geral, porque a conversão de uma entrada simples para o formato
expandido para uso por uma única função pode custar mais do que o
ganho de operar no formato expandido.
Quando apenas o formato expandido precisa ser manipulado, a conversão
de entradas simples para a forma expandida pode ser ocultada dentro de
uma macro de busca de argumentos, de modo que a função não pareça mais
complexa do que outra que trabalha com a entrada
varlena
padrão.
Para lidar com os dois tipos de entrada, deve ser escrita uma função
de busca de argumento que expanda as entradas
varlena
externas, de cabeçalho curto e comprimidas,
mas não as entradas expandidas.
Essa função pode ser definida como o retorno de um ponteiro para uma
união do formato varlena
simples e o formato
expandido.
Elas podem usar a macro
VARATT_IS_EXPANDED_HEADER()
para determinar
o formato recebido.
A infra-estrutura TOAST não apenas permite que
valores varlena
padrão sejam diferenciados
de valores expandidos, mas também distingue ponteiros de
“leitura e escrita” e “leitura-apenas”
para valores expandidos.
As funções C que precisam apenas examinar um valor
expandido, ou apenas o alterar de maneiras segura e não semanticamente
visível, não precisam se importar com o tipo de ponteiro que recebem.
As funções C que produzem uma versão modificada
do valor de entrada, podem modificar o valor de entrada expandido no
local se receberem um ponteiro de leitura/escrita, mas não devem
modificar a entrada se receberem um ponteiro de leitura-apenas;
nesse caso, elas precisam copiar o valor primeiro, produzindo um novo
valor para modificar.
Uma função C que construiu um novo valor expandido,
deve retornar sempre um ponteiro de leitura/escrita para ele.
Além disso, uma função C que modifica um
valor expandido de leitura/escrita no local, deve ter o cuidado de
deixar o valor em um estado saudável se ela falhar no meio do caminho.
Para obter exemplos de como trabalhar com valores expandidos, consulte
a infraestrutura de matriz padrão, em particular o arquivo
src/backend/utils/adt/array_expanded.c
.