Reviers, le 8 juin 2023

Après quelques discussions en interne, nous sommes tombés d’accord pour mettre en place une journée mensuelle sur le codage dans PostgreSQL pour les consultants et développeurs de Dalibo.

Le séminaire fut l’occasion d’une première journée, qui a été consacrée à la création d’une extension. Pour cela, nous avons créé deux fonctions en SQL, puis une fonction en C, et ajouté un opérateur. Cela nous a permis de comprendre comment créer et mettre à jour une extension, et voir ainsi tout son cycle de vie. Le dernier point a concerné la mise en place de tests unitaires pour cette extension. Cet article détaille tout ce que nous avons appris lors de cette journée.

NOTE DU 15/06/2023. Suite à la parution de cet article, Julien Rouhaud est revenu vers nous pour nous indiquer deux soucis dans la fonction C. Elle doit être déclarée STRICT si elle ne veut pas gérer les arguments NULL. De plus, la fonction telle qu’elle est écrite ne gère pas le dépassement de la valeur. Nous en avons profité pour améliorer notre fonction et corriger l’article. Un gros merci à Julien pour nous en avoir parlé.

Atelier Hack'PG à La Rochelle
Atelier Hack'PG à La Rochelle

Qu’est-ce qu’une extension ?

Une extension est un moyen d’ajouter des fonctionnalités à PostgreSQL. Avant l’arrivée de l’objet extension, il était coutume de les appeler « modules », voire modules contrib pour ceux livrés avec PostgreSQL. Sur d’autres applications, le terme d’« addon » est souvent utilisé.

À l’époque des modules, l’utilisateur disposait d’un script SQL d’installation et, dans les bons cas, d’un script de désinstallation. Les objets créés par ces scripts étaient mélangés aux objets utilisateurs (tables, vues, routines, etc) dans les bases comme au sein des sauvegardes. Il n’était pas simple de mettre à jour ces objets. Il n’était pas forcément simple de tous les supprimer d’un coup. Pour des petits modules, ce n’était pas vraiment un problème mais pour des modules comme PostGIS comprenant des centaines de fonctions, cela pouvait rapidement devenir un casse-tête.

Dimitri Fontaine a conçu l’idée d’extensions. Tous les objets d’une extension sont liés à cette extension. Il n’est pas possible de modifier ou supprimer ces objets directement, il est nécessaire de passer par l’extension pour les mettre à jour ou les supprimer tous. Une extension a une version, il est possible de la mettre à jour. Il est aussi possible d’obtenir une liste des extensions installées sur le système. Bref, un vrai système de gestion de ces addons apparaît avec la version 9.1 de PostgreSQL.

Pour un utilisateur, une extension est un regroupement d’objets SQL. L’extension doit d’abord être ajoutée au niveau du système d’exploitation. Les extensions fournies par PostgreSQL sont dans un paquet nommé par exemple postgresql15-contrib, les extensions fournies par d’autres développeurs sont disponibles sous la forme d’un paquet unique (par exemple sqlite_fdw-15) ou sous la forme de sources, généralement dans un dépôt git ou sur le site PGXN. Une fois disponibles, il est possible de les ajouter à une base de données avec l’ordre CREATE EXTENSION. Les supprimer se fait avec l’ordre DROP EXTENSION. Il est possible de modifier quelques méta-données, mais la force de ALTER EXTENSION tient à la possibilité de mise à jour d’une extension (avec la clause UPDATE).

Pour un développeur d’extension, cet objet est surtout un ensemble de fichiers :

  • un ou plusieurs fichiers .control, qui permettent d’indiquer des méta-données sur l’extension ;
  • un ou plusieurs fichiers SQL pour l’installation et la mise à jour des extensions ;
  • potentiellement un Makefile ;
  • dans le cas où des fonctions en C sont à compiler, les fichiers C en question.

Fichiers de contrôle

Un fichier de contrôle contient une ou plusieurs directives. Aucune n’est obligatoire. Son suffixe est forcément .control.

Il peut y avoir plusieurs fichiers de contrôle suite à des changements de version. Dans ce cas, le nom du fichier contiendra la version, par exemple monextension--0.2.control.

Voici une liste non exhaustive des directives :

  • directory, répertoire contenant les scripts SQL de l’extension (par défaut, le nom de l’extension) ;
  • default_version, la version à installer si elle n’est pas indiquée dans l’instruction CREATE EXTENSION ;
  • comment, commentaire sur l’extension, affiché par la vue pg_available_extensions ;
  • encoding, encodage des scripts SQL s’ils contiennent des caractères autres que ASCII ;
  • requires, liste de noms d’extensions dont cette extension est dépendante ;
  • relocatable, à true pour qu’un utilisateur puisse déplacer les objets d’une extension dans un autre schéma que celui de l’installation.

Ce ne sont évidemment pas les seules directives possibles. Nous en aborderons une autre dans cet article. Ils sont de toute façon tous détaillés dans le chapitre sur les extensions de la documentation de PostgreSQL.

Fichiers SQL

Ces fichiers ont un nom de la forme nomextension--version.sql ou nomextension--version1--version2.sql. Dans le premier cas, il s’agit d’un script d’installation de l’extension dans la version indiquée. Dans le deuxième cas, il s’agit d’un script SQL de mise à jour de l’extension pour aller de la version version1 à la version version2.

Ces fichiers peuvent contenir toute instruction SQL, sauf celles relatives à la gestion des transactions (BEGIN, COMMIT, ROLLBACK), ainsi que toute commande non exécutable dans une transaction (VACUUM par exemple).

Ils peuvent aussi contenir la méta commande \echo. Le but est de pouvoir afficher un message dans psql si quelqu’un essaie d’exécuter un script sans passer par l’instruction CREATE/ALTER EXTENSION adéquate. Cette méta-commande est ignorée par les instructions CREATE/ALTER EXTENSION.

Un script SQL a accès à quelques variables :

  • @extowner@, nom de l’utilisateur qui installe l’extension ;
  • @extschema@, nom du schéma d’installation de l’extension.

Fichier Makefile

Cela pourrait en étonner certain•e•s d’utiliser un fichier Makefile sans avoir de code en C mais dans les faits, cela facilite la vie pour l’installation des fichiers de l’extension dans les répertoires d’installation de PostgreSQL.

Le fichier est basé sur un système nommé PGXS que nous n’expliquerons pas ici. Pour plus de détails, vous pouvez vous référer à la documentation de PostgreSQL sur ce sujet.

Pour pouvoir utiliser la commande make et le système PGXS, il faut au préalable avoir installé le paquet postgresql15-devel. Les dépendances de ce paquet devraient installer tout le nécessaire pour compiler en C.

Première version de notre extension

Trêve de théorie, passons au codage de notre première extension.

Je vais la nommer monextension parce que mon imagination n’est pas aussi débordante que j’aimerais, et elle ne va avoir qu’une seule fonction écrite en SQL. La fonction est très basique, elle incrémente de 1 une valeur donnée en argument. C’est juste un exemple, vu que notre but est d’étudier les extensions, pas le SQL.

Voici le code du fichier monextension--1.0.sql :

\echo Ne pas exécuter ce script, mais passer par CREATE EXTENSION

CREATE OR REPLACE FUNCTION incremente(int)
RETURNS int
LANGUAGE sql
AS 'SELECT $1+1';

Le fichier de contrôle, nommé monextension.control va contenir uniquement deux informations : la version à installer par défaut et le commentaire. Ce qui nous donne :

comment = 'Mon extension'
default_version = '1.0'

Il ne nous manque plus que le Makefile pour installer le fichier SQL et le fichier de contrôle :

EXTENSION = monextension
DATA = monextension--1.0.sql

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

La variable EXTENSION indique le nom de l’extension, ce qui lui permet d’en déduire le nom du fichier contrôle. La variable DATA précise les fichiers de données à installer, donc ici les scripts SQL d’installation et de mise à jour. Le reste fait partie du système PGXS.

En lançant la commande make, petite déception :

$ make
make: Nothing to be done for 'all'.

En fait, c’est normal. make ne fait que compiler les fichiers C. Il n’y a pas de fichiers C, donc il n’y a rien à faire.

La commande make install réalise l’installation :

$ make install
/usr/bin/mkdir -p '/usr/pgsql-15/share/extension'
/usr/bin/mkdir -p '/usr/pgsql-15/share/extension'
/usr/bin/install -c -m 644 .//monextension.control '/usr/pgsql-15/share/extension/'
/usr/bin/install -c -m 644 .//monextension--1.0.sql  '/usr/pgsql-15/share/extension/'

Il est souvent nécessaire d’être root pour pouvoir copier les fichiers à leur destination.

Lorsque la commande réussit, nous voyons bien qu’il crée le répertoire share/extension dans le répertoire d’installation de PostgreSQL (s’il n’existait pas), puis qu’il y copie le fichier de contrôle et le script SQL d’installation.

Vérifions que l’extension est bien disponible. Pour cela, nous allons interroger la vue pg_available_extensions en filtrant sur notre extension :

guillaume=> select * from pg_available_extensions where name='monextension';
     name     | default_version | installed_version |    comment
--------------+-----------------+-------------------+---------------
 monextension | 1.0             |                   | Mon extension
(1 row)

L’extension est bien présente. Sa version par défaut est la 1.0 et elle n’est pas installée (colonne installed_version à NULL).

Nous pouvons maintenant l’ajouter et l’utiliser :

guillaume=# CREATE EXTENSION monextension;
CREATE EXTENSION
guillaume=# \dx+ monextension
Objects in extension "monextension"
      Object description
------------------------------
 function incremente(integer)
(1 row)

guillaume=# SELECT incremente(10);
 incremente
------------
         11
(1 row)

Avant toute chose, bien voir que je me suis connecté en tant que superutilisateur (visible par l’invite contenant # dans psql). L’extension n’étant pas déclarée trusted, seul un utilisateur ayant l’attribut SUPERUSER peut l’installer. L’extension a bien installé la fonction incremente() qui est fonctionnelle.

Comme indiqué plus haut, un utilisateur ne peut pas supprimer un objet d’une extension :

guillaume=> DROP FUNCTION incremente(integer);
ERROR:  cannot drop function incremente(integer) because extension monextension requires it
HINT:  You can drop extension monextension instead.

Le seul moyen de supprimer cette fonction revient à supprimer l’extension complète.

guillaume=# DROP EXTENSION monextension;
DROP EXTENSION
guillaume=# SELECT incremente(10);
ERROR:  function incremente(integer) does not exist
LINE 1: SELECT incremente(10);
               ^
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.

Une fois l’extension supprimée, il n’est plus possible d’utiliser la fonction car elle a été supprimée en cascade.

Complexifions notre extension

Nous allons maintenant créer une version 2.0 qui va ajouter une fonction SQL et un opérateur qui va l’exécuter. Cet exemple vient d’un cas d’un client qui souhaitait avoir la possibilité de ne pas avoir d’erreur en cas de division par zéro.

Pour cela, nous avons besoin d’un script SQL de mise à jour. Cette mise à jour consiste en l’ajout de la nouvelle fonction et du nouvel opérateur. Voici le contenu de ce script, nommé monextension--1.0--2.0.sql :

\echo Ne pas exécuter ce script, mais passer par CREATE EXTENSION

CREATE OR REPLACE FUNCTION division_sans_erreur(numeric, numeric)
RETURNS numeric
LANGUAGE sql
AS $$
SELECT CASE WHEN $2=0 THEN NULL ELSE $1/$2 END;
$$;

CREATE OPERATOR //
  (FUNCTION=division_sans_erreur,
   LEFTARG=numeric,
   RIGHTARG=numeric);

Il reste à mettre à jour le fichier de contrôle pour indiquer la nouvelle version par défaut :

default_version = '2.0'

ainsi que le fichier Makefile pour indiquer le deuxième script SQL :

DATA = monextension--1.0.sql monextension--1.0--2.0.sql

Après avoir exécuté le make install (qui doit montrer la copie des deux scripts SQL), nous pouvons tester l’extension. Comme j’ai supprimé l’extension précédemment, je vais devoir de nouveau la créer :

guillaume=# CREATE EXTENSION monextension;
CREATE EXTENSION
guillaume=# \dx+ monextension
      Objects in extension "monextension"
               Object description
------------------------------------------------
 function division_sans_erreur(numeric,numeric)
 function incremente(integer)
 operator //(numeric,numeric)
(3 rows)

guillaume=# select incremente(15);
 incremente
------------
         16
(1 row)

guillaume=# select 10//5;
      ?column?
--------------------
 2.0000000000000000
(1 row)

guillaume=# select 10//0;
 ?column?
----------

(1 row)

Cette nouvelle version a bien installé les différents objets SQL, et l’opérateur fonctionne bien.

Testons la mise à jour

Pour tester les différents scripts, nous allons supprimer la version actuellement installée :

guillaume=# DROP EXTENSION monextension;
DROP EXTENSION

Maintenant, installons la version 1.0. Pour cela, il suffit d’utiliser la clause VERSION en précisant la version à installer :

guillaume=# CREATE EXTENSION monextension VERSION '1.0';
CREATE EXTENSION
guillaume=# \dx+ monextension
Objects in extension "monextension"
      Object description
------------------------------
 function incremente(integer)
(1 row)

Le seul objet installé correspond bien à la version 1.0. Maintenant, mettons à jour en version 2.0 grâce à l’instruction ALTER EXTENSION ... UPDATE ... :

guillaume=# ALTER EXTENSION monextension UPDATE TO '2.0';
ALTER EXTENSION
guillaume=# \dx+ monextension
      Objects in extension "monextension"
               Object description
------------------------------------------------
 function division_sans_erreur(numeric,numeric)
 function incremente(integer)
 operator //(numeric,numeric)
(3 rows)

Et nous retrouvons bien les objets de la version 2.0 !

À présent, essayons l’inverse, à savoir repasser en version 1.0 :

guillaume=# ALTER EXTENSION monextension UPDATE TO '1.0';
ERROR:  extension "monextension" has no update path from version "2.0" to version "1.0"

Ça ne passe pas. Il n’existe pas de script monextension--2.0--1.0.sql, ni d’ensemble de scripts permettant d’arriver à ce résultat.

Faisons-le. Pour passer la version 2.0 à la version 1.0, il faut supprimer la nouvelle fonction et le nouvel opérateur. Cela nous donne le script SQL monextension--2.0--1.0.sql suivant :

\echo Ne pas exécuter ce script, mais passer par CREATE EXTENSION

DROP OPERATOR // (numeric,numeric);
DROP FUNCTION division_sans_erreur(numeric, numeric);

Ne pas oublier de modifier le fichier Makefile pour l’installation du nouveau fichier SQL.

Après le make install, testons :

guillaume=# ALTER EXTENSION monextension UPDATE TO '1.0';
ALTER EXTENSION
guillaume=# \dx+ monextension
Objects in extension "monextension"
      Object description
------------------------------
 function incremente(integer)
(1 row)

Parfait !

Créons une version C de incremente()

Pour aller plus loin dans les possibilités du système des extensions, nous allons maintenant créer une version C de la fonction incremente().

Pour cela, nous allons créer un fichier monextension.c qui va contenir le code suivant :

#include "postgres.h"
#include "fmgr.h"

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(incremente);

Datum
incremente(PG_FUNCTION_ARGS)
{
  int32   valeur = PG_GETARG_INT32(0);

  PG_RETURN_INT32(valeur + 1);
}

Voici quelques explications sur ce code.

Les deux lignes #include indiquent deux fichiers d’entête de PostgreSQL. Le premier contient un ensemble de définitions utiles pour tout programme ou extension PostgreSQL, le second est plus spécifique aux fonctions écrites en C, et donc les extensions.

PG_MODULE_MAGIC (lien doxygen fmgr.h permet d’ajouter un nombre magique à la bibliothèque partagée qui sera compilée pour qu’au chargement, PostgreSQL puisse s’assurer qu’il s’agit bien d’une extension et que la version de compilation correspond bien à la version du serveur.

PG_FUNCTION_INFO_V1 (lien doxygen fmgr.h) permet de déclarer les fonctions intégrables dans des fonctions d’une base de données. V1 correspond à la version interne d’appels des fonctions, sachant qu’il n’existe que deux versions (0, qui est obsolète, et 1, qui est l’actuelle).

PG_GETARG_INT32 et PG_RETURN_INT32 permettent grossièrement de décoder une valeur SQL en valeur C et inversement. Ces fonctions existent pour tous les types de données SQL et leurs équivalents en C. Une liste complète est disponible dans le fichier fmgr.h.

Le code de la fonction en C est assez simple. Nous récupérons l’argument sous la forme d’un type C int32. Nous lui ajoutons 1 et renvoyons le résultat.

Il nous faut maintenant le script SQL de mise à jour pour la définition de la fonction C. Le voici, nommé monextension--2.0--3.0.sql :

\echo Ne pas exécuter ce script, mais passer par CREATE EXTENSION

CREATE OR REPLACE FUNCTION incremente(int)
RETURNS int
AS '$libdir/monextension', 'incremente'
STRICT
LANGUAGE C;

La définition de la fonction SQL doit indiquer le nom de la bibliothèque partagée (ici, $libdir/monextension), et le nom de la fonction exportée de cette bibliothèque. Dans notre cas, le nom de la fonction SQL et le nom de la fonction C sont identiques mais ce n’est en rien une obligation. La clause STRICT permet de ne pas avoir à gérer une valeur NULL dans le code de la fonction C. Dans le cas d’une fonction STRICT, PostgreSQL renverra immédiatement la valeur NULL quand un des arguments de la fonction vaut NULL.

Le fichier Makefile va un peu plus changer. Le voici en intégralité :

EXTENSION = monextension
MODULE_big = monextension
OBJS = monextension.o
DATA = monextension--1.0.sql
DATA += monextension--1.0--2.0.sql
DATA += monextension--2.0--1.0.sql
DATA += monextension--2.0--3.0.sql

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

La grosse différence réside en l’ajout des variables MODULE_big et OBJS. Ces deux variables indiquent ce qu’il faut compiler et en quoi.

Les plus observateurs et observatrices d’entre vous remarqueront que j’ai remplacé la longue ligne DATA en plusieurs. Je trouve ça plus simple à lire, c’est un goût personnel, les deux sont possibles.

Le fichier de contrôle se voit aussi ajouter une nouvelle variable. Voici le contenu complet :

comment = 'Mon extension'
default_version = '3.0'
module_pathname = '$libdir/monextension'

La variable supplémentaire est module_pathname. Elle indique la présence d’une bibliothèque.

Compilons notre code :

$ make
gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wimplicit-fallthrough=3 -Wcast-function-type -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -Wno-format-truncation -Wno-stringop-truncation -O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -march=x86-64-v2 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -fPIC -I. -I./ -I/usr/pgsql-15/include/server -I/usr/pgsql-15/include/internal  -D_GNU_SOURCE -I/usr/include/libxml2  -I/usr/include  -c -o monextension.o monextension.c
gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wimplicit-fallthrough=3 -Wcast-function-type -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -Wno-format-truncation -Wno-stringop-truncation -O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -march=x86-64-v2 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -fPIC -shared -o monextension.so monextension.o -L/usr/pgsql-15/lib  -Wl,--as-needed -L/usr/lib64  -L/usr/lib64 -Wl,--as-needed -Wl,-rpath,'/usr/pgsql-15/lib',--enable-new-dtags
/usr/lib64/ccache/clang -Wno-ignored-attributes -fno-strict-aliasing -fwrapv -Xclang -no-opaque-pointers -Wno-unused-command-line-argument -Wno-compound-token-split-by-macro -Wno-deprecated-non-prototype -O2  -I. -I./ -I/usr/pgsql-15/include/server -I/usr/pgsql-15/include/internal  -D_GNU_SOURCE -I/usr/include/libxml2  -I/usr/include -flto=thin -emit-llvm -c -o monextension.bc monextension.c

Nous nous retrouvons donc avec un fichier monextension.so qui est la bibliothèque partagée contenant notre code. Elle sera copiée lors de l’installation.

Donc maintenant, installons-le :

[root@localhost monextension]# make install
/usr/bin/mkdir -p '/usr/pgsql-15/lib'
/usr/bin/mkdir -p '/usr/pgsql-15/share/extension'
/usr/bin/mkdir -p '/usr/pgsql-15/share/extension'
/usr/bin/install -c -m 755  monextension.so '/usr/pgsql-15/lib/monextension.so'
/usr/bin/install -c -m 644 .//monextension.control '/usr/pgsql-15/share/extension/'
/usr/bin/install -c -m 644 .//monextension--1.0.sql .//monextension--1.0--2.0.sql .//monextension--2.0--1.0.sql .//monextension--2.0--3.0.sql  '/usr/pgsql-15/share/extension/'
/usr/bin/mkdir -p '/usr/pgsql-15/lib/bitcode/monextension'
/usr/bin/mkdir -p '/usr/pgsql-15/lib/bitcode'/monextension/
/usr/bin/install -c -m 644 monextension.bc '/usr/pgsql-15/lib/bitcode'/monextension/./
cd '/usr/pgsql-15/lib/bitcode' && /usr/bin/llvm-lto -thinlto -thinlto-action=thinlink -o monextension.index.bc monextension/monextension.bc

Nous pouvons enfin mettre à jour l’extension et utiliser notre fonction en C :

guillaume=# ALTER EXTENSION monextension UPDATE TO '3.0';
ALTER EXTENSION
guillaume=# \dx+ monextension
      Objects in extension "monextension"
               Object description
------------------------------------------------
 function division_sans_erreur(numeric,numeric)
 function incremente(integer)
 operator //(numeric,numeric)
(3 rows)

guillaume=# SELECT lanname FROM pg_proc p
  JOIN pg_language l ON l.oid=p.prolang
  WHERE proname='incremente';

 lanname
---------
 c
(1 row)

guillaume=# SELECT incremente(10);
 incremente
------------
         11
(1 row)

Comme nous avons déclaré la fonction comme STRICT, elle gère bien les valeurs NULL :

guillaume=# SELECT incremente(NULL);
 incremente
------------

(1 row)

Néanmoins, un cas pourrait ne pas fonctionner, celui de la valeur maximale d’une entier sur 32 bits :

guillaume=# SELECT incremente(2147483647);
 incremente
-------------
 -2147483648
(1 row)

En effet, notre fonction ne fait pas attention au dépassement de la valeur d’un entier sur 32 bits. Il nous faut donc corriger notre fonction ainsi :

Datum
incremente(PG_FUNCTION_ARGS)
{
  int32   valeur = PG_GETARG_INT32(0);

  if (valeur == PG_INT32_MAX)
  {
    elog (ERROR, 'valeur maximale dépassée après incrément');
  }

  PG_RETURN_INT32(valeur + 1);
}

elog() est une fonction interne de PostgreSQL permettant d’envoyer une trace au gestionnaire des traces et, suivant le niveau, d’annuler l’exécution d’une requête.

Après compilation et installation de la nouvelle bibliothèque, voici le résultat :

guillaume=# select incremente(10);
 incremente
------------
         11
(1 row)

guillaume=# select incremente(2147483647);
ERROR:  valeur maximale dépassée après incrément

Tests unitaires pour une extension

Il est possible de préparer des tests unitaires pour une extension.

Pour cela, les scripts SQL à exécuter vont dans un sous-répertoire sql, et les résultats vont dans un sous-répertoire expected. Voici un exemple de script SQL :

CREATE EXTENSION monextension;
SELECT incremente(10);

et du résultat :

CREATE EXTENSION monextension;
SELECT incremente(10);
 incremente
------------
         11
(1 row)

Le fichier Makefile doit être modifié pour indiquer les scripts à exécuter (sans l’extension .sql). Cela se fait via la variable REGRESS :

REGRESS = incremente

Il ne reste plus qu’à lancer le test avec la commande make installcheck :

$ make installcheck
echo "+++ regress install-check in  +++" && /usr/pgsql-15/lib/pgxs/src/makefiles/../../src/test/regress/pg_regress --inputdir=./ --bindir='/usr/pgsql-15/bin'    --dbname=contrib_regression incremente
+++ regress install-check in  +++
(using postmaster on Unix socket, default port)
============== dropping database "contrib_regression" ==============
SET
DROP DATABASE
============== creating database "contrib_regression" ==============
CREATE DATABASE
ALTER DATABASE
ALTER DATABASE
ALTER DATABASE
ALTER DATABASE
ALTER DATABASE
ALTER DATABASE
============== running regression test queries        ==============
test incremente                   ... ok           25 ms

=====================
 All 1 tests passed.
=====================

Comme indiqué dans la sortie, une base contrib_regression est créée. Il faut donc que l’utilisateur de connexion au serveur PostgreSQL ait le droit de créer des bases.

En cas d’erreur, un message comme celui qui suit s’afficherait :

============== running regression test queries        ==============
test incremente                   ... FAILED       23 ms

======================
 1 of 1 tests failed.
======================

The differences that caused some tests to fail can be viewed in the
file "/home/guillaume/monextension/regression.diffs".  A copy of the test summary that you see
above is saved in the file "/home/guillaume/monextension/regression.out".

make: *** [/usr/pgsql-15/lib/pgxs/src/makefiles/pgxs.mk:433: installcheck] Error 1

Il ne restera plus qu’à regarder le fichier regression.diffs pour rechercher l’origine du problème.

Reste au lecteur à ajouter des tests sur les cas limites comme un incrément sur NULL et un incrément sur la valeur maximale d’un entier.

Pour aller plus loin

Écrire des fonctions en C est plus dangereux, notamment quand on manipule des pointeurs. Cependant, cela a aussi des avantages. Nous avons eu le cas d’un client qui avait une fonction mathématique écrite en PL/pgsql. Cette fonction codée en C est bien plus rapide. Il n’est pas étonnant que beaucoup des fonctions de PostGIS soient écrites en C.

Un autre intérêt est l’accès à des fonctions internes de PostgreSQL, comme les hooks.

Sur cette journée du séminaire, nous avons eu le temps d’aller plus loin en examinant un peu le concept des hooks, mais cet article est déjà assez long. Le système des hooks fera l’objet d’un deuxième article.

Pour les curieux, le code de l’extension se trouve sur le dépôt GitHub des journées de hacking PostgreSQL.


DALIBO

DALIBO est le spécialiste français de PostgreSQL®. Nous proposons du support, de la formation et du conseil depuis 2005.