31.11. Types définis par l'utilisateur

Décrit dans la Section 31.2, PostgreSQL peut être étendu pour supporter de nouveaux types de données. Cette section décrit comment définir de nouveaux types de base, qui sont des types de données définis en-dessous du niveau du langage SQL. Créer un nouveau type de base requiert l'implémentation de fonctions pour opérer sur le type dans un langage de base du niveau du C.

Les exemples de cette section sont disponibles dans complex.sql et complex.c du répertoire src/tutorial de la distribution des sources. Voir le fichier README de ce répertoire pour les instructions d'exécution des exemples.

Un type défini par l'utilisateur doit toujours avoir des fonctions d'entrée et de sortie. Ces fonctions déterminent comment le type apparaît dans les chaînes de caractères (pour l'entrée par l'utilisateur et le renvoi à l'utilisateur) et comment ce type est organisé en mémoire. La fonction d'entrée prend comme argument une chaîne de caractères terminée par NULL et renvoie la représentation interne (en mémoire) du type. La fonction de sortie prend comme argument la représentation interne du type et renvoie une chaîne de caractères terminée par NULL. Si vous voulez faire plus avec le type que simplement l'enregistrer, vous devez apporter des fonctions supplémentaires pour implémenter toutes opérations que vous souhaitez avoir pour ce type.

Supposons que nous voulions définir un type complex représentant les nombres complexes. Une façon naturelle de représenter un nombre complexe en mémoire serait la structure C suivante :

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

Nous aurons besoin d'utiliser ce type par référence car il est trop important pour tenir sur une seule valeur Datum.

Comme représentation externe du type sous forme de chaîne, nous choisissons une chaîne de la forme (x,y).

Habituellement, les fonctions d'entrée et de sortie ne sont pas compliquées à écrire, surtout la fonction de sortie. Mais en définissant la représentation externe du type par une chaîne, souvenez-vous que vous devez éventuellement écrire un analyseur complet et robuste pour cette représentation en tant que fonction d'entrée. Par exemple :

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 complex: \"%s\"",
                        str)));

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

La fonction de sortie peut simplement s'écrire :

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

Vous devriez faire attention en écrivant des fonctions d'entrée et de sortie inverses l'une de l'autre. Sinon, vous aurez de graves problèmes quand vous aurez besoin de sauvegarder votre base de données dans un fichier et ensuite de le relire. Ceci est un problème particulièrement fréquent quand des nombres à virgule flottante sont concernés.

De manière optionnelle, un type défini par l'utilisateur peut apporter des routines d'entrée et de sortie binaires. Les entrées/sorties binaires sont normalement plus rapides mais moins portables que les entrées/sorties textuelles. Avec les entrées/sorties textuelles, c'est à vous de définir exactement la représentation binaire externe. La plupart des types de données intégrés essaient d'apporter une représentation binaire indépendante de la machine. Pour complex, nous allons revenir aux convertisseurs d'entrées/sorties binaires pour le type 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));
}

Pour définir le type complex, nous avons besoin de créer les fonctions d'entrées/sorties définies par l'utilisateur avant de créer le type :

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;

Notez que la déclaration des fonctions d'entrée et de sortie doit pouvoir référencer un type non encore défini. Ceci est permis mais provoque des messages d'avertissement qui peuvent être ignorés. La fonction en entrée doit d'abord apparaître.

Finalement, nous pouvons déclarer le type de données :

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

Quand vous définissez un nouveau type de base, PostgreSQL fournit automatiquement le support pour des tableaux de ce type. Pour des raisons historiques, le type tableau a le même nom que le type de base avec un caractère souligné (_) en préfixe.

Une fois que le type de données existe, nous pouvons déclarer les fonctions supplémentaires pour apporter des opérations utiles pour ce type de données. Les opérateurs peuvent alors être définis au-dessus de ces fonctions et, si nécessaire, les classes d'opérateurs peuvent aussi être créées pour apporter le support de l'indexage du type de données. Ces couches supplémentaires sont discutées dans les sections suivantes.

Si les valeurs de votre type de donnée peuvent excéder une taille de quelques centaines d'octets (sous la forme interne), vous devriez marquer le type de données comme TOAST-able. Pour cela, la représentation interne doit suivre le cadre standard des données à longueur variable : les quatre premiers octets doivent être un int32 contenant la longueur totale en octets de la donnée (lui-même inclus). Les fonctions C opérant sur le type de données doivent faire bien attention à déballer toutes les valeurs toast des données (ce détail peut normalement être cachée dans les macros GETARG). Puis, quand on exécute la commande CREATE TYPE, spécifiez la longueur interne comme variable et choisissez l'option de stockage en mémoire appropriée.

Pour plus de détails, voir la description de la commande CREATE TYPE.