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

Mise à jour : plusieurs corrections de Thomas, Merci !

Désolé pour l'interruption des programmes, j'ai été plutôt occupé ces derniers temps.

Avant tout, n'oubliez pas de récupérer la mise à jour des sources et à compiler le tout.

On en était à vouloir réellement implémenter le déplacement des fichiers. Malheureusement, c'est peut-être encore un peu trop tôt pour cela. Avant de déplacer les fichiers, il faut d'abord savoir quels fichiers déplacer. Et avant cela, il faut aussi savoir si on a le droit de le faire.

Commençons par les droits. Il existe une autre fonction qui touche à tous les fichiers : celle de suppression d'une base de données. La fonction se trouve là-aussi dans le fichier backend/commands/dbcommands.c, elle s'appelle dropdb. Le mieux est de la dupliquer et de renommer la nouvelle en movedb. L'argument de cette nouvelle fonction est le nom de la base. Il faut aussi supprimer certains des tests inutiles dans notre cas : celui sur les bases de données modèles, celui qui empêche la suppression de la base de données où l'utilisateur est connecté. D'autres appels de fonctions sont aussi à supprimer car ils n'ont rien à voir avec notre fonction. Voici le résultat de l'élagage :

 /*
  * ALTER DATABASE SET TABLESPACE
  */
 void
 movedb(const char *dbname)
 {
 	Oid			db_id;
 	bool		db_istemplate;
 	Relation	pgdbrel;
 	int			notherbackends;
 	int			npreparedxacts;
 
 	/*
 	 * Look up the target database's OID, and get exclusive lock on it. We
 	 * need this to ensure that no new backend starts up in the target
 	 * database while we are moving it, and that no one is
 	 * using it as a CREATE DATABASE template or trying to delete it for
 	 * themselves.
 	 */
 	pgdbrel = heap_open(DatabaseRelationId, RowExclusiveLock);
 
 	if (!get_db_info(dbname, AccessExclusiveLock, &db_id, NULL, NULL,
 					 &db_istemplate, NULL, NULL, NULL, NULL, NULL, NULL))
 	{
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_DATABASE),
 				 errmsg("database \"%s\" does not exist", dbname)));
 	}
 
 	/*
 	 * Permission checks
 	 */
 	if (!pg_database_ownercheck(db_id, GetUserId()))
 		aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_DATABASE,
 					   dbname);
 
       /* Obviously can't move the tables of another database than my own */
       if (db_id != MyDatabaseId)
           ereport(ERROR,
                  (errcode(ERRCODE_OBJECT_IN_USE),
                    errmsg("cannot move the relations of another database than the currently open one")));  
 	/*
 	 * Check for other backends in the target database.  (Because we hold the
 	 * database lock, no new ones can start after this.)
 	 *
 	 * As in CREATE DATABASE, check this after other error conditions.
 	 */
 	CountOtherDBBackends(db_id, &notherbackends, &npreparedxacts);
 	if (notherbackends > 1 || npreparedxacts > 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_IN_USE),
 				 errmsg("database \"%s\" is being accessed by other users",
 						dbname),
 				 errdetail_busy_db(notherbackends, npreparedxacts)));
 
 	/*
 	 * Force a checkpoint to make sure the bgwriter has received the message
 	 * sent by ForgetDatabaseFsyncRequests. On Windows, this also ensures that
 	 * the bgwriter doesn't hold any open files, which would cause rmdir() to
 	 * fail.
 	 */
 	RequestCheckpoint(CHECKPOINT_IMMEDIATE | CHECKPOINT_FORCE | CHECKPOINT_WAIT);
 
 	/*
 	 * Close pg_database, but keep lock till commit (this is important to
 	 * prevent any risk of deadlock failure while updating flat file)
 	 */
 	heap_close(pgdbrel, NoLock);
 
 	/*
 	 * Set flag to update flat database file at commit.  Note: this also
 	 * forces synchronous commit, which minimizes the window between removal
 	 * of the database files and commital of the transaction. If we crash
 	 * before committing, we'll have a DB that's gone on disk but still there
 	 * according to pg_database, which is not good.
 	 */
 	database_file_update_needed();
 }

Nous devons déclarer cette fonction dans l'en-tête. Ce fichier se trouve dans le répertoire src/include/commands et se nomme dbcommands.h. Il faut ajouter cette ligne de déclaration :

 extern void movedb(const char *dbname);

Maintenant, il faut appeler cette fonction. Pour cela, retrouvons le message qu'on envoyait dans le log (à savoir, « set tablespace ! »). Il suffit d'ajouter l'appel à la fonction. J'en profite aussi pour changer le message et obtenir quelque chose de plus intéressant, ce qui me donne le résultat suivant :

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

Après toute ces modifications, compilons pour tester le résultat. Et enfin testons. Je connecte un psql sur db1. Puis je lance un autre psql sur la base postgres. J'exécute le ALTER DATABASE à partir de ce deuxième psql et j'obtiens :

 postgres=# alter database db1 tablespace mon_espace_de_stockage;
 NOTICE:  alter tablespace db1 set tablespace mon_espace_de_stockage!
 ERROR:  database "db1" is being accessed by other users
 DETAIL:  There are 1 other session(s) using the database.

La fonction a bien détecté l'autre client connecté et a empêché la suite des opérations. Déconnectons cet utilisateur et tentons de nouveau l'opération :

 postgres=# alter database db1 tablespace mon_espace_de_stockage;FATAL:  role "postgres" does not exist
 NOTICE:  alter tablespace db1 set tablespace mon_espace_de_stockage!
 ALTER DATABASE

Ça passe sans problème. Je vous laisse tester le reste. Je préfère passer à la suite, à savoir récupérer la liste des tables. Un petit soucis pointe son nez. Pour récupérer la liste des tables, je vais exécuter une requête sur pg_class. Or cette dernière ne va me donner des informations que sur la base où je suis connecté. Il faut donc modifier les tests pour s'assurer qu'il n'y a qu'un seul client connecté et que ce client est moi.

Ceci fait, la récupération des tables de la base devient notre priorité. Pour cela, nous allons utiliser la fonction heap_beginscan qui permet de réaliser un parcours d'une table. Elle demande plusieurs arguments dont la table à parcourir et un filtre optionnel. La table doit d'abord est ouverte, ce qui se fait avec la fonction heap_open, qui prend deux arguments, à savoir le nom de la table et le type de verrou). Quant au filtre, nous voulons récupéré toutes les lignes représentant des relations (constante Anum_pg_class_relkind). Ceci fait, nous avons donc ouvert la table, initié un parcours de table. Reste à récupérer les valeurs. La fonction heap_getnext va nous aider pour cela. Il nous suffit de l'emballer dans une boucle et le tour est joué. Tout ceci nous donne le code suivant :

 /* Process all plain relations listed in pg_class */
 ScanKeyInit(&key,
   Anum_pg_class_relkind,
   BTEqualStrategyNumber, F_CHAREQ,
   CharGetDatum(RELKIND_RELATION));
 
 pgclass = heap_open(RelationRelationId, AccessShareLock);
 
 scan = heap_beginscan(pgclass, SnapshotNow, 1, &key);
 
 while ( ( tuple = heap_getnext(scan, ForwardScanDirection ) ) != NULL)
 {
   elog(NOTICE, "relation %s", NameStr->relname));
 }
 
 heap_endscan(scan);
 heap_close(pgclass, AccessShareLock);

Reprenons notre cycle habituel : compilation, installation, tests...

 db1=# alter database db1 tablespace mon_espace_de_stockage;
 NOTICE:  alter tablespace db1 set tablespace mon_espace_de_stockage!
 NOTICE:  relation pg_type                                           
 NOTICE:  relation sql_implementation_info                           
 NOTICE:  relation sql_languages                                     
 [... coupé parce qu'inintéressant ...]
 NOTICE:  relation pg_namespace
 NOTICE:  relation pg_conversion
 NOTICE:  relation pg_depend
 NOTICE:  relation t1
 ALTER DATABASE

Et voilà, c'est parfait. Il ne reste plus qu'à donner l'ordre de déplacer la table.

Ajouter un commentaire

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

Fil des commentaires de ce billet