Guides

Récupérer une base de données SQLite corrompue dans votre navigateur

Quand SQLite signale « database disk image is malformed », une page d'un arbre B a cessé de concorder avec ses voisines — et le correctif qui récupère vraiment vos lignes n'est pas integrity_check, mais un parcours brut des pages qui réinsère chaque ligne décodable dans un fichier propre.

L'erreur, et ce qu'elle vous dit vraiment

Vous ouvrez une base de données et le client s'arrête net :

Error: database disk image is malformed

La même condition apparaît comme SQLITE_CORRUPT dans l'API C, comme SqliteException: SQLite Error 11 dans les surcouches, et comme un « malformed » laconique depuis la console sqlite3. Quelle que soit la formulation, le moteur a suivi un pointeur à l'intérieur du fichier et est tombé sur quelque chose qui ne peut pas être une page valide, alors il s'arrête plutôt que de vous livrer des octets dont il ne peut pas répondre.

Cette rigueur explique pourquoi le fichier paraît souvent en plus mauvais état qu'il ne l'est : une seule page endommagée peut rendre une table entière illisible alors que toutes les autres pages restent intactes. Le travail de la récupération est de lire les pages directement et de sauver ce qui a survécu, et cela commence par savoir ce qu'est un fichier SQLite.

Ce que « malformed » signifie pour une base de données en arbre B

Une base de données SQLite est un fichier unique divisé en pages de taille fixe (couramment 4096 octets chacune). La première page contient l'en-tête et le schéma. Chaque table et chaque index est un arbre B : une structure ramifiée de pages pointant vers d'autres pages, se terminant par des pages feuilles qui contiennent les lignes réelles. Une ligne vit à l'intérieur d'une page sous forme de cellule, portant sa propre longueur, les types de ses colonnes et ses valeurs.

Une seule page malformée dans un arbre B suffit à rendre une table illisible via les requêtes, alors que les pages alentour restent parfaitement décodables.

« Malformed », c'est ce que dit SQLite quand les pointeurs cessent de concorder avec les données : une page de branche pointe vers un enfant qui n'est pas une page d'arbre B valide, une cellule déclare une charge utile plus longue que ce que la page peut contenir, la liste de libres boucle, ou l'octet de type d'une page ne signifie rien. SQLite considère chacun de ces cas comme un motif suffisant pour déclarer corrompu le fichier entier, même si le dommage est d'ordinaire localisé : la page défectueuse et tout ce qui en dépend en aval dans cet unique arbre B sont douteux, tandis que le reste du fichier n'est que des données ordinaires et décodables.

Comment les fichiers SQLite se corrompent vraiment

SQLite est robuste, et l'essentiel de la corruption rencontrée dans le monde réel remonte à une poignée de mécanismes plutôt qu'à un moteur instable. Celui sur lequel vous tombez détermine ce que vous pouvez espérer récupérer.

Coupure de courant ou plantage en pleine écriture

Une écriture en cours au moment où la machine a perdu le courant, où le processus a été tué ou où le conteneur a été détruit peut laisser une page à demi mise à jour. Les modes de journal d'annulation (rollback journal) et WAL sont conçus pour y survivre, mais seulement si le journal ou le log et ses garanties de fsync ont survécu eux aussi. Sur un stockage qui ment sur le vidage (flush), ce filet de sécurité disparaît et vous vous retrouvez avec une page déchirée.

Un fichier -wal absent ou dépareillé

En mode WAL, les données validées les plus récentes peuvent résider dans le fichier auxiliaire -wal, pas encore réintégrées dans la base de données principale. Copiez le .sqlite tout seul et vous laissez ces commits en rade ; associez-le à un -wal pris à un autre instant et c'est pire. Dans les deux cas cela se lit comme de la corruption, car les deux moitiés ne décrivent plus la même base de données.

Copier une base de données en cours d'utilisation sans son WAL

Copier tel quel une base de données qu'un autre processus est en train d'écrire capture un instantané incohérent : la copie attrape certaines pages avant une transaction et d'autres après. Elle paraissait correcte à la copie et échoue à l'instant où on l'ouvre ailleurs. Pour les bases de données en cours d'utilisation, utilisez l'API de sauvegarde de SQLite ou VACUUM INTO plutôt qu'une copie brute.

Deux processus qui écrivent alors qu'ils ne le devraient pas

SQLite coordonne les accès au moyen de verrous de fichier. Placez le fichier sur un partage réseau où le verrouillage n'est pas fiable, ou laissez deux processus aux hypothèses de verrouillage différentes écrire en même temps, et les mises à jour s'entrelacent dans un état qu'aucun rédacteur unique ne produirait. Les systèmes de fichiers en réseau sont le coupable récurrent, et c'est pourquoi la documentation de SQLite met en garde contre eux.

Comment fonctionne réellement le sauvetage façon .recover

Le réflexe est de dégainer PRAGMA integrity_check. Cela vaut la peine de le lancer, mais soyez clair sur ce qu'il fait : il signale les dégâts, sans rien réparer ni sortir vos lignes. Pour cela, il vous faut l'approche que suit la commande .recover de la console sqlite3, et elle n'a rien à voir avec une requête normale.

Au lieu de se fier aux pointeurs de l'arbre B, le sauvetage parcourt le fichier page par page. Pour chaque page qui ressemble à une feuille de table, il lit chaque cellule directement et décode la ligne : rowid, types sériels des colonnes et valeurs. Chaque ligne décodable est réinsérée dans une base de données neuve et vide, bâtie à partir du schéma qui peut être reconstruit. Les pages endommagées sont ignorées ; les pages lisibles livrent leurs lignes, que l'arbre qui les indexait ait survécu ou non.

Les lignes dont la table d'origine ne peut pas être déterminée ne sont pas jetées : elles atterrissent dans une table nommée par convention lost_and_found, indexée par la page et la cellule dont elles proviennent, afin que vous puissiez les inspecter et les retrier à la main. Le résultat est un fichier neuf qui s'ouvre proprement et contient toutes les lignes qui étaient décodables dans l'original.

C'est la limite honnête. Le sauvetage récupère ce qui a survécu sur le disque ; les lignes de la page défectueuse, ou des pages en aval qui n'ont jamais pu être analysées, ne sont pas reconstruites. Un fichier SQLite ne possède aucune redondance à partir de laquelle les régénérer, si bien qu'une ligne qui n'est décodable nulle part a tout simplement disparu. La récupération rebâtit une base de données propre autour des données qui ont subsisté ; elle n'invente jamais des données qui n'existaient pas.

À notre connaissance, aucun outil de navigateur côté client n'avait proposé cela auparavant. Un sauvetage de ce type imposait une ligne de commande et une compilation locale de SQLite, ce qui l'excluait pour quiconque ne peut pas, ou ne doit pas, poser d'abord le fichier sur un serveur. Exécuter le parcours des pages dans le navigateur comble ce manque : même technique, sans téléversement.

Le résultat récupéré est toujours suspect

Une base de données sauvée s'ouvre et se requête proprement, ce qui incite à la traiter comme faisant autorité. Résistez : le parcours des pages décode des octets bruts sans les contraintes qu'impose une base de données saine, de sorte que le résultat traîne des bizarreries qui méritent une vérification :

  • Suppressions ressuscitées. Une ligne que vous avez supprimée subsiste sur sa page jusqu'à ce que cet espace soit réutilisé, et le sauvetage ne sait pas distinguer une ligne vivante d'une ligne supprimée-mais-non-écrasée : les enregistrements supprimés peuvent donc réapparaître.
  • Changements de type. Chaque valeur est stockée avec un type sériel. Décodée sous un type inattendu, un entier peut revenir sous forme de blob ou la précision d'un nombre peut se décaler. Vérifiez par sondage les colonnes dont les types comptent.
  • Orphelines dans lost_and_found. Les lignes de lost_and_found arrivent sans leur table d'origine ni un ordre fiable ; il vous faudra le schéma et votre propre connaissance des données pour les remettre à leur place.
  • Invariants rompus. Les clés étrangères, les contraintes d'unicité et les déclencheurs ont été contournés lors de la réinsertion, si bien qu'une base de données récupérée peut contenir des combinaisons que le schéma d'origine aurait rejetées.

Traitez une base de données récupérée comme une piste de grande qualité, pas comme un verdict : vérifiez-la par rapport à des enregistrements que vous savez corrects avant que quoi que ce soit en aval en dépende.

Gérer un fichier -wal voisin

Avant de conclure que la base de données elle-même est endommagée, cherchez un fichier -wal à côté d'elle (et son compagnon -shm). En mode WAL, les transactions validées les plus récentes peuvent encore se trouver dans ce log, pas encore reportées (checkpoint) dans le fichier principal. Récupérez sans elles et vous manquerez justement les données les plus récentes, celles qui vous importent le plus.

Gardez le trio ensemble : le .sqlite, le -walet le -shm, copiés au même instant. Ouverts ensemble par un SQLite sain, le log est appliqué et la base de données peut se révéler cohérente en fin de compte, sans aucun sauvetage nécessaire. Quand elle est réellement corrompue, un sauvetage qui lit aussi le WAL peut décoder les pages validées qu'il contient. Le risque inverse : un -walprovenant d'une autre base de données, ou un log périmé issu d'une ancienne session, fabrique de la corruption lorsqu'il est associé au mauvais fichier. Si la provenance d'un fichier auxiliaire est incertaine, sauvez la base de données toute seule et comparez.

FAQ

Que signifie « database disk image is malformed » ?

Cela signifie que SQLite a suivi un pointeur à l'intérieur du fichier et est tombé sur quelque chose qui ne peut pas être une page de base de données valide : une cellule d'arbre B qui pointe au-delà de la fin d'une page, une page dont l'octet de type n'a aucun sens, une liste de libres qui boucle, ou une ligne dont la longueur de charge utile est en désaccord avec son en-tête. SQLite lève SQLITE_CORRUPT dès qu'il ne peut plus se fier à la structure. Il nomme un symptôme, pas une cause, et ne vous dit pas combien de lignes sont touchées.

Peut-on récupérer une base de données SQLite corrompue ?

En général une bonne partie, oui. Chaque table et chaque index est un arbre B réparti sur des pages de taille fixe, et une seule page défectueuse détruit rarement les autres. Un sauvetage façon .recover parcourt les pages brutes, décode chaque cellule qu'il parvient à interpréter et réinsère ces lignes dans une base de données neuve, plus une table lost_and_found pour les lignes dont la table d'origine n'a pas pu être identifiée.

Dois-je lancer la récupération sur le fichier original ?

Jamais sur votre unique copie. Faites d'abord une copie octet par octet et sauvez à partir de celle-ci, de sorte qu'une tentative ratée ne vous coûte rien. Si un fichier -wal ou -shm se trouve à côté de la base de données, copiez-les aussi, car ils peuvent contenir les changements validés les plus récents. Le sauvetage écrit un fichier de sortie entièrement neuf et laisse l'entrée intacte, mais une copie de secours reste une assurance bon marché.

Pourquoi les données récupérées semblent-elles parfois erronées ?

Parce que le sauvetage décode les octets qui se trouvent physiquement dans chaque cellule, sans les contrôles qu'impose une base de données saine. Des lignes supprimées peuvent réapparaître, des valeurs peuvent revenir avec le mauvais type et les lignes orphelines atterrissent dans lost_and_found sans ordre fiable. Le résultat récupéré est toujours suspect : vérifiez-le par rapport à des enregistrements que vous savez corrects plutôt que de vous y fier aveuglément.

Lecture complémentaire : est-il sûr de téléverser vos fichiers vers un outil de réparation en ligne ?