31.11. Tipos definidos pelo usuário

PostgreSQL 14.5: Tipos de dados definidos pelo usuário

Conforme descrito na Seção 31.2, o PostgreSQL pode ser estendido para dar suporte a novos tipos de dado. Esta seção descreve como definir novos tipos base, que são tipos de dado definidos abaixo do nível da linguagem SQL. A criação de um novo tipo base requer a implementação de funções para operar o tipo em uma linguagem de baixo nível, geralmente a linguagem C.

Os exemplos desta seção podem ser encontrados nos arquivos complex.sql e complex.c no diretório src/tutorial da distribuição do código fonte. [1] Para obter informações sobre como executar os exemplos deve ser visto o arquivo README presente neste diretório.

Os tipos definidos pelo usuário devem possuir função de entrada e de saída, sempre. Estas funções determinam como o tipo aparece nas cadeias de caracteres (na entrada pelo usuário e na saída para o usuário), e como estes tipos ficam organizados na memória. A função de entrada recebe como argumento uma cadeia de caracteres terminada por nulo, e retorna a representação interna (em memória) do tipo. A função de saída recebe como argumento a representação interna do tipo, e retorna uma cadeia de caracteres terminada por nulo. Se for desejado fazer algo mais com o tipo que simplesmente armazená-lo, devem ser fornecidas funções adicionais para implementar as operações desejadas para o tipo.

Supondo que se deseja definir o tipo complex para representar os números complexos, uma forma natural de representar um número complexo na memória seria através da seguinte estrutura na linguagem C:

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

É necessário torná-lo um tipo passado por referência, uma vez que este tipo é muito grande para caber em um único valor Datum.

Para representação externa do tipo foi escolhida uma cadeia de caracteres na forma (x,y).

Geralmente não é difícil escrever as funções de entrada e de saída, em especial a função de saída. Mas ao definir a cadeia de caracteres para representação externa do tipo, deve ser lembrado que pode ser necessário escrever um analisador completo e robusto para esta representação como função de entrada. 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 complex: \"%s\"",
                        str)));

    result = (Complex *) palloc(sizeof(Complex));
    result->x = x;
    result->y = y;
    PG_RETURN_POINTER(result);
}

A função de saída pode ser simplesmente:

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);
}

Deve-se ter o cuidado de tornar as funções de entrada e saída inversas entre si. Se isto não for feito, vão ocorrer sérios problemas quando os dados forem salvos em um arquivo e depois lidos a partir deste arquivo. Este é um problema particularmente comum quando estão envolvidos números de ponto flutuante.

Um tipo definido pelo usuário pode possuir, opcionalmente, rotinas de entrada e saída binárias. Normalmente a entrada e saída binárias são mais rápidas, mas menos portáveis que a entrada e saída na forma de texto. Assim como a entrada e saída na forma de texto, a definição exata da representação binária fica a cargo do desenvolvedor. A maioria dos tipos de dado nativos tentam fornecer uma representação binária independente de máquina. Para o tipo complex será tirado proveito dos conversores para entrada e saída binária do tipo 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));
}

Para definir o tipo complex é necessário criar as funções de entrada e saída definidas pelo usuário antes de criar o tipo:

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;

Deve ser observado que as declarações das funções de entrada e de saída fazem referência a um tipo ainda não definido. Isto é permitido, mas causa mensagens de advertência que podem ser ignoradas. A função de entrada deve vir primeiro.

Por fim o tipo de dado pode ser declarado:

CREATE TYPE complex (
   internallength = 16,
   input = complex_in,
   output = complex_out,
   receive = complex_recv,
   send = complex_send,
   alignment = double
);

Ao se definir um novo tipo base, o PostgreSQL disponibiliza automaticamente suporte para matrizes deste tipo. Por motivos históricos o tipo da matriz possui o mesmo nome do tipo base, com o caractere sublinhado (_) prefixado.

Uma vez que o tipo de dado tenha passado a existir, podem ser declaradas funções adicionais para disponibilizar operações úteis para o tipo de dado. Em seguida podem ser definidos operadores por cima das funções e, se houver necessidade, podem ser criadas classes de operadores para dar suporte à indexação deste tipo de dado. Estas camadas adicionais são vistas nas próximas seções.

Se os valores do tipo de dado puderem exceder algumas poucas centenas de bytes em tamanho (na forma interna), o tipo de dado deve ser tornado fatiável (TOAST-able) (consulte a Seção 50.2). Para se fazer isto, a representação interna deve seguir a organização padrão para dados de comprimento variável: os primeiros quatro bytes devem ser um int32 contendo o comprimento total, em bytes, do datum (incluindo a si próprio). As funções C que operam no tipo de dado devem tomar o cuidado de desempacotar os valores fatiados manuseados utilizando PG_DETOAST_DATUM (Este detalhe é geralmente escondido definindo-se macros GETARG específicas para o tipo). Depois, ao executar o comando CREATE TYPE, o comprimento interno deve ser especificado como variable, e selecionada a opção de armazenamento apropriada.

Para obter mais detalhes deve ser consultada a descrição do comando CREATE TYPE.

Notas

[1]

Para gerar o arquivo complex.sql primeiro este diretório deve ser tornado o diretório corrente, e depois executado make, conforme descrito no arquivo README. (N. do T.)

SourceForge.net Logo CSS válido!