Lors d'une mise à jour de Nuxeo Drive, il s'avère qu'une base de données SQLite n'ait pas aimé :
sqlite3.DatabaseError: database disk image is malformed
J'avais déjà eu ce genre de soucis et avais corrigé le fichier avec sqlite3
directement :
$ sqlite3 "bad.db" .dump | sqlite3 "good.db"
Simple, rapide et efficace.
Mais sqlite3
n'est jamais installé sur les machines des clients. Une solution en Python pur est donc nécessaire.
Imports
Pour commencer, nous aurons besoin de ces imports :
import os
import os.path
import sqlite3
from shutil import copyfile
Check-up
Ensuite, il nous faut un moyen de vérifier que ladite base de données est bien corrompue :
def is_healthy(database: str) -> bool:
"""
Integrity check of the entire database.
http://www.sqlite.org/pragma.html#pragma_integrity_check
"""
with sqlite3.connect(database) as con:
status = con.cursor().execute('PRAGMA integrity_check(1)').fetchone()
return status[0] == 'ok'
Le pragma integrity_check
peut être gourmant, ici je l'appelle avec un argument 1
pour arrêter la vérification dès la 1ère erreur. Sans argument (ni parenthèses du coup), sera retournée la liste des 100 premières erreurs trouvées.
Si le fichier est sain, une seule ligne contenant ok
sera retournée.
L'équivalent en ligne de commande :
$ sqlite3 "bad.db" "PRAGMA integrity_check(2)"
*** in database main ***
Page 11: btreeInitPage() returns error code 11
Main freelist: 166 of 10945 pages missing from overflow list starting at 18207
$ sqlite3 "good.db" "PRAGMA integrity_check"
ok
Export
Vient ensuite le moment d'exporter le contenu de la base de donnée endommagée :
def dump(database: str, dump_file: str) -> None:
"""
Dump the entire database content into `dump_file`.
This function provides the same capabilities as the .dump command
in the sqlite3 shell.
"""
with sqlite3.connect(database) as con:
with open(dump_file, 'w', encoding='utf-8') as f:
for line in con.iterdump():
f.write(f'{line}\n')
L'équivalent en ligne de commande :
$ sqlite3 "bad.db" .dump > $dump_file
Import
Pour faire l'opération inverse :
def read(dump_file: str, database: str) -> None:
"""
Load the `dump_file` content into the given database.
This function provides the same capabilities as the .read command
in the sqlite3 shell.
"""
with sqlite3.connect(database) as con:
with open(dump_file, encoding='utf-8') as f:
con.executescript(f.read())
L'équivalent en ligne de commande :
$ sqlite3 "good.db" ".read $dump_file"
# ou
$ sqlite3 "good.db" < $dump_file
Empaquetage
Enfin, pour souder le tout :
def fix_db(database: str, dump_file: str = 'dump.sql') -> None:
"""
Re-generate the whole database content to fix eventual FS corruptions.
This will prevent `sqlite3.DatabaseError: database disk image is malformed`
issues. The whole operation is quick and help saving disk space.
Will raise sqlite3.DatabaseError in case of unrecoverable file.
"""
if is_healthy(database):
return
dump(database, dump_file)
read(dump_file, database)
os.remove(dump_file)
Et voilà !
L'équivalent en ligne de commande :
$ sqlite3 "bad.db" .dump | sqlite3 "good.db"
Petit bonus de fin : le fichier réparé sera plus léger car il est compacté et purgé des données vides.
Finalement, tout cela est possible grâce aux auteurs de SQLite : How SQLite Is Tested, des malades ! ☢
Sources :
- NXDRIVE-605: Handle corrupted SQLite database
- Why does an sqlite3 .dump and restore result in a smaller database file size?
- How To Corrupt An SQLite Database File
Historique
- 2019-07-07 : Utilisation de l'encodage UTF-8 lors de l'ouverture des fichiers.
- 2018-07-02 : Suppression des lignes de log et utilisation de Python 3.6+.