L'errore, e cosa ti sta davvero dicendo
Apri un database e il client si blocca di colpo:
Error: database disk image is malformed
La stessa condizione compare come SQLITE_CORRUPT nell'API C, come SqliteException: SQLite Error 11 nei wrapper e come un laconico "malformed" dalla console sqlite3. Qualunque sia la formulazione, il motore ha seguito un puntatore dentro il file ed è atterrato su qualcosa che non può essere una pagina valida, quindi si ferma invece di consegnarti byte di cui non può rispondere.
È questo rigore il motivo per cui il file spesso sembra messo peggio di com'è: una sola pagina danneggiata può rendere illeggibile un'intera tabella mentre tutte le altre pagine restano lì intatte. Il compito del recupero è leggere le pagine direttamente e salvare ciò che è sopravvissuto, e questo comincia dal sapere cos'è un file SQLite.
Cosa significa «malformed» per un database a b-tree
Un database SQLite è un unico file diviso in pagine di dimensione fissa (di solito 4096 byte ciascuna). La prima pagina contiene l'intestazione e lo schema. Ogni tabella e indice è un b-tree: una struttura ramificata di pagine che puntano ad altre pagine, terminando in pagine foglia che contengono le righe vere e proprie. Una riga vive dentro una pagina come una cella, che porta con sé la propria lunghezza, i tipi delle colonne e i valori.
Una sola pagina malformata in un b-tree basta a rendere una tabella illeggibile attraverso le query, mentre le pagine circostanti restano perfettamente decodificabili.
«Malformed» è ciò che dice SQLite quando i puntatori smettono di concordare con i dati: una pagina ramo punta a un figlio che non è una pagina b-tree valida, una cella dichiara un payload più lungo di quanto la pagina possa contenere, la freelist forma un ciclo, oppure il byte di tipo di una pagina non significa nulla. SQLite tratta uno qualsiasi di questi casi come motivo per dichiarare corrotto l'intero file, anche se il danno è di solito localizzato: la pagina danneggiata e tutto ciò che ne dipende a valle in quell'unico b-tree sono in dubbio, mentre il resto del file sono dati ordinari e decodificabili.
Come si corrompono davvero i file SQLite
SQLite è robusto, e la maggior parte della corruzione del mondo reale si riconduce a una manciata di meccanismi più che a un motore inaffidabile. Quale ti capita determina cosa puoi aspettarti di riavere indietro.
Interruzione di corrente o crash a metà scrittura
Una scrittura in corso quando la macchina ha perso corrente, il processo è stato terminato o il container è stato smontato può lasciare una pagina aggiornata a metà. Le modalità rollback-journal e WAL sono progettate per sopravvivere a questo, ma solo se sono sopravvissuti anche il journal o il log e le loro garanzie di fsync. Su uno storage che mente sul flush, quella rete di sicurezza sparisce e ti ritrovi con una pagina lacerata.
Un file -wal mancante o non abbinato
In modalità WAL, i dati confermati più recenti possono vivere nel file accessorio -wal, non ancora reintegrati nel database principale. Copia il .sqlite da solo e lasci a terra quei commit; abbinalo a un -wal di un altro momento nel tempo ed è peggio. In entrambi i casi viene letto come corruzione, perché le due metà non descrivono più lo stesso database.
Copiare un database attivo senza il suo WAL
Copiare senza accorgimenti un database che un altro processo sta scrivendo attivamente cattura uno snapshot incoerente: la copia prende alcune pagine prima di una transazione e altre dopo. Sembrava a posto al momento della copia e fallisce nell'istante in cui viene aperta altrove. Per i database attivi usa l'API di backup di SQLite o VACUUM INTO invece di una copia grezza.
Due scrittori che non dovrebbero esserlo
SQLite coordina l'accesso tramite i lock sui file. Metti il file su una condivisione di rete dove il locking è inaffidabile, oppure lascia che due processi con presupposti di locking diversi scrivano allo stesso tempo, e gli aggiornamenti si intrecciano in uno stato che nessun singolo scrittore produrrebbe. I filesystem in rete sono il colpevole ricorrente, ed è per questo che la documentazione di SQLite mette in guardia contro di essi.
Come funziona davvero il recupero in stile .recover
L'istinto è quello di ricorrere a PRAGMA integrity_check. Vale la pena eseguirlo, ma sii chiaro su cosa fa: segnala il danno, senza aggiustare nulla né tirare fuori le tue righe. Per quello ti serve l'approccio del comando .recover della console sqlite3, e funziona in modo completamente diverso da una query normale.
Invece di fidarsi dei puntatori del b-tree, il recupero percorre il file pagina per pagina. Per ogni pagina che sembra una foglia di tabella, legge ogni cella direttamente e decodifica la riga: rowid, tipi seriali delle colonne e valori. Ogni riga decodificabile viene reinserita in un database nuovo e vuoto, costruito a partire da qualunque schema si riesca a ricostruire. Le pagine danneggiate vengono saltate; le pagine leggibili cedono le loro righe, che l'albero che le indicizzava sia sopravvissuto o meno.
Le righe la cui tabella di origine non può essere determinata non vengono buttate via: finiscono in una tabella che per convenzione si chiama lost_and_found, identificata dalla pagina e dalla cella da cui provengono, così puoi ispezionarle e riordinarle a mano. L'output è un file nuovo che si apre in modo pulito e contiene ogni riga che era decodificabile nell'originale.
Questo è il tetto onesto. Il recupero recupera ciò che è sopravvissuto su disco; le righe sulla pagina danneggiata, o su pagine a valle che non sono mai state analizzate, non vengono ricostruite. Un file SQLite non ha ridondanza da cui rigenerarle, quindi una riga che non è decodificabile da nessuna parte è semplicemente persa. Il recupero ricostruisce un database pulito attorno ai dati che sono rimasti in vita; non inventa mai dati che non c'erano.
Per quanto ne sappiamo, nessuno strumento da browser lato client aveva mai offerto questo prima. Un recupero di questo tipo ha sempre significato riga di comando e una build locale di SQLite, il che lo escludeva per chiunque non possa, o non debba, mettere prima il file su un server. Eseguire il percorso tra le pagine nel browser colma quel divario: stessa tecnica, nessun caricamento.
L'output recuperato è sempre sospetto
Un database recuperato si apre e si interroga in modo pulito, il che tenta a trattarlo come autorevole. Resisti: il percorso tra le pagine decodifica byte grezzi senza i vincoli che un database sano impone, quindi l'output porta con sé stranezze che vale la pena controllare:
- Cancellazioni risorte. Una riga che hai cancellato resta viva sulla sua pagina finché quello spazio non viene riutilizzato, e il recupero non sa distinguere una riga viva da una cancellata-ma-non-sovrascritta, quindi i record cancellati possono riaffiorare.
- Cambi di tipo. Ogni valore è memorizzato con un tipo seriale. Decodificato sotto uno inatteso, un intero può tornare come un blob oppure la precisione di un numero può cambiare. Controlla a campione le colonne i cui tipi contano.
- Orfane in lost_and_found. Le righe in
lost_and_foundarrivano senza la loro tabella originale né un ordine affidabile; ti serviranno lo schema e la tua conoscenza dei dati per rimetterle al loro posto. - Invarianti infrante. Le chiavi esterne, i vincoli di unicità e i trigger sono stati aggirati durante il reinserimento, quindi un database recuperato può contenere combinazioni che lo schema originale avrebbe rifiutato.
Tratta un database recuperato come una pista di alta qualità, non come un verdetto: verificalo rispetto a record noti come corretti prima che qualsiasi processo a valle vi dipenda.
Come gestire un file -wal gemello
Prima di concludere che il database in sé è danneggiato, cerca un file -wal accanto a esso (e il suo compagno -shm). In modalità WAL, le transazioni confermate più recenti possono essere ancora in quel log, non ancora riversate (checkpoint) nel file principale. Recupera senza di esse e ti perderai proprio i dati più nuovi, quelli che ti stanno più a cuore.
Tieni insieme il trio: il .sqlite, il -wale lo -shm, copiati dallo stesso momento. Aperti insieme da un SQLite sano, il log viene applicato e il database potrebbe rivelarsi coerente dopotutto, senza bisogno di recupero. Quando è genuinamente corrotto, un recupero che legge anche il WAL può decodificare le pagine confermate che vi sono contenute. Il rischio opposto: un -waldi un altro database, o uno obsoleto di una vecchia sessione, fabbrica corruzione quando viene abbinato al file sbagliato. Se la provenienza di un file accessorio è incerta, recupera il database da solo e confronta.
Domande frequenti
Cosa significa «database disk image is malformed»?
Significa che SQLite ha seguito un puntatore all'interno del file e ha trovato qualcosa che non può essere una pagina di database valida: una cella del b-tree che punta oltre la fine di una pagina, una pagina il cui byte di tipo non ha senso, una freelist che forma un ciclo, o una riga la cui lunghezza del payload non concorda con la sua intestazione. SQLite solleva SQLITE_CORRUPT nel momento in cui non può più fidarsi della struttura. Indica un sintomo, non una causa, e non ti dice quante righe sono interessate.
Un database SQLite corrotto si può recuperare?
Di solito una buona parte, sì. Ogni tabella e indice è un b-tree distribuito su pagine di dimensione fissa, e una singola pagina danneggiata raramente distrugge le altre. Un recupero in stile .recover percorre le pagine grezze, decodifica ogni cella che riesce a interpretare e reinserisce quelle righe in un database nuovo, più una tabella lost_and_found per le righe la cui tabella di origine non è stata identificata.
Dovrei eseguire il recupero sul file originale?
Mai sulla tua unica copia. Fai prima una copia byte per byte e recupera da quella, così un tentativo fallito non ti costa nulla. Se accanto al database c'è un file -wal o -shm, copia anche quelli, perché potrebbero contenere le modifiche confermate più recenti. Il recupero scrive un file di output completamente nuovo e lascia intatto l'input, ma una copia di riserva resta comunque un'assicurazione a basso costo.
Perché a volte i dati recuperati sembrano sbagliati?
Perché il recupero decodifica i byte che sono fisicamente presenti in ogni cella, senza i controlli che un database sano impone. Righe cancellate possono riaffiorare, i valori possono tornare con il tipo sbagliato e le righe orfane finiscono in lost_and_found senza un ordine affidabile. L'output recuperato è sempre sospetto: verificalo rispetto a record noti come corretti invece di fidartene ciecamente.
Letture correlate: è sicuro caricare i tuoi file su uno strumento di riparazione online?