Ripara ora
- Trascina il file
- La riparazione avviene in locale
- Scarica il risultato
Quando SQLite — o qualsiasi cosa costruita su di esso: la cronologia di un browser, il backup di uno smartphone, il database locale di un'app di messaggistica, le impostazioni di un'applicazione desktop — segnala "database disk image is malformed", ha incontrato SQLITE_CORRUPT (codice di risultato 11). Stava percorrendo il b-tree che indicizza le tue tabelle ed è atterrato su una pagina il cui byte di tipo, i puntatori di cella o l'intestazione del record contraddicono il formato, quindi si ferma invece di restituire spazzatura. Trascina qui sopra il file .db, .sqlite o .sqlite3 e lo strumento lo legge pagina per pagina nel tuo browser, segue la struttura di b-tree che sopravvive, spazza via il resto con una scansione senza puntatori e scrive ogni riga decodificabile in un database nuovo e apribile — senza che il file lasci mai il tuo dispositivo.
Cosa significa "malformed" a livello di pagina
Un database SQLite è un unico file suddiviso in pagine di dimensione fissa — una potenza di due tra 512 e 65536 byte, memorizzata come valore a 16 bit all'offset 16 dell'intestazione (il valore su disco 1 è il modo in cui la specifica rappresenta 65536). Ogni file inizia con la stringa magica di 16 byte SQLite format 3\0, seguita da un'intestazione di 100 byte che registra la dimensione di pagina, la codifica del testo all'offset 56 (1 = UTF-8, 2 = UTF-16le, 3 = UTF-16be), il numero di pagine dichiarato all'offset 28 e la lista dei liberi (freelist). Se la dimensione di pagina viene letta male, ogni pagina successiva alla prima cade all'offset sbagliato — ed è uno dei modi in cui un file diventa "malformed" senza che venga toccata una sola riga.
All'interno, i dati vivono in b-tree. Ogni pagina inizia con un byte di tipo: 0x0d per una foglia di tabella (che contiene le righe vere e proprie), 0x05 per una interna di tabella (che contiene solo puntatori a pagine figlie) e 0x0a/0x02 per gli equivalenti dell'indice. La pagina 1 è speciale — la sua intestazione di b-tree si trova dopo l'intestazione di database di 100 byte — ed è la radice di sqlite_master, la tabella di schema le cui righe sono (type, name, tbl_name, rootpage, sql). Per leggere una delle tue tabelle, SQLite cerca lì la sua rootpage, poi scende attraverso le pagine interne fino alle foglie, leggendo da ogni cella la lunghezza del payload (varint), il rowid (varint) e il corpo del record.
Poiché una query attraversa questo albero, un solo collegamento rotto avvelena tutto ciò che sta al di sotto. Una pagina interna azzerata, un puntatore di cella che punta oltre la fine della pagina, un puntatore all'estremità destra che forma un ciclo, o un'intestazione di record i cui tipi seriali non tornano — ognuno di questi fa sì che SQLite sollevi "database disk image is malformed" nel momento in cui calpesta il danno. Le righe nelle pagine foglia sane sono intatte; SQLite semplicemente non riesce a raggiungerle attraverso un indice rotto. PRAGMA integrity_check segnala questa stessa classe di guasto.
Come un percorso grezzo delle pagine recupera le tue righe
La soluzione imita ciò che fa il comando .recover della shell di SQLite, ma viene eseguita interamente nel browser. Per prima cosa lo strumento legge sqlite_master dalla pagina 1 per conoscere il nome di ogni tabella, le sue colonne (estratte dal SQL del suo CREATE TABLE) e la sua rootpage, poi percorre ogni b-tree da quella radice — la via rapida ed esatta quando i puntatori sono intatti. Quando un percorso incontra una pagina mancante o non valida, degrada a "incompleto" invece di fallire, conservando tutte le righe raccolte fino alla rottura.
Poi arriva la parte che supera un semplice tentativo di riapertura: uno spazzamento senza puntatori. Lo scanner ignora tutti i puntatori e tratta ogni pagina foglia di tabella (tipo 0x0d) del file come un sacco di record, decodificando ogni cella direttamente dai suoi byte — la lunghezza dell'intestazione (varint), l'array dei tipi seriali e poi ogni valore (il tipo seriale 0 è NULL, 8/9 sono i letterali 0 e 1, 7 è un float64, e i codici pari/dispari sopra 12 sono BLOB/TEXT). Le catene di overflow dei valori grandi vengono seguite di pagina in pagina. È esattamente così che le righe sopravvivono a una pagina interna azzerata, a una pagina nella freelist o a una tabella eliminata: la foglia contiene ancora i dati anche quando nulla punta più a essa. Le righe il cui numero di colonne corrisponde in modo inequivocabile a una tabella nota vengono reinserite in essa; quelle davvero non attribuibili vengono scritte in una tabella lost_and_found con le colonne pgno, cellidx, nfield, rowid e c0…cN, così che nulla di decodificabile venga scartato.
Infine lo strumento costruisce un database nuovo con sqlite3.wasm (caricato dalla stessa origine da /engines/sqlite/), emettendo un CREATE TABLE permissivo per ogni tabella — i nomi originali delle colonne e qualsiasi INTEGER PRIMARY KEY vengono conservati affinché l'alias del rowid rimanga valido, ma senza i vincoli NOT NULL/UNIQUE/CHECK/chiave esterna che rifiuterebbero una riga resuscitata — poi esegue INSERT OR IGNORE di ogni riga recuperata ed esporta il risultato come un .db pulito. Se viene fornito un file fratello -wal (registro di scrittura anticipata, write-ahead log), i suoi frame confermati vengono sovrapposti per primi, così che il recupero rifletta l'ultima transazione confermata.
Dove il dato si ferma davvero — e perché non viene caricato nulla
Il recupero raggiunge ciò che è presente; non può inventare ciò che il disco non conserva più. Se il file è stato troncato — una copia o una sincronizzazione che si è interrotta troppo presto, lasciando una dimensione che non è un multiplo esatto della dimensione di pagina — ogni pagina successiva al taglio è fisicamente sparita, e ottieni le righe che erano state scritte per intero prima di esso. Le celle che si trovano su pagine intatte ma la cui intestazione di record è incoerente con se stessa (il classico bit-rot) vengono scartate e contate, invece di essere indovinate. Un database cifrato (SQLCipher o l'estensione SEE) non ha alcuna stringa magica in chiaro — la sua prima pagina è testo cifrato — quindi senza la chiave non c'è nulla da percorrere, e il recupero non viene tentato.
Due avvertenze di onestà su cui SQLite stesso insiste. Le righe eliminate possono riapparire: la scansione senza puntatori legge le pagine della freelist e quelle non compattate (vacuum), quindi record che credevi spariti possono riaffiorare nel risultato. E i valori recuperati riflettono le classi di memorizzazione su disco, non le affinità di colonna dichiarate — il tipo apparente di una colonna può cambiare. La guida di SQLite stessa è netta: i dati estratti da un database corrotto sono sempre sospetti. Verificali rispetto a una fonte attendibile prima di farci affidamento.
Tutto questo viene eseguito nella scheda del tuo browser — lo scanner è TypeScript senza dipendenze e lo scrittore è WASM, quindi non c'è alcun viaggio di andata e ritorno verso un server. Questo conta quando il database è un gestore di password, una cronologia di chat, dati sanitari o l'archivio privato di un'app: puoi aprire la scheda Rete e verificare che 0 byte del file lasciano il tuo dispositivo.
Cosa può e cosa non può riparare
Può riparare
- Database che sollevano SQLITE_CORRUPT / "database disk image is malformed" a causa di una pagina o un puntatore di b-tree danneggiato
- Righe su pagine foglia di tabella sane rese irraggiungibili da una pagina interna rotta o da una rootpage difettosa
- Righe su pagine interne azzerate, pagine nella freelist o tabelle eliminate, recuperate dalla scansione delle foglie senza puntatori
- Valori grandi traboccati in catene di pagine di overflow, riassemblati durante il percorso
- Modifiche senza checkpoint in un file fratello -wal, sovrapposte prima della scansione
Non può riparare
- Righe oltre il taglio in un file troncato — quelle pagine sono fisicamente sparite
- Celle la cui intestazione di record è troppo corrotta per essere decodificata (bit-rot) — vengono scartate, non indovinate
- Database cifrati SQLCipher/SEE senza la chiave (le pagine sono testo cifrato)
- Le garanzie originali UNIQUE/CHECK/chiave esterna — il recupero allenta i vincoli affinché le righe resuscitate non vengano rifiutate
- Un file di 0 byte oppure senza la stringa magica di SQLite e senza pagine foglia decodificabili
Se una riparazione fallisce, ti diciamo perché (dati mancanti rispetto a struttura danneggiata), e non ti viene mai addebitato nulla per una riparazione fallita.