un peu de recherche
Nous venons de voir qu'une table est un fichier dont le nom dépend du relfilenode de la table et qui se trouve dans le sous-répertoire correspondant à l'OID de la base de données, lui même appartenant au répertoire du tablespace.
Déplacer une table d'un tablespace à un autre doit simplement revenir à faire un mv du fichier vers le nouveau tablespace. Vérifions cela.
Avant toute action sur les sources, commençons par les mettre à jour :
guillaume@laptop$ cd postgresql_src/pgsql/
guillaume@laptop$ cvs -q update -d
[... coupé parce que trop long ...]
P src/timezone/data/leapseconds
P src/timezone/data/northamerica
P src/timezone/data/southamerica
P src/timezone/data/zone.tab
P src/tools/msvc/Install.pm
OK. Plaçons-nous maintenant dans le répertoire src où se concentrent tous les fichiers sources de PostgreSQL (pour ceux qui connaissent le répertoire contrib, il contient bien des sources, mais ces sources ne font pas partis du moteur).
guillaume@laptop$ cd src
Recherchons toutes les occurences de « set tablespace », mais filtrons les fichiers intermédiaires de compilation (suffixe .o), les fichiers de traduction (suffixe .po) et les fichiers binaires.
guillaume@laptop$ grep -ri "set tablespace" * | grep -v "\.po" | grep -v "\.o" | grep -v "Fichier binaire"
backend/parser/gram.y: /* ALTER TABLE <name> SET TABLESPACE <tablespacename> */
backend/parser/gram.y: | SET TABLESPACE name
backend/commands/tablecmds.c: case AT_SetTableSpace: /* SET TABLESPACE */
backend/commands/tablecmds.c: case AT_SetTableSpace: /* SET TABLESPACE */
backend/commands/tablecmds.c: * If we had SET TABLESPACE but no reason to reconstruct tuples,
backend/commands/tablecmds.c: * ALTER TABLE SET TABLESPACE
backend/commands/tablecmds.c: errmsg("cannot have multiple SET TABLESPACE subcommands")));
backend/commands/tablecmds.c: * Execute ALTER TABLE SET TABLESPACE for cases where there is no tuple
bin/psql/sql_help.h: N_("ALTER INDEX name RENAME TO new_name\nALTER INDEX name SET TABLESPACE tablespace_name\nALTER INDEX name SET ( storage_parameter = value , ... )\nALTER INDEX name RESET ( storage_parameter , ... )") },
bin/psql/sql_help.h: N_("ALTER TABLE ONLY name *\n action , ...\nALTER TABLE ONLY name *\n RENAME COLUMN column TO new_column\nALTER TABLE name\n RENAME TO new_name\nALTER TABLE name\n SET SCHEMA new_schema\n\nwhere action is one of:\n\n ADD COLUMN column type column_constraint [ ... ]\n DROP COLUMN column RESTRICT \n ALTER COLUMN column TYPE type USING expression\n ALTER COLUMN column SET DEFAULT expression\n ALTER COLUMN column DROP DEFAULT\n ALTER COLUMN column { SET | DROP } NOT NULL\n ALTER COLUMN column SET STATISTICS integer\n ALTER COLUMN column SET STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN }\n ADD table_constraint\n DROP CONSTRAINT constraint_name RESTRICT \n DISABLE TRIGGER trigger_name \n ENABLE TRIGGER trigger_name \n ENABLE REPLICA TRIGGER trigger_name\n ENABLE ALWAYS TRIGGER trigger_name\n DISABLE RULE rewrite_rule_name\n ENABLE RULE rewrite_rule_name\n ENABLE REPLICA RULE rewrite_rule_name\n ENABLE ALWAYS RULE rewrite_rule_name\n CLUSTER ON index_name\n SET WITHOUT CLUSTER\n SET WITHOUT OIDS\n SET ( storage_parameter = value , ... )\n RESET ( storage_parameter , ... )\n INHERIT parent_table\n NO INHERIT parent_table\n OWNER TO new_owner\n SET TABLESPACE new_tablespace") },
bin/psql/tab-complete.c: {"SET TABLESPACE", "OWNER TO", "RENAME TO", "SET", "RESET", NULL};
bin/psql/tab-complete.c: /* If we have TABLE <sth> SET TABLESPACE provide a list of tablespaces */
include/nodes/parsenodes.h: AT_SetTableSpace, /* SET TABLESPACE */
interfaces/ecpg/preproc/preproc.y: /* ALTER <name> SET TABLESPACE <tablespacename> */
interfaces/ecpg/preproc/preproc.y: | SET TABLESPACE name
interfaces/ecpg/preproc/preproc.y: { $$ = cat2_str(make_str("set tablespace"), $3); }
interfaces/ecpg/preproc/preproc.c: { (yyval.str) = cat2_str(make_str("set tablespace"), (yyvsp(3) - (3).str)); ;}
test/regress/input/tablespace.source:ALTER TABLE testschema.atable SET TABLESPACE testspace;
test/regress/input/tablespace.source:ALTER INDEX testschema.anindex SET TABLESPACE testspace;
test/regress/output/tablespace.source:ALTER TABLE testschema.atable SET TABLESPACE testspace;
test/regress/output/tablespace.source:ALTER INDEX testschema.anindex SET TABLESPACE testspace;
Les deux premières lignes concernent un fichier nommé gram.y. Le suffixe .y indique qu'il s'agit d'un fichier bison. Bison est un outil permettant de créer facilement un langage (pour les anciens Unixiens, Bison est la version GNU de yacc). C'est d'ailleurs ce fichier qu'il nous faudra modifier pour ajouter notre variante à l'instruction « ALTER TABLE ». Le fichier tablecmds.c a l'air de s'occuper de l'exécution concrète des commandes sur les tables. Son emplacement est d'ailleurs significatif. Le répertoire backends contient tous les sources du serveur, le sous-répertoire commands tous les sources concernant l'exécution des commandes SQL. Le fichier sql_help.h semble contenir l'aide proposée par psql via la commande \\h. Son emplacement est significatif là-aussi. Le répertoire bin contient tous les sources des outils binaires de PostgreSQL (psql, initdb, pg_dump, etc.). Le nom du fichier bin/psql/tab-complete.c nous donne une indication sur son utilité : il s'agit des sources pour la gestion de la complétion automatique dans psql. Quant au reste des fichiers, ils nous intéressent peu. Cela touche à ECPG et aux tests de régression. Beurk 
Bref, le fichier qui nous intéresse le plus actuellement, c'est tablecmds.h. Ouvrons-le et recherchons la première occurence de « SET TABLESPACE ».
La première occurence correspond au début d'une instruction case :
case AT_SetTableSpace: /* SET TABLESPACE */
ATSimplePermissionsRelationOrIndex(rel);
/* This command never recurses */
ATPrepSetTableSpace(tab, rel, cmd->name);
pass = AT_PASS_MISC; /* doesn't actually matter */
break;
Deux fonctions sont appelées. D'après leur nom, on imagine facilement que la première va vérifier les droits de la table ou de l'index, et que la second prépare l'exécution du « SET TABLESPACE ». Si on regarde dans quelle fonction se trouve ces instructions (simplement en remontant un peu), on trouve l'en-tête suivant :
/*
* ATPrepCmd
*
* Traffic cop for ALTER TABLE Phase 1 operations, including simple
* recursion and permission checks.
*
* Caller must have acquired AccessExclusiveLock on relation already.
* This lock should be held until commit.
*/
Cette fonction traite la phase 1 de l'instruction « ALTER TABLE », qui laisse penser qu'il y a d'autres phases et que cette phase 1 est la phase préparatoire.
Bon, deuxième occurence :
case AT_SetTableSpace: /* SET TABLESPACE */
/*
* Nothing to do here; Phase 3 does the work
*/
break;
Ce code ne fait rien. On apprend juste que le travail se fait en trois phases et que seule la phase 3 fait le boulot.
Occurence suivante :
/*
* If we had SET TABLESPACE but no reason to reconstruct tuples,
* just do a block-by-block copy.
*/
if (tab->newTableSpace)
ATExecSetTableSpace(tab->relid, tab->newTableSpace);
Là-aussi, peu de choses. On apprend seulement que le « SET TABLESPACE » semble fonctionner dans un mode copie bloc par bloc. Ce qui serait assez logique, tout PostgreSQL fonctionne par bloc.
Occurence suivante :
/*
* ALTER TABLE SET TABLESPACE
*/
static void
ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, char *tablespacename)
Tiens, c'est la fonction appelée à la phase 1. Je ne vais pas mettre tout le code ici, mais voici l'explication du code de cette fonction. La fonction commence par vérifier si le tablespace existe. Dans le cas contraire, elle envoie un message « tablespace "%s" does not exist » avec un niveau ERROR. Ensuite, les droits sont vérifiés. En fait, un seul, celui de création sur le tablespace cible. Enfin, des informations sont sauvegardées pour la fameuse phase 3.
Allez, courage, occurence suivante :
/*
* Execute ALTER TABLE SET TABLESPACE for cases where there is no tuple
* rewriting to be done, so we just want to copy the data as fast as possible.
*/
static void
ATExecSetTableSpace(Oid tableOid, Oid newTableSpace)
Cette fois, c'est l'occurence 3 qui fait appel à cette fonction. Pareil, je ne mets pas le code, mais une explication de son travail. Joie, c'est là que le boulot se fait. La fonction commence par poser un verrou AccessExclusiveLock sur la table. Les tables systèmes partagées ne sont pas déplaçables, donc elle vérifie aussi ce cas. Le tablespace cible est vérifié : il ne faut pas que ce soit pg_global, qui est le tablespace des objets systèmes non partagés. La fonction vérifie aussi le statut de la table : si elle est temporaire, elle n'est pas déplaçable. Dernière vérification, le tablespace cible est-il le tablespace où se trouve déjà la table ? Une fois toutes ces vérifications terminées, si le déplacement est possible, le boulot de copie va commencer. Tout d'abord, la fonction force l'enregistrement des blocs modifiés de cette table du cache de PostgreSQL (shared_buffers) sur le disque. Ensuite, la fonction exécute une autre fonction pour chaque fichier de la table source (en effet, une table est stockée sur plusieurs fichiers de 1 Go). Cette autre fonction, de nom copy_relation_data, s'occupe de la copie physique des données. En la parcourant rapidement, on s'aperçoit que le fichier source est copié bloc par bloc, c'est-à-dire 8 Ko par 8 Ko. Chaque copie de bloc est tracée dans les journaux de transactions dans le cas où l'archivage des journaux est activé. Les blocs ne passent pas par le cache disque de PostgreSQL, on ne risque donc pas de dévalider l'entièreté du cache simplement parce qu'on déplace des fichiers.
Faisons un petit résumé. Le fichier backend/commands/tablecmds.c contient toutes ls instructions permettant d'exécuter le déplacement d'une table/index d'un tablespace vers un autre. La copie est elle-même est tout simple : une copie de chaque bloc, un par un.
On sait maintenant comment PostgreSQL réagit à l'ordre de déplacement d'une table. Essayons d'extrapoler. Si on veut déplacer une base de données complète vers un autre tablespace, cela revient à récupérer la liste des tables et index et à les envoyer un par un vers le nouveau tablespace. En fait, plus exactement, cela revient à déplacer les fichiers qui sont dans le tablespace par défaut de la base de données vers le nouveau tablespace. En effet, si d'autres objets ont été déplacés volontairement par l'utilisateur, il ne faut pas les prendre en compte.
Du coup, on peut écrire un pseudo code :
Pour toutes les tables et index compris dans le tablespace par défaut de la base de données ciblée,
Faire l'équivalent d'un ALTER TABLE la table SET TABLESPACE le nouveau tablespace
Enregistrer que le tablespace par défaut est le tablespace cible.
Donc, le gros du travail est déjà fait. Ça nous simplifie considérablement la tâche. La prochaine étape est donc de faire accepter la nouvelle instruction à PostgreSQL en déclenchant l'exécution d'une fonction. Enfin ! On va écrire un peu de code 