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.

Une des slides de cette journée
Une des slides de cette journée

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.


DALIBO

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