Développons une nouvelle fonctionnalité pour PostgreSQL (7/13)

J'ai un peu avancé aujourd'hui sur ce projet, alors allons-y.

Encore une fois, n'oubliez pas de récupérer la mise à jour des sources et de compiler le tout. J'ai oublié dès le début que, vu le rythme effréné des commit dans le tronc des sources de PostgreSQL, il peut être nécessaire par moment de tout recompiler : ça peut être dû à un initdb qui foire avec un drôle de message d'erreur, ou le serveur lui-même qui renvoie des messages d'erreurs en lisant des tables systèmes. Bref, en cas de doute, « make clean » pour tout nettoyer, puis de nouveau « make && make install ». Normalement, après ça, tout doit être redevenu normal... sauf si on a ajouté des bugs :)

Donc la dernière fois, la fonction movedb était capable d'indiquer toutes les tables de la base où nous étions connecté. Maintenant, on va faire appel à la fonction de déplacement de table. Celle-ci a déjà été codée pour l'instruction ALTER TABLE SET TABLESPACE. On en a même parlé à l'épisode 3 : il s'agit de la fonction ATExecSetTableSpace qui prend en arguments l'OID de la table et celui du tablespace de destination. Du coup, on a plusieurs problèmes :

  1. récupérer l'OID d'une table en ayant son nom
  2. récupérer l'OID d'un tablespace en ayant son nom
  3. quoique la fonction movedb ne récupère pas encore le nom du tablespace en argument, donc il faut aussi récupérer le nom du tablespace
  4. exécuter la fonction ATExecSetTableSpace.

Allez, courage, on va se le faire point par point.

Comment récupérer l'OID d'une table à partir de son nom

Nous récupérons actuellement le nom de chaque table dans cette boucle :

 while ( ( tuple = heap_getnext(scan, ForwardScanDirection ) ) != NULL)
 {
     elog(NOTICE, "relation %s, oid %d", NameStr( ( (Form_pg_class) GETSTRUCT(tuple) )->relname), (int)relid);
 }

La fonction GETSTRUCT renvoie une structure dont le format dépend de l'objet renvoyé par la fonction heap_getnext. Ça peut être une table, un index, une séquence, une vue, etc. La structure permet d'accéder aux champs de la table. C'est ainsi que nous récupérons le nom de la table (la colonne relname est « mappée » sur l'élément relname de la structure renvoyée). On peut donc imaginer qu'il est possible de récupérer aussi l'OID de la ligne récupérée. Cependant, si on essaie le champ oid, cela ne fonctionne pas. Il faut passer par la fonction HeapTupleGetOid() qui prend en argument la ligne récupérée. En modifiant un peu la boucle ci-dessus, on obtient ce code :

 while ( ( tuple = heap_getnext(scan, ForwardScanDirection ) ) != NULL)
 {
     Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
     Oid relid;

     relid = HeapTupleGetOid(tuple);

     elog(NOTICE, "relation %s, oid %d", NameStr(classForm->relname), (int)relid);
 }

Le résultat après compilation est impeccable :

 guillaume=# alter database guillaume tablespace mon_espace_de_stockage ;
 NOTICE:  alter tablespace guillaume set tablespace mon_espace_de_stockage!
 NOTICE:  relation t1, oid 16387
 ALTER DATABASE

(l'OID peut différer chez vous).

Récupérer le nom du tablespace

Pour cela, il faut déjà changer la façon dont la fonction est appelée en lui ajoutant un deuxième argument. De cette façon :

 movedb(const char *dbname, const char *tblspcname)

Maintenant, il faut renseigner cette information dans l'appel de cette fonction. Le code ressemble à ceci actuellement :

 else if (strcmp(defel->defname, "tablespace") == 0)
 {
     elog(NOTICE,
         "alter tablespace %s set tablespace %s!", stmt->dbname, strVal(defel->arg));
     movedb(stmt->dbname);
 }

Là encore, il suffit d'ajouter le nom du tablespace dans l'appel de la fonction. On obtient ceci :

 else if (strcmp(defel->defname, "tablespace") == 0)
 {
     elog(NOTICE,
         "alter tablespace %s set tablespace %s!", stmt->dbname, strVal(defel->arg));
     movedb(stmt->dbname, strVal(defel->arg));
 }

Simple, non ?

Récupérer l'OID du tablespace

Au départ, j'ai commencé à chercher comment lire la table pg_tablespace et dans ma recherche, je suis tombé sur une fonction dont le nom semblait prometteur : get_tablespace_oid(). Elle l'est. Elle attend en argument le nom du tablespace et renvoie son OID s'il existe. Elle renvoie par contre le code InvalidOid s'il n'existe pas. Super, ça va nous permettre de tester que le tablespace existe bien avant d'essayer d'y envoyer les tables. Voici le code à ajouter :

 tblspcoid = get_tablespace_oid(tblspcname);
 if (tblspcoid == InvalidOid)
 {
     ereport(ERROR,
         (errcode(ERRCODE_UNDEFINED_DATABASE),
         errmsg("tablespace \"%s\" does not exist", tblspcname)));
 }

(J'ai conservé le code d'erreur de table car celui des tablespace n'a pas l'air d'exister... cela fera partie des nettoyages à faire avant envoi du patch résultant).

On a l'OID de la table dans la variable relid et l'OID du tablespace dans tblspcoid. Il ne reste plus qu'à appeler la fonction de déplacement.

Appeler la fonction ATExecSetTableSpace

Attention, cette fonction n'est pas si simple à appeler. En effet, les developpeurs de PostgreSQL n'ont pas cru que quelqu'un en aurait besoin en dehors de son fichier source. Donc elle est inaccessible de notre fichier source (pour rappel, backend/commands/dbcommands.c). Il faut d'abord modifier la déclaration de cette fonction en supprimant le mot clé static et en déclarant la fonction dans le fichier include/commands/tablecmds.h. Ceci fait, il ne reste plus qu'à ajouter l'appel dans la boucle de la première partie de ce billet :

 while ( ( tuple = heap_getnext(scan, ForwardScanDirection ) ) != NULL)
 {
     Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
     Oid relid;

     relid = HeapTupleGetOid(tuple);

     elog(NOTICE, "relation %s, oid %d", NameStr(classForm->relname), (int)relid);
     ATExecSetTableSpace(relid, tblspcoid);
 }

Après compilation et exécution, voici le résultat :

 guillaume=# alter database guillaume tablespace mon_espace_de_stockage ;
 NOTICE:  alter tablespace guillaume set tablespace mon_espace_de_stockage!
 ERROR:  cannot move system relation "pg_type"
 STATEMENT:  alter database guillaume tablespace mon_espace_de_stockage ;
 NOTICE:  relation pg_type, oid 1247
 ERROR:  cannot move system relation "pg_type"

Grmbl... On ne peut pas déplacer une table système. En lisant le code de la fonction ATExecSetTableSpace, c'est indiqué très clairement :

 /*
  * We can never allow moving of shared or nailed-in-cache relations,
  * because we can't support changing their reltablespace values.
  */

Intéressant, mais très ennuyant. Cela voudrait dire que nous ne pouvons pas déplacer une base complète : tous les objets systèmes resteront sur le tablespace de création. Bon, c'est pas très grave, c'est normalement pas les gros objets mais c'est quand même gênant. Essayons de revoir notre code pour qu'il ne prenne en compte que les tables utilisateurs. Pour cela, on va faire un test très bête : on ne s'occupe que des tables dont l'OID est supérieur à 16380 :

 while ( ( tuple = heap_getnext(scan, ForwardScanDirection ) ) != NULL)
 {
     Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
     Oid relid;

     relid = HeapTupleGetOid(tuple);

     if ((int)relid > 16380)
     {
         elog(NOTICE, "relation %s, oid %d", NameStr(classForm->relname), (int)relid);
         ATExecSetTableSpace(relid, tblspcoid);
     }
 }

Et cette fois, cela semble fonctionner :

 guillaume=# select oid, spcname, spclocation from pg_tablespace where spcname='mon_espace_de_stockage';
   oid  |        spcname         |            spclocation
 - - - -+- - - - - - - - - - - - +- - - - - - - - - - - - - - - - - -
  16386 | mon_espace_de_stockage | /home/guillaume/postgresql_tblspc
 (1 row)
 
 guillaume=# select relname, relfilenode, reltablespace from pg_class where relname='t1';
  relname | relfilenode | reltablespace
 - - - - -+- - - - - - -+- - - - - - - -
  t1      |       16394 |             0
 (1 row)
 
 guillaume=# \! ls -l /home/guillaume/postgresql_devel/data/base/16384/16394
 -rw--- 1 guillaume guillaume 8192 2008-10-15 00:39 /home/guillaume/postgresql_devel/data/base/16384/16394
 guillaume=# \! ls -l /home/guillaume/postgresql_tblspc/16384
 total 0
 guillaume=# alter database guillaume tablespace mon_espace_de_stockage ;
 NOTICE:  alter tablespace guillaume set tablespace mon_espace_de_stockage!
 NOTICE:  relation t1, oid 16387
 NOTICE:  relation t1, oid 16387
 ALTER DATABASE

(Aucune idée actuellement du pourquoi j'ai deux fois le message)

 guillaume=# select relname, relfilenode, reltablespace from pg_class where relname='t1';
  relname | relfilenode | reltablespace
 - - - - -+- - - - - - -+- - - - - - - -
  t1      |       16395 |         16386
 (1 row)
 
 guillaume=# \! ls -l /home/guillaume/postgresql_devel/data/base/16384/16394
 -rw- - - - 1 guillaume guillaume 0 2008-10-15 01:05 /home/guillaume/postgresql_devel/data/base/16384/16394
 guillaume=# \! ls -l /home/guillaume/postgresql_tblspc/16384
 total 8
 -rw- - - - 1 guillaume guillaume 8192 2008-10-15 01:05 16395
 -rw- - - - 1 guillaume guillaume    0 2008-10-15 01:05 16395_fsm

Le fichier correspondant à notre table existe toujours mais est de taille zéro, prêt à être supprimé au prochain CHECKPOINT. Le nouveau fichier (et remarquer le nouveau relfilenode) existe directement dans le tablespace. Il a bien été déplacé. Je vous laisse le soin de vérifier qu'on peut écrire/supprimer/modifier des lignes dans cette table. Je vous laisse aussi le soin de vérifier qu'on peut renvoyer la table dans son ancien tablespace ou encore dans un autre. Je vous laisse enfin le soin de vérifier que la commande déplace toutes les tables, et non pas une seule.

Il nous manque quoi maintenant ? deux objets ne sont pas déplacés : les index et les tables TOAST. On peut faire ça un peu goret et rapidement. Je vais dormir un peu et on verra si on peut faire ça plus proprement.

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet