33.11. Types utilisateur
Comme cela est décrit dans la Section 33.2,
« Le système des types de PostgreSQL™ », PostgreSQL™ peut être étendu pour
supporter de nouveaux types de données. Cette section décrit la
définition de nouveaux types basiques. Ces types de données sont
définis en-dessous du SQL. Créer
un nouveau type requiert d'implanter des fonctions dans un langage de
bas niveau, généralement le C.
Les exemples de cette section sont disponibles dans complex.sql et complex.c du
répertoire src/tutorial de la distribution.
Voir le fichier README de ce répertoire
pour les instructions d'exécution des exemples.
Un type utilisateur doit toujours posséder des fonctions d'entrée et
de sortie. Ces fonctions déterminent la présentation du type en
chaînes de caractères (pour la saisie par l'utilisateur et le renvoi
à l'utilisateur) et son organisation en mémoire. La fonction d'entrée
prend comme argument une chaîne de caractères terminée par NULL et
retourne la représentation interne (en mémoire) du type. La fonction
de sortie prend en argument la représentation interne du type et
retourne une chaîne de caractères terminée par NULL.
Il est possible de faire plus que stocker un type, mais il faut pour
cela implanter des fonctions supplémentaires gérant les opérations
souhaitées.
Soit le cas d'un type complex représentant
les nombres complexes. Une façon naturelle de représenter un nombre
complexe en mémoire passe par la structure C suivante :
typedef struct Complex {
double x;
double y;
} Complex;
Ce type ne pouvant tenir sur une simple valeur Datum, il sera passé par référence.
La représentation externe du type se fera sous la forme de la chaîne
(x,y).
En général, les fonctions d'entrée et de sortie ne sont pas
compliquées à écrire, particulièrement la fonction de sortie. Mais
lors de la définition de la représentation externe du type par une
chaîne de caractères, il faudra peut-être écrire un analyseur complet
et robuste, comme fonction d'entrée, pour cette représentation. 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 s'écrire simplement :
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);
}
Il est particulièrement important de veiller à ce que les fonctions
d'entrée et de sortie soient bien inversées l'une par rapport à
l'autre. Dans le cas contraire, de grosses difficultés pourraient
apparaître lors de la sauvegarde de la base dans un fichier en vue
d'une future relecture de ce fichier. Ceci est un problème
particulièrement fréquent lorsque des nombres à virgule flottante
entrent en jeu.
De manière optionnelle, un type utilisateur peut fournir 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. Comme avec les entrées/sorties textuelles, c'est
l'utilisateur qui définit précisément la représentation binaire
externe. La plupart des types de données intégrés tentent de fournir
une représentation binaire indépendante de la machine. Dans le cas du
type complex, des convertisseurs
d'entrées/sorties binaires pour le type float8 sont utilisés :
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));
}
Lorsque les fonctions d'entrée/sortie sont écrites et compilées en
une bibliothèque partagée, le type complex
peut être défini en SQL. Tout d'abord, il est déclaré comme un type
shell :
CREATE TYPE complex;
Ceci sert de paramètre qui permet de mettre en référence le type
pendant la définition de ses fonctions E/S. Les fonctions E/S peuvent
alors être définies :
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;
La définition du type de données peut ensuite être fournie
complètement :
CREATE TYPE complex (
internallength = 16,
input = complex_in,
output = complex_out,
receive = complex_recv,
send = complex_send,
alignment = double
);
Quand un nouveau type de base est défini, PostgreSQL™ fournit automatiquement le
support pour des tableaux de ce type. Pour des raisons historiques,
le type tableau a le nom du type de base préfixé par un caractère
souligné (_).
Lorsque le type de données existe, il est possible de déclarer les
fonctions supplémentaires de définition des opérations utiles pour ce
type. Les opérateurs peuvent alors être définis par dessus ces
fonctions et, si nécessaire, des classes d'opérateurs peuvent être
créées pour le support de l'indexage du type de données. Ces couches
supplémentaires sont discutées dans les sections suivantes.
Si les valeurs du type de données peuvent excéder une taille de
quelques centaines d'octets (sous la forme interne), le type de
données doit être marqué comme TOAST-able. Pour cela, la
représentation interne doit suivre le cadre standard des données à
longueur variable : les quatre premiers octets sont un int32 contenant la longueur totale en octets de la
donnée (incluant les quatre premiers octets). Les fonctions C opérant
sur le type de données doivent être attentives à déballer toutes les
valeurs toastées des données (ce détail est généralement masqué par
la définition de macros GETARG
spécifiques). Puis, lors de l'exécution de la commande
CREATE TYPE
, la longueur interne
sera spécifiée comme variable et l'option de
stockage en mémoire appropriée sera choisie.
Pour plus de détails, voir la description de la commande CREATE
TYPE.