Comme cela est décrit dans la Section 37.2, 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 = psprintf("(%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. Le type tableau a habituellement 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 la représentation interne du type de données est de longueur variable, la
représentation interne doit poursuivre l'organisation standard pour une
donnée de longueur variable : les quatre premiers octets doivent être
un champ char[4]
qui n'est jamais accédé directement (nommé
vl_len_
). Vous devez utiliser la macro
SET_VARSIZE()
pour enregistrer la taille totale de la
donnée (ceci incluant le champ de longueur lui-même) dans ce champ et
VARSIZE()
pour la récupérer. (Ces macros existent parce
que le champ de longueur pourrait être encodé suivant la plateforme.)
Pour plus de détails, voir la description de la commande CREATE TYPE.
Si les valeurs du type de données varient en taille (sous la forme interne), il est généralement préférable que le type de données soit marqué comme TOAST-able (voir Section 67.2). Vous devez le faire même si les données sont trop petites pour être compressées ou stockées en externe car TOAST peut aussi gagner de la place sur des petites données en réduisant la surcharge de l'en-tête.
Pour supporter un stockage TOAST, les fonctions C opérant
sur le type de données doivent toujours faire très attention à déballer les
valeurs dans le TOAST qui leur sont données par
PG_DETOAST_DATUM
. (Ce détail est généralement caché en
définissant les macros GETARG_DATATYPE_P
spécifiques au
type.) Puis, lors de l'exécution de la commande CREATE
TYPE
, indiquez la longueur interne comme
variable
et sélectionnez certaines options de stockage
spécifiques autres que plain
.
Si l'alignement n'est pas important (soit seulement pour une fonction
spécifique soit parce que le type de données spécifie un alignement par
octet), alors il est possible d'éviter
PG_DETOAST_DATUM
. Vous pouvez utiliser
PG_DETOAST_DATUM_PACKED
à la place (habituellement
caché par une macro GETARG_DATATYPE_PP
) et utiliser les
macros VARSIZE_ANY_EXHDR
et
VARDATA_ANY
pour accéder à un datum potentiellement
packagé.
Encore une fois, les données renvoyées par ces macros ne sont pas alignées
même si la définition du type de données indique un alignement. Si
l'alignement est important pour vous, vous devez passer par l'interface
habituelle, PG_DETOAST_DATUM
.
Un ancien code déclare fréquemment vl_len_
comme un champ de type int32
au lieu de
char[4]
. C'est correct tant que la définition de la structure
a d'autres champs qui ont au moins un alignement int32
.
Mais il est dangereux d'utiliser une telle définition de structure en
travaillant avec un datum potentiellement mal aligné ; le compilateur
peut le prendre comme une indication pour supposer que le datum est en
fait aligné, ceci amenant des « core dump » sur des architectures qui
sont strictes sur l'alignement.
Une autre fonctionnalité, activée par le support des TOAST est la possibilité d'avoir une représentation des données étendue en mémoire qui est plus agréable à utiliser que le format enregistré sur disque. Le format de stockage varlena standard ou plat (« flat ») est en fait juste un ensemble d'octets ; par exemple, il ne peut pas contenir de pointeurs car il pourrait être copié à d'autres emplacements en mémoire. Pour les types de données complexes, le format plat pourrait être assez coûteux à utiliser, donc PostgreSQL fournit une façon d'« étendre » le format plat en une représentation qui est plus confortable à utiliser, puis passe ce format en mémoire entre les fonctions du type de données.
Pour utiliser le stockage étendu, un type de données doit fournir un format
étendu qui suit les règles données dans
src/include/utils/expandeddatum.h
, et fournir des
fonctions pour « étendre » une valeur varlena plate en un format
étendu et « aplatir » un format étendu en une représentation
varlena standard. Puis s'assurer que toutes les fonctions C pour le type de
données puissent accepter chaque représentation, si possible en
convertissant l'une en l'autre immédiatement à réception. Ceci ne nécessite
pas de corriger les fonctions existantes pour le type de données car la
macro standard PG_DETOAST_DATUM
est définie pour
convertir les entrées étendues dans le format plat standard. De ce fait, les
fonctions existantes qui fonctionnent avec le format varlena plat
continueront de fonctionner, bien que moins efficacement, avec des entrées
étendues ; elles n'ont pas besoin d'être converties jusqu'à ou à moins
que d'avoir de meilleures performances soit important.
Les fonctions C qui savent comment fonctionner avec une représentation
étendue tombent typiquement dans deux catégories : celles qui savent
seulement gérer le format étendu et celles qui peuvent gérer les deux
formats. Les premières sont plus simples à écrire mais peuvent être moins
performantes car la conversion d'une entrée à plat vers sa forme étendue par
une seule fonction pourrait coûter plus que ce qui est gagné par le format
étendu. Lorsque seul le format étendu est géré, la conversion des entrées à
plat vers le format étendu peut être cachée à l'intérieur d'une macro de
récupération des arguments, pour que la fonction n'apparaisse pas plus
complexe qu'une fonction travaillant avec le format varlena standard. Pour
gérer les deux types d'entrée, écrire une fonction de récupération des
arguments qui peut enelver du toast les entrées varlena externes, à court
en-tête et compressées, mais qui n'étend pas les entrées. Une telle fonction
peut être définie comme renvoyant un pointeur vers une union du fichier
varlena à plat et du format étendu. Ils peuvent utiliser la macro
VARATT_IS_EXPANDED_HEADER()
pour déterminer le format
reçu.
L'infrastructure TOAST permet non seulement de distinguer les valeurs varlena standard des valeurs étendues, mais aussi de distinguer les pointeurs « read-write » et « read-only » vers les valeurs étendues. Les fonctions C qui ont seulement besoin d'examiner une valeur étendue ou qui vont seulement la changer d'une façon sûre et non visible sémantiquement, doivent ne pas faire attention au type de pointeur qu'elles ont reçus. Les fonctions C qui produisent une version modifiée d'une valeur en entrée sont autorisées à modifier une valeur étendue en entrée directement si elles reçoivent un pointeur read-only ; dans ce cas, elles doivent tout d'abord copier la valeur pour produire la nouvelle valeur à modifier. Une fonction C qui a construit une nouvelle valeur étendue devrait toujours renvoyer un pointeur read-write vers ce dernier. De plus, une fonction C qui modifie une valeur étendue en read-write devrait faire attention à laisser la valeur dans un état propre s'il échoue en chemin.
Pour des exemples de code sur des valeurs étendues, voir l'infrastructure
sur les tableaux standards, tout particulièrement
src/backend/utils/adt/array_expanded.c
.