ERROR: pg_attribute catalog is missing 1 attribute(s) for relation OID…

Après le déplacement d’une application web et de sa base de données PostgreSQL d’un serveur à un autre, j’ai eu une erreur surprenante. Dans les journaux de mon application, je peux lire :

ERROR:  pg_attribute catalog is missing 1 attribute(s) for relation OID 25947 at character 15

J’imagine assez vite qu’il s’agit d’une erreur de la base de données PostgreSQL et après avoir ouvert son journal, je trouve là même erreur.

Après quelques recherches, je comprend que mon déménagement ne s’est pas si bien passé et que ma base de données est corrompu. 😱

Tous ce que je trouve d’intéressant c’est de :

  1. Faire une sauvegarde du répertoire /var/lib/postgresql dans le doute
  2. Travaillé sur une copie pour être sûr de ne pas empirer les choses sur le serveur de production
  3. Si possible ré-appliquer la sauvegarde pour revenir à un état fonctionnel

Le point 1 et 2 me paressent pertinent. Par contre, ça fait 2 jours que mon application tourne sur la nouvelle base de données et si j’applique une sauvegarde, je vais perdre les données d’au moins toute la journée voir des deux jours. Je décide donc d’essayer de comprendre un peu ce qu’il se passe et voir si je peux corriger le problème à la main.

Mes recherches m’apprennent que la table pg_attribute fait partie du schéma pg_catalog et que celui-ci contient les tables système et tous les types de données, fonctions et opérateurs intégrés. Et plus spécifiquement, la table pg_attribute stocke les informations concernant les colonnes des tables. Il y a exactement une ligne de pg_attribute par colonne de table de la base de données.

Ahah ! Je comprends pourquoi personne n’a trop envie de toucher à cela, le risque est gros !

Bon, je me lance quand même !

Je me connecte sur ma base de données avec l’utilisateur postgres pour avoir les droits d’administrer ma base et je lance la commande suivante :

SELECT * FROM pg_catalog.pg_attribute WHERE attrelid = 25947;

Ca me ressort 13 lignes. J’exécute la même requête sur l’ancienne base de donnes (oui heureusement pour moi j’ai encore accès à l’ancien serveur avec la base de données en l’état avant le déménagement).

Ca ne me ressort rien. Je suppose que les attrelid sont sûrement différents…

La doc m’apprend que le champ attname correspond au nom de la colonne de la table. Aller avec un peu de chance je vais pouvoir trouver un discriminent dedans.

Dans les 13 lignes, un attname a la valeur recurrenceid, ça me semble un bon départ. Je fais une recherche dans mon ancienne base avec cet attname et… Oui ! Il n’y a qu’une occurrence ce qui me permet de trouver le attrelid correspondant !

Je lance le SELECT sur l’ancienne base et… tiens tiens, il y a 14 lignes ! OK donc il en manque bien une sur la nouvelle. Ah oui et il a pour attname id. Je profite de Beekeeper Studio pour faire un copie qui me donne directement l’insertion SQL suivante :

insert into "pg_catalog"."pg_attribute" ("attacl", "attalign", "attbyval", "attcacheoff", "attcollation", "attcompression", "attfdwoptions", "attgenerated", "atthasdef", "atthasmissing", "attidentity", "attinhcount", "attisdropped", "attislocal", "attlen", "attmissingval", "attname", "attndims", "attnotnull", "attnum", "attoptions", "attrelid", "attstattarget", "attstorage", "atttypid", "atttypmod") values (NULL, 'i', true, -1, 0, '', NULL, '', true, false, '', 0, false, true, 4, NULL, 'id', 0, true, 1, NULL, 15342, -1, 'p', 23, -1)

Aller, je prends juste garde à changer l’attrelid et je lance cette commande sur ma nouvelle base.

ERROR:  duplicate key value violates unique constraint "pg_attribute_relid_attnam_index"
DETAIL:  Key (attrelid, attname)=(25947, id) already exists.

Pardon ?!?!?! Il existe déjà ??? S’il existe pourquoi il ne ressort pas avec le SELECT… 😠

Par acquis de conscience, je vérifie avec un SELECT plus ciblé :

SELECT * FROM pg_catalog.pg_attribute WHERE attrelid = 25947 AND attname = 'id';

Et là oui… Ca me renvoie bien une ligne… Wow… 🤯

Je refais le SELECT sans le critère attname qui me renvoie toujours 13 ligne sans de attname = ‘id’…

Ce n’est pas cohérent… Bon… Je tente le tout pour le tout. Je vais supprimer la ligne et la créer en suite. Ce qui donne :

DELETE FROM pg_catalog.pg_attribute WHERE attrelid = 25947 AND attname = 'id'; insert into "pg_catalog"."pg_attribute" ("attacl", "attalign", "attbyval", "attcacheoff", "attcollation", "attcompression", "attfdwoptions", "attgenerated", "atthasdef", "atthasmissing", "attidentity", "attinhcount", "attisdropped", "attislocal", "attlen", "attmissingval", "attname", "attndims", "attnotnull", "attnum", "attoptions", "attrelid", "attstattarget", "attstorage", "atttypid", "atttypmod") values (NULL, 'i', true, -1, 0, '', NULL, '', true, false, '', 0, false, true, 4, NULL, 'id', 0, true, 1, NULL, 25947, -1, 'p', 23, -1);

Qui me retourne :

DELETE 1
INSERT 0 1

Une ligne supprimée et une ligne insérée !!! Aller on croise les doigts ! 🤞

Ouiii ça a fonctionné ! Ma base de donnée ne renvoie plus d’erreur lors de l’utilisation de l’application web !!! 🥳

Je me dis quand même que ce n’est peut-être pas un cas isolé alors je cherche une méthode pour tester toutes les tables de ma base.

pg_dump -f /dev/null my_db

Et j’ai bien fait, j’ai trouvé ainsi 3 autres tables corrompues ! En appliquant la même méthode j’ai pu tout réparer. J’ai évidemment vérifier de nouveau avec pg_dump et cette fois pas d’erreur !

Ouf ! Ma base de données est sauf ! Et, mise à part les données qui n’ont pas pu s’écrire dans les tables corrompues ces deux derniers jours, je n’ai perdu aucune donnée ! 🎉

Sources :

  • https://docs.postgresql.fr/15/ddl-schemas.html#DDL-SCHEMAS-CATALOG
  • https://docs.postgresql.fr/15/catalog-pg-attribute.html