Reviers, le 26 octobre 2023
Après les extensions et le module d’archivage, il nous a semblé intéressant de nous pencher sur la création d’une application cliente en C. Cette journée a elle aussi été réalisée à distance.
Driver C
Comme tout autre langage, le langage C a besoin d’un driver pour accéder à une base de données. Le driver du langage C est fourni directement par les développeurs de PostgreSQL, tout simplement parce qu’ils en ont besoin pour les différents outils qu’ils fournissent aussi (par exemple psql et pg_dump). Ce driver a pour nom libpq et est disponible sous la forme d’une bibliothèque partagée. Sur un PC qui ne dispose pas de PostgreSQL, il est possible de l’installer. Par exemple, sous RedHat et en tant que l’utilisateur root, cette commande devrait l’installer :
# dnf install libpq
Sur le PC qui compile l’application utilisant le driver, il est nécessaire d’avoir un deuxième paquet, installable avec la commande suivante :
# dnf install libpq-devel
À quoi sert justement un driver ? Son but est simple : permettre de se connecter à une base PostgreSQL, d’exécuter des requêtes et d’en récupérer les résultats. Il dispose donc d’une API qui est détaillée dans le chapitre « libpq — C Library ».
Dépôt, tags, fichiers
Comme d’habitude sur cette série d’articles « Hack’PG », le code des exemples est disponible sur le dépôt GitHub des journées de hacking PostgreSQL. Nous avons ajouté des tags pour chaque étape.
Le squelette d’un programme C se trouve au tag
j3e0.
Par manque d’imagination, le programme va s’appeler client
. Nous avons besoin
des deux fichiers que voici.
Le fichier C
(client.c)
contient le code de notre application cliente. Le fichier
Makefile
permet de compiler facilement le fichier C. Son contenu diffère un peu des
fichiers Makefile
déjà abordés dans cette série d’articles. En voici son
contenu :
PROGRAM = client
OBJS = client.o
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
Nous nous basons toujours sur le système PGXS pour la compilation. De ce fait,
il est aussi nécessaire d’avoir le paquet postgresql16-devel
(ou celui qui
correspond à votre version et à votre distribution).
Après compilation du fichier C avec la commande make
, son exécution devrait
vous amener le message suivant :
$ ./client
ca marche ?
Permettre l’utilisation de la libpq
Pour utiliser la libpq, nous allons avoir besoin de déclarer son fichier d’entête. Cela se fait avec cette ligne dans le fichier C :
#include "libpq-fe.h"
Nous devons aussi indiquer à l’éditeur de liens qu’il faut lier le programme compilé à la libpq. Cela se fait avec ces deux lignes dans le fichier Makefile :
PG_CPPFLAGS = -I$(libpq_srcdir)
PG_LIBS = $(libpq_pgport)
Toute opération de compilation devrait maintenant utiliser en plus la libpq.
Se connecter à une base PostgreSQL
La libpq propose plusieurs fonctions pour se connecter à un serveur PostgreSQL. Nous allons utiliser la fonction PQconnectdb() car elle demande un seul argument. Cet argument est la chaîne de connexion au serveur PostgreSQL. Par exemple :
host=localhost user=guillaume dbname=b1
ou l’équivalent sous la forme d’une URI :
postgresql://guillaume@localhost/b1
Cette fonction renvoie une structure PGCon qui sera ensuite utilisée par d’autres fonctions, par exemple pour exécuter des requêtes.
Quant à la déconnexion, il n’existe qu’une seule fonction, appelée PQfinish(). Elle requiert en argument la structure PGCon, indiquant ainsi la connexion à fermer.
Le tag j3e1 correspond au code d’un client qui se connecte, puis se déconnecte aussitôt. Pour voir que le serveur reçoit bien une demande de connexion, puis de déconnexion, il suffit de configurer les paramètres suivants sur le serveur PostgreSQL :
log_connections = on
log_disconnections = on
Le fichier journee3.conf contient une configuration des traces intéressantes dans le cas des tests de ce programme. Il est à inclure dans la configuration de PostgreSQL.
Après rechargement de la configuration du serveur, compilation du programme, son exécution doit amener les traces suivantes pour une connexion échouée :
LOG: connection received: host=::1 port=42116
LOG: connection authorized: user=guillaume database=b1
FATAL: role "guillaume" does not exist
Et les traces suivantes pour une connexion réussie :
LOG: connection received: host=::1 port=40616
LOG: connection authorized: user=guillaume database=b1
LOG: disconnection: session time: 0:00:00.014 user=guillaume database=b1 host=::1 port=40616
Gérer l’erreur de connexion
Actuellement, le seul moyen de savoir si la connexion a réussi ou échoué est de regarder dans les fichiers de trace de PostgreSQL. C’est pour le moins peu pratique. Il est préférable de tester directement le retour de la fonction. Sauf problème mémoire grave, la structure PGCon est toujours instanciée, que la connexion réussisse ou pas. La fonction PQstatus() permet de connaître l’état de la connexion. Le tag j3e2 contient ce code de vérification.
Après exécution, nous obtenons ce message en cas d’échec :
$ ./client "host=localhost user=guillaume dbname=b2"
connection to server at "localhost" (::1), port 5415 failed: FATAL: database "b2" does not exist
Et dans la plus pure tradition Unix, en cas de réussite, nous n’avons aucun message.
Gérer les connexions nécessitant un mot de passe
Actuellement, mon fichier pg_hba.conf
est très permissif, vu qu’il accepte les
connexions sans mot de passe. Que se passerait-il si je changeais la méthode
d’authentification par scram-sha-256 ? (petit rappel, il est déconseillé
d’utiliser md5, qui est moins sécurisé que scram-sha-256).
Voici ce qui se passerait :
$ ./client "host=localhost user=guillaume dbname=b1"
connection to server at "localhost" (::1), port 5415 failed: fe_sendauth: no password supplied
Je n’ai pas eu la possibilité de saisir un mot de passe. Je pourrais évidemment
passer par la variable d’environnement PGPASSWORD ou par un fichier .pgpass
.
Je pourrais faire par exemple :
$ PGPASSWORD=supersecret ./client "host=localhost user=guillaume dbname=b1"
Et la connexion passerait. C’est évidemment à ne jamais faire, le mot de passe
étant visible par toute personne qui exécuterait la commande ps
ou la commande
top
au même moment. Le fichier .pgpass
est la seule vraie solution ici…
sauf à demander à l’utilisateur de saisir son mot de passe si la connexion ne
passe pas à cause d’un souci de mot de passe. Cependant, on ne peut pas demander
un mot de passe à chaque échec de connexion, vu qu’un échec de connexion n’est
pas forcément dû à un manque de mot de passe. La fonction
PQconnectionNeedsPassword()
nous permet de savoir cela, et le tag
j3e3 en fait bon
usage :
$ ./client "host=localhost user=guillaume dbname=b1"
Password:
Connection successfull!
Il est à noter que ce nouveau code contient deux entêtes supplémentaires :
#include "postgres_fe.h"
#include "common/string.h"
En effet, PostgreSQL propose des fonctions intéressantes pour les applications
clients codées en C, comme simple_prompt() utilisée
ici.
Ces fonctions ne rentrant pas strictement dans le cadre du driver libpq
, elles
nécessitent un ou plusieurs fichiers d’entête supplémentaires. Les bibliothèques
sont automatiquement prises en charge par PGXS.
Gestion des messages et des traces de l’application
Dans les autres fonctions que PostgreSQL propose, il existe aussi tout un
système de gestion des messages à destination de l’utilisateur. Ce système doit
être mis en place avec la fonction pg_logging_init()
. Les messages sont
envoyés avec les fonctions pg_log_debug
, pg_log_info
, pg_log_warning
et
pg_log_error
. S’il faut aussi quitter l’application, une autre fonction est
disponible : pg_fatal
.
Le tag j3e4 montre une utilisation de ce système. En voici le résultat :
$ ./client "host=localhost user=guillaume dbname=b1"
Password:
client: Connection successfull!
$ ./client "host=localhost user=guillaume dbname=b2"
Password:
client: error: could not connect: connection to server at "localhost" (::1), port 5415 failed: FATAL: database "b2" does not exist
Les niveaux sont colorisés si la variable d’environnement PG_COLORS
est
correctement configurée à la valeur always
.
Et si on exécutait enfin une requête ?
Là aussi, il existe un grand nombre de fonctions pour exécuter des requêtes, dépendant par exemple du type de protocole.
Nous allons commencer simple avec le protocole simple. La fonction s’appelle PQexec() et accepte deux arguments : la structure PGCon correspondant à la base où cette requête doit être exécutée, et la requête elle-même. La fonction renvoie une donnée de type PGresult qui permettra de récupérer les résultats.
Plusieurs fonctions permettent de récupérer des informations sur le résultat :
- la fonction PQresultStatus() indique si la requête a été correctement exécutée ;
- la fonction PQresultErrorMessage() renvoie le message d’erreur le cas échéant ;
- la fonction PQntuples() indique le nombre de lignes du résultat ;
- la fonction PQnfields() indique le nombre de colonnes du résultat.
Quant aux données du résultat, il faut passer par deux fonctions : PQgetisnull() pour savoir si une certaine donnée est NULL et PQgetvalue() pour récupérer la valeur d’une donnée. Cette valeur est toujours renvoyée avec le type de données char* qu’il conviendra de convertir dans le bon type de données C si besoin est.
Le tag j3e5
exécute la requête SELECT version()
et renvoie le résultat en cas de réussite
:
$ ./client "host=localhost user=guillaume dbname=b1"
PostgreSQL Release: PostgreSQL 16.0 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 13.2.1 20231011 (Red Hat 13.2.1-4), 64-bit
Réinitialisation de la connexion
Supposons maintenant que notre application, une fois connectée, passe son temps à exécuter la requête toutes les secondes. Comment se comporte l’application si la connexion est rompue ?
Le tag j3e6 permet de tester ce cas. En voici le résultat :
$ ./client "host=localhost user=guillaume dbname=b1"
client: Connection successfull! (backend PID is 677768)
PostgreSQL Release: PostgreSQL 16.0 on [...]
PostgreSQL Release: PostgreSQL 16.0 on [...]
PostgreSQL Release: PostgreSQL 16.0 on [...]
client: error: query failed: FATAL: terminating connection due to administrator command
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
client: error: query failed: no connection to the server
client: error: query failed: no connection to the server
[...]
J’ai simplement redémarré le serveur. Notre application ne testant pas si la connexion est toujours présente, il continue de tenter l’exécution de la requête. Dès le premier échec d’exécution, la libpq apprend que la connexion n’est plus présente et ne tente même plus d’exécuter la requête que l’application lui donne.
Il serait plus intelligent de la part de l’application de tester la raison de l’échec d’exécution et d’y réagir correctement. Par exemple, en cas de connexion rompue, il est possible de fermer la connexion, et de tenter une nouvelle connexion. Ou, encore plus intelligent, de réinitialiser la connexion. Cela se fait avec la fonction PQreset() comme le montre le tag j3e7, qui nous permettra ce beau résultat :
$ ./client "host=localhost user=guillaume dbname=b1"
Password:
client: Connection successfull! (backend PID is 678361)
PostgreSQL Release: PostgreSQL 16.0 on [...]
PostgreSQL Release: PostgreSQL 16.0 on [...]
PostgreSQL Release: PostgreSQL 16.0 on [...]
client: resetting connection
client: resetting connection
client: Reset successfull! (backend PID is 678403)
PostgreSQL Release: PostgreSQL 16.0 on [...]
PostgreSQL Release: PostgreSQL 16.0 on [...]
[...]
Nous voyons bien les deux tentatives de réinitialisation de la connexion (temps nécessaire pour redémarrer le serveur PostgreSQL), le changement de processus backend suite à cette nouvelle connexion (le PID est renvoyé par la fonction PQbackendPID(), et l’exécution de nouveau réussie une fois la réinitialisation acceptée.
Récupérons maintenant un résultat plus complexe
Par plus complexe, j’entends « composé de plusieurs lignes et de plusieurs colonnes ». La requête est fournie en deuxième argument du programme. L’affichage sera simple avec un tiret entre chaque cellule, un retour à la ligne pour chaque ligne. Une boucle imbriquée est utilisée pour récupérer chaque donnée et chaque ligne. Le résultat correspond au tag j3e8.
Voici le résultat d’une exécution :
$ ./client "host=localhost user=guillaume dbname=b1" "SELECT oid, datname FROM pg_database"
client: Connection successfull! (backend PID is 680093)
5 - postgres -
1 - template1 -
4 - template0 -
16452 - b1 -
32878 - b2 -
[...]
C’est bien mais ce n’est pas très joli. Il est possible de faire bien mieux en calculant la largeur de chaque colonne pour que les résultats soient joliment alignés. C’est ce que fait le tag j3e9, qui nous donne ce résultat :
$ ./client "host=localhost user=guillaume dbname=b1" "SELECT oid, datname FROM pg_database"
client: Connection successfull! (backend PID is 680906)
oid datname
--------------------
5 postgres
1 template1
4 template0
16452 b1
32878 b2
[...]
Mode single row, et requête asynchrone
La fonction PQexec a l’avantage d’être simple mais elle souffre d’un gros défaut. Elle récupère de façon synchrone l’intégralité des résultats. Si ce résultat est très volumineux, il n’est pas garanti que la mémoire disponible soit suffisante. Pour ces cas, il est possible de passer par le mode « single row » et l’exécution asynchrone.
L’exécution d’une requête en mode asynchrone se fait par l’appel à la fonction PQsendQuery(). Les résultats sont récupérés par la fonction PQgetResult(). Cette fonction renvoie les lignes disponibles depuis l’exécution de la fonction PQsendQuery ou depuis la dernière exécution de PGgetResult, suivant laquelle est la plus récente. Il faut donc l’exécuter plusieurs fois, tant que PQgetResult renvoie quelque chose.
Une requête exécutée en asynchrone peut utiliser ou non le mode « single row ». Ce mode n’est pas activé par défaut mais peut l’être en appelant la fonction PQsetSingleRowMode().
Un exemple complet se trouve au tag j3e10.
Conclusion
La journée s’est terminée avec la lecture du code de l’outil oid2name
disponible dans les modules contrib de PostgreSQL et avec l’écriture d’une
version personnalisée de l’outil dropdb
(disponible dans les tags de j3e11 à
j3e14).
Il existe évidemment plein d’autres fonctions dans la libpq :
- le nouveau mode pipeline ;
- le système d’annulation d’une requête en cours ;
- les notifications asynchrones ;
- le copie en batch ;
- et certainement plein d’autres que j’oublie.
Ils feront peut-être l’objet d’une prochaine journée de hacking sur PostgreSQL.