Reviers, le 22 août 2023

Dans l’article précédent, nous avons vu comment créer un module d’archivage. Lors de cette deuxième journée de codage sur PostgreSQL, nous avions été un peu plus loin en codant deux fonctions d’information que nous allons présenter dans ce nouvel article.

Fonction de récupération de statistiques

Parfois, l’accès au shell sur le serveur est impossible ou limité. Avoir une fonction interrogeable avec une requête SQL pour récupérer des informations sur l’archive ZIP est intéressant.

L’idée est donc de récupérer les informations suivantes :

  • nombre d’archives ;
  • noms de la première et dernière archives ;
  • dates de modification de la première et dernière archives.

Cette fonction ne renvoie donc pas une seule donnée, mais cinq, et pour cela, nous allons utiliser la clause OUT pour la définition SQL de la fonction :

CREATE OR REPLACE FUNCTION get_archive_stats(
  OUT entries_count integer,
  OUT first_wal_name text,
  OUT last_wal_name text,
  OUT first_wal_mtime timestamptz,
  OUT last_wal_mtime timestamptz)
AS '$libdir/zip_archive', 'get_archive_stats'
LANGUAGE C;

La définition C de la fonction ressemble en tout point à celles déjà créées :

PG_FUNCTION_INFO_V1(get_archive_stats);

Reste maintenant à savoir comment renvoyer plusieurs colonnes dans une fonction C. En fait, nous allons renvoyer une ligne composée de plusieurs colonnes. Il faut dans un premier temps définir la ligne. Il est ensuite possible de renseigner chaque colonne de cette ligne. Pour cela, nous utiliserons un tableau dont chaque élément correspond à une colonne. Nous utiliserons un deuxième tableau qui indiquera pour chaque colonne si la valeur est NULL. Ce deuxième tableau est donc un tableau de booléens. Ces deux tableaux contiendront exactement le même nombre d’éléments que de colonnes en sortie de la fonction. Il ne restera plus qu’à construire la ligne à partir de ces deux tableaux.

Maintenant, comment récupérer nos informations à partir de la bibliothèque libzip ? La première étape concerne l’ouverture de l’archive zip. Pour la récupération du nombre de fichiers archivés, nous allons utiliser la fonction zip_get_num_entries(). Ensuite, pour le premier et le dernier fichier, nous allons récupérer leur nom avec la fonction zip_get_name() et leur date de modification avec la fonction zip_stat_index. Il ne nous restera plus qu’à fermer l’archive.

Voici le code de la fonction :

Datum
get_archive_stats(PG_FUNCTION_ARGS)
{
  TupleDesc       tupdesc;
  Datum           values[5];
  bool            nulls[5];
  HeapTuple       tuple;
  Datum           result;
  char            destination[MAXPGPATH];
  zip_t          *ziparchive;
  int             error;
  int             entries_count;
  struct zip_stat zipstat;

  if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
    ereport(ERROR,
        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
         errmsg("function returning record called in context that cannot accept type record")));

  if (cluster_name != NULL && cluster_name[0] != '\0')
  {
    snprintf(destination, MAXPGPATH, "%s/%s.zip", archive_directory, cluster_name);
  }
  else
  {
    snprintf(destination, MAXPGPATH, "%s/%s.zip", archive_directory, "zip_archive");
  }

  ziparchive = zip_open(destination, ZIP_CREATE, &error);
  if (!ziparchive)
  {
    zip_error_t ziperror;
    zip_error_init_with_code(&ziperror, error);
    elog(ERROR, "cannot open zip archive '%s': %s\n", destination, zip_error_strerror(&ziperror));
    // ne va pas être exécuté
    zip_error_fini(&ziperror);
  }

  entries_count = zip_get_num_entries(ziparchive, 0);

  values[0] = Int32GetDatum(entries_count);
  nulls[0] = false;

  values[1] = CStringGetTextDatum(zip_get_name(ziparchive, 0, ZIP_FL_ENC_GUESS));
  nulls[1] = false;

  nulls[2] = entries_count < 0;
  if (!nulls[2])
  {
    values[2] = CStringGetTextDatum(zip_get_name(ziparchive, entries_count-1, ZIP_FL_ENC_GUESS));
  }

  zip_stat_index(ziparchive, 0, 0, &zipstat);
  nulls[3] = !(zipstat.valid && ZIP_STAT_MTIME);
  if (!nulls[3])
  {
    values[3] = TimestampTzGetDatum(time_t_to_timestamptz(zipstat.mtime));
  }

  zip_stat_index(ziparchive, entries_count-1, 0, &zipstat);
  nulls[4] = !(zipstat.valid && ZIP_STAT_MTIME);
  if (!nulls[4])
  {
    values[4] = TimestampTzGetDatum(time_t_to_timestamptz(zipstat.mtime));
  }

  tuple = heap_form_tuple(tupdesc, values, nulls);
  result = HeapTupleGetDatum(tuple);

  PG_RETURN_DATUM(result);
}

Encore une fois, il manque toute la gestion d’erreurs dans ce code.

Le code de cette partie est disponible sur le tag j2e8.

Une fois compilé, installé, et l’extension re-créée, voici le résultat suite à l’appel de cette fonction :

# SELECT * FROM get_archive_stats();
┌─[ RECORD 1 ]────┬──────────────────────────┐
│ entries_count   │ 2                        │
│ first_wal_name  │ 00000001000000080000002D │
│ last_wal_name   │ 00000001000000080000002E │
│ first_wal_mtime │ 2023-06-19 15:34:08+02   │
│ last_wal_mtime  │ 2023-06-19 15:34:54+02   │
└─────────────────┴──────────────────────────┘

Fonction de récupération de statistiques sur les fichiers de l’archive

Créons maintenant une autre fonction, dont le but est de donner des informations sur chaque fichier de l’archive. Les informations qu’on souhaite récupérer sont assez simples :

  • Nom du fichier ;
  • Taille (compressée et non) ;
  • Date de modification ;
  • CRC ;
  • méthode de compression ;
  • méthode de chiffrement.

Cela va beaucoup ressembler à la fonction précédente avec un détail pas si léger que ça : la fonction doit renvoyer plusieurs lignes.

La définition SQL de la fonction ajoute donc une clause supplémentaire, SETOF pour préciser que la fonction renvoie plusieurs lignes :

CREATE OR REPLACE FUNCTION get_archived_wals(OUT index int8,
  OUT name text,
  OUT uncompressed_size int8,
  OUT compressed_size int8,
  OUT modification_time timestamptz,
  OUT crc int4,
  OUT compression_method int2,
  OUT encrytion_method int2)
RETURNS SETOF record
AS '$libdir/zip_archive', 'get_archived_wals'
LANGUAGE C;

Par contre, la définition C de la fonction ne bouge pas :

PG_FUNCTION_INFO_V1(get_archived_wals);

Écrire une fonction C renvoyant plusieurs lignes revient à créer une fonction ré-entrante. Le premier appel va devoir initialiser une structure et traiter une ligne. Les appels suivants vont traiter chacun une ligne, jusqu’à arriver à la ligne finale. Pour nous aider, nous disposons de quelques fonctions importantes :

  • SRF_IS_FIRSTCALL() nous permet de savoir qu’il s’agit du premier appel à la fonction ;
  • SRF_FIRSTCALL_INIT() initialise une structure FuncCallContext qui va nous permettre d’y stocker des informations que nous retrouverons à chaque appel de la fonction ;
  • SRF_PERCALL_SETUP() doit être appelé à chaque appel de la fonction et permet de récupérer la structure initialisée lors du premier appel ;
  • SRF_RETURN_NEXT() renvoie la ligne traitée (qui aura été conçue comme dans la première fonction de cet article) ;
  • SRF_RETURN_DONE() indique que la fonction a terminé.

Le point essentiel est cette structure d’informations réutilisée. Elle permet de conserver des informations spécifiques à notre fonction, comme le pointeur de l’archive ZIP pour ne pas avoir besoin de l’ouvrir pour chaque ligne. Nous allons donc créer une structure personnalisée, que nous allons appeler ZipArchiveContext qui contiendra deux éléments : la description d’une ligne et un pointeur vers l’archive ZIP :

typedef struct
{
  TupleDesc  tupdesc;
  zip_t     *ziparchive;
} ZipArchiveContext;

Il nous reste à savoir comment récupérer les informations avec libzip. En dehors des opérations d’ouverture et de fermeture de l’archive ZIP, nous avons besoin de récupérer beaucoup d’informations sur chaque élément de l’archive. Pour cela, nous allons passer par la fonction zip_stat_index.

Plutôt que de fournir le code complet de la fonction (que vous pouvez retrouver dans le tag j2e9), nous allons y aller par étape. Tout d’abord, voici la structure globale de la fonction :

if (SRF_IS_FIRSTCALL())
{
  /* à faire uniquement au premier appel de la fonction */
  funcctx = SRF_FIRSTCALL_INIT();
  ... partie 1 ...
}

/* à faire pour chaque appel de la fonction */
funcctx = SRF_PERCALL_SETUP();
... partie 2 ...

/* à faire tant qu'il y a une ligne à traiter */
if (encore une ligne à traiter)
{
  ... partie 3 ...
  SRF_RETURN_NEXT(funcctx, result);
}

/* C'est terminé :) */
SRF_RETURN_DONE(funcctx);

Sur la partie 1, nous devons récupérer la structure FuncCallContext qui sera disponible à chaque appel de la fonction. Nous allons allouer notre structure en mémoire grâce à la fonction palloc(). Nous allons ensuite construire le descripteur de lignes, que nous stockerons dans notre structure. Nous pouvons enfin ouvrir l’archive ZIP (en stockant le pointeur vers l’archive dans notre stucture spécialisée) et y récupérer le nombre d’éléments dans l’archive ZIP. Ceci est très important car cela permettra d’indiquer à PostgreSQL le nombre d’appels à réaliser à la fonction (un appel par ligne à renvoyer, donc un appel par élément dans l’archive ZIP). S’il n’y a aucun élément, la fonction se termine ici, pas d’appel à faire. S’il y a des éléments, nous enregistrons le nombre d’appels maximum à la fonction (variable max_calls) et nous enregistrons le pointeur vers notre structure spécialisée dans la structure FuncCallContext

Petite note sur palloc(). Il s’agit d’une version améliorée de malloc() pour PostgreSQL. Elle s’utilise dans le cadre des contextes mémoires, autre système mise à disposition par PostgreSQL. Il serait un peu long d’expliquer les contextes mémoires dans cet article, mais vu l’intérêt que cela a suscité pendant la journée de codage, il est possible que nous y consacrions une journée (et donc un article).

Ce qui nous donne ce code pour la partie 1 :

TupleDesc tupdesc;

/* Créer un contexte mémoire pour la persistence des informations entre appels */
funcctx = SRF_FIRSTCALL_INIT();

/* Basculer vers ce contexte mémoire */
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

/* Créer notre structure personnalisée */
fctx = (ZipArchiveContext *) palloc(sizeof(ZipArchiveContext));

/* Construire le descripteur de ligne */
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
  ereport(ERROR,
      (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
       errmsg("function returning record called in context that cannot accept type record")));
fctx->tupdesc = BlessTupleDesc(tupdesc);

/* Ouvrir l'archive ZIP */
if (cluster_name != NULL && cluster_name[0] != '\0')
  snprintf(destination, MAXPGPATH, "%s/%s.zip", archive_directory, cluster_name);
else
  snprintf(destination, MAXPGPATH, "%s/%s.zip", archive_directory, "zip_archive");
/* Le pointeur de l'archive ZIP est stocké dans notre structure personnalisée */
fctx->ziparchive = zip_open(destination, ZIP_CREATE, &error);

/* Récupérer le nombre d'éléments présents dans l'archive ZIP */
max_calls = zip_get_num_entries(fctx->ziparchive, 0);

if (max_calls > 0)
{
  /* S'il y a des éléments,
     indiquer à PostgreSQL le nombre maximum d'appels de la fonction */
  funcctx->max_calls = max_calls;
  /* Et lui donner notre structure pour la récupérer au prochain appel */
  funcctx->user_fctx = fctx;
}
else
{
  /* Sans éléments, la fonction peut terminer son exécution */
  MemoryContextSwitchTo(oldcontext);
  SRF_RETURN_DONE(funcctx);
}

/* Basculer vers l'ancien contexte */
MemoryContextSwitchTo(oldcontext);

La partie 2 est assez simple. Nous récupérons le numéro d’appel à la fonction (via funcctx->call_cntr), le nombre maximum d’appels à la fonction (via funcctx->max_calls, pour savoir si on est arrivé à la fin) et notre structure spécialisée (via funcctx->user_fctx). Cela nous donne ce petit code :

funcctx = SRF_PERCALL_SETUP();
call_cntr = funcctx->call_cntr;
max_calls = funcctx->max_calls;
fctx = funcctx->user_fctx;

La partie 3 traite une ligne. Pour cela, nous allons utiliser la fonction zip_stat_index() pour récupérer les statistiques dans une structure. Puis, pour chaque information souhaitée, nous allons tester si l’information est valide et enregistrer les informations dans les tableaux de valeurs (values[]) et de NULL (nulls[]).

if (call_cntr < max_calls)
{
  Datum   values[8];
  bool    nulls[8];
  HeapTuple tuple;
  Datum   result;

  /* Obtenir les stats de l'archive ZIP pour l'élément d'index call_cntr */
  zip_stat_index(fctx->ziparchive, call_cntr, 0, &zipstat);

  /* Récupérer l'index */
  nulls[0] = !(zipstat.valid && ZIP_STAT_INDEX);
  if (!nulls[0])
    values[0] = Int64GetDatum(zipstat.index);

  /* Récupérer le nom */
  nulls[1] = !(zipstat.valid && ZIP_STAT_NAME);
  if (!nulls[1])
    values[1] = CStringGetTextDatum(zipstat.name);

  /* Récupérer la taille */
  nulls[2] = !(zipstat.valid && ZIP_STAT_SIZE);
  if (!nulls[2])
    values[2] = Int64GetDatum(zipstat.size);

  /* Récupérer la taille compressée */
  nulls[3] = !(zipstat.valid && ZIP_STAT_COMP_SIZE);
  if (!nulls[3])
    values[3] = Int64GetDatum(zipstat.comp_size);

  /* Récupérer la date de modification */
  nulls[4] = !(zipstat.valid && ZIP_STAT_MTIME);
  if (!nulls[4])
    values[4] = TimestampTzGetDatum(time_t_to_timestamptz(zipstat.mtime));

  /* Récupérer la somme de contrôle */
  nulls[5] = !(zipstat.valid && ZIP_STAT_CRC);
  if (!nulls[5])
    values[5] = Int32GetDatum(zipstat.crc);

  /* Récupérer la méthode de compression */
  nulls[6] = !(zipstat.valid && ZIP_STAT_COMP_METHOD);
  if (!nulls[6])
    values[6] = Int16GetDatum(zipstat.comp_method);

  /* Récupérer la méthode de chiffrement */
  nulls[7] = !(zipstat.valid && ZIP_STAT_ENCRYPTION_METHOD);
  if (!nulls[7])
    values[7] = Int16GetDatum(zipstat.encryption_method);

  /* Construire la ligne */
  tuple = heap_form_tuple(fctx->tupdesc, values, nulls);
  result = HeapTupleGetDatum(tuple);

  /* Quitter la fonction en renvoyant la ligne */
  SRF_RETURN_NEXT(funcctx, result);
}

Le code de cette partie est disponible sur le tag j2e9.

Là aussi, il faut compiler, installer, supprimer puis créer l’extension pour pouvoir tester la nouvelle fonction :

# SELECT * FROM get_archived_wals();
┌───────┬──────────────────────────┬───────────────────┬─────────────────┬────────────────────────┬─────────────┬────────────────────┬──────────────────┐
│ index │           name           │ uncompressed_size │ compressed_size │   modification_time    │     crc     │ compression_method │ encrytion_method │
├───────┼──────────────────────────┼───────────────────┼─────────────────┼────────────────────────┼─────────────┼────────────────────┼──────────────────┤
│     0 │ 00000001000000080000002D │          16777216 │           16467 │ 2023-06-19 15:34:08+02 │ -1002810270 │                  8 │                0 │
│     1 │ 00000001000000080000002E │          16777216 │           73520 │ 2023-06-19 15:34:54+02 │  1547868743 │                  8 │                0 │
│     2 │ 00000001000000080000002F │          16777216 │          111808 │ 2023-06-20 08:01:10+02 │ -2013698773 │                  8 │                0 │
│     3 │ 000000010000000800000030 │          16777216 │             667 │ 2023-06-20 08:04:30+02 │ -2075256837 │                 93 │                0 │
│     4 │ 000000010000000800000031 │          16777216 │           73405 │ 2023-06-20 08:50:12+02 │   268518886 │                  8 │                0 │
│     5 │ 000000010000000800000032 │          16777216 │           73343 │ 2023-06-20 08:51:50+02 │  -259808507 │                  8 │                0 │
└───────┴──────────────────────────┴───────────────────┴─────────────────┴────────────────────────┴─────────────┴────────────────────┴──────────────────┘

Conclusion

Ces deux fonctions complémentent bien notre module d’archivage.

Le code manque toujours d’une gestion plus poussée des erreurs et de nombreux commentaires. J’ai décidé de ne pas les ajouter avant pour que l’article soit plus compréhensible. Néanmoins, la gestion d’erreurs et des commentaires supplémentaires sont disponibles sur la version finale.


DALIBO

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