El error, y lo que de verdad te está diciendo
Abres una base de datos y el cliente se detiene en seco:
Error: database disk image is malformed
La misma condición aparece como SQLITE_CORRUPT en la API de C, como SqliteException: SQLite Error 11 en los envoltorios y como un escueto «malformed» desde la consola sqlite3. Sea cual sea la redacción, el motor siguió un puntero dentro del archivo y aterrizó en algo que no puede ser una página válida, así que se detiene en lugar de entregarte bytes de los que no puede responder.
Esa rigidez es la razón de que el archivo parezca a menudo peor de lo que está: una sola página dañada puede dejar ilegible una tabla entera mientras las demás páginas siguen ahí intactas. El trabajo de la recuperación es leer las páginas directamente y rescatar lo que sobrevivió, y eso empieza por saber qué es un archivo SQLite.
Qué significa «malformed» para una base de datos de árbol B
Una base de datos SQLite es un único archivo dividido en páginas de tamaño fijo (habitualmente 4096 bytes cada una). La primera página contiene la cabecera y el esquema. Cada tabla e índice es un árbol B: una estructura ramificada de páginas que apuntan a otras páginas, terminando en páginas hoja que contienen las filas reales. Una fila vive dentro de una página como una celda, que lleva su propia longitud, tipos de columna y valores.
Una sola página malformada en un árbol B basta para dejar ilegible una tabla a través de las consultas, mientras las páginas de alrededor siguen siendo perfectamente descodificables.
«Malformed» es lo que dice SQLite cuando los punteros dejan de concordar con los datos: una página rama apunta a un hijo que no es una página de árbol B válida, una celda declara una carga útil más larga de lo que la página puede contener, la lista de libres forma un bucle o el byte de tipo de una página no significa nada. SQLite trata cualquiera de estos casos como motivo para declarar corrupto el archivo entero, aunque el daño suele ser local: la página dañada y todo lo que cuelga de ella en ese único árbol B quedan en duda, mientras que el resto del archivo son datos corrientes y descodificables.
Cómo se corrompen de verdad los archivos SQLite
SQLite es robusto, y la mayor parte de la corrupción del mundo real se remonta a un puñado de mecanismos más que a un motor inestable. El que te toque condiciona lo que puedes esperar recuperar.
Corte de corriente o cuelgue a mitad de escritura
Una escritura en curso cuando la máquina se quedó sin corriente, el proceso fue terminado o el contenedor fue destruido puede dejar una página actualizada a medias. Los modos de diario de reversión y WAL están diseñados para sobrevivir a esto, pero solo si el diario o el registro y sus garantías de fsync sobrevivieron también. En almacenamiento que miente sobre el vaciado, esa red de seguridad desaparece y te queda una página rota.
Un archivo -wal ausente o desemparejado
En modo WAL, los datos confirmados más recientes pueden vivir en el archivo auxiliar -wal, todavía sin integrarse en la base de datos principal. Copia el .sqlite por su cuenta y dejarás varados esos commits; emparéjalo con un -wal de otro momento en el tiempo y es peor. En cualquiera de los dos casos se lee como corrupción, porque las dos mitades ya no describen la misma base de datos.
Copiar una base de datos en uso sin su WAL
Copiar sin más una base de datos que otro proceso está escribiendo activamente captura una instantánea inconsistente: la copia atrapa algunas páginas antes de una transacción y otras después. Parecía correcta al copiarla y falla en el instante en que se abre en otro sitio. Usa la API de copia de seguridad de SQLite o VACUUM INTO para bases de datos en uso en lugar de una copia en bruto.
Dos escritores que no deberían serlo
SQLite coordina el acceso mediante bloqueos de archivo. Pon el archivo en un recurso compartido de red donde el bloqueo no es fiable, o deja que dos procesos con supuestos de bloqueo distintos escriban a la vez, y las actualizaciones se entrelazan en un estado que ningún escritor único produciría. Los sistemas de archivos en red son el infractor recurrente, y por eso la documentación de SQLite advierte contra ellos.
Cómo funciona de verdad el rescate al estilo .recover
El acto reflejo es echar mano de PRAGMA integrity_check. Vale la pena ejecutarlo, pero ten claro qué hace: informa del daño, sin arreglar nada ni sacar tus filas. Para eso quieres el enfoque que toma el comando .recover de la consola sqlite3, y funciona de un modo muy distinto a una consulta normal.
En lugar de fiarse de los punteros del árbol B, el rescate recorre el archivo página a página. Por cada página que parece una hoja de tabla, lee cada celda directamente y descodifica la fila: rowid, tipos serie de las columnas y valores. Cada fila descodificable se reinserta en una base de datos nueva y vacía, construida a partir del esquema que se pueda reconstruir. Las páginas dañadas se saltan; las páginas legibles entregan sus filas, sobreviviera o no el árbol que las indexaba.
Las filas cuya tabla de origen no se puede determinar no se descartan: acaban en una tabla que por convención se llama lost_and_found, identificada por la página y la celda de la que proceden, para que puedas inspeccionarlas y reordenarlas a mano. La salida es un archivo nuevo que se abre limpiamente y contiene todas las filas que eran descodificables en el original.
Ese es el techo honesto. El rescate recupera lo que sobrevivió en disco; las filas de la página dañada, o de páginas posteriores que nunca llegaron a analizarse, no se reconstruyen. Un archivo SQLite no tiene redundancia con la que regenerarlas, así que una fila que no es descodificable en ningún sitio simplemente ha desaparecido. La recuperación reconstruye una base de datos limpia en torno a los datos que quedaron con vida; nunca inventa datos que no existían.
Que sepamos, ninguna herramienta de navegador del lado del cliente había ofrecido esto antes. Un rescate de este tipo ha significado línea de comandos y una compilación local de SQLite, lo que lo descartaba para quien no puede, o no debe, poner primero el archivo en un servidor. Ejecutar el recorrido de páginas en el navegador cierra esa brecha: la misma técnica, sin subida.
La salida recuperada siempre es sospechosa
Una base de datos rescatada se abre y se consulta limpiamente, lo que tienta a tratarla como autoritativa. Resístete: el recorrido de páginas descodifica bytes en bruto sin las restricciones que impone una base de datos sana, así que la salida arrastra rarezas que conviene comprobar:
- Borrados resucitados. Una fila que borraste sigue viva en su página hasta que ese espacio se reutiliza, y el rescate no sabe distinguir una fila viva de una borrada-pero-no-sobrescrita, así que los registros borrados pueden reaparecer.
- Cambios de tipo. Cada valor se almacena con un tipo serie. Descodificado bajo uno inesperado, un entero puede volver como un blob o la precisión de un número puede cambiar. Revisa las columnas cuyos tipos importan.
- Huérfanas en lost_and_found. Las filas de
lost_and_foundllegan sin su tabla original ni un orden fiable; necesitarás el esquema y tu propio conocimiento de los datos para devolverlas a su sitio. - Invariantes rotas. Las claves foráneas, las restricciones de unicidad y los disparadores se saltaron durante la reinserción, así que una base de datos recuperada puede contener combinaciones que el esquema original habría rechazado.
Trata una base de datos recuperada como una pista de alta calidad, no como un veredicto: verifícala contra registros que sabes que son correctos antes de que cualquier proceso dependa de ella.
Cómo tratar un archivo -wal hermano
Antes de concluir que la base de datos en sí está dañada, busca un archivo-wal junto a ella (y su compañero -shm). En modo WAL, las transacciones confirmadas más recientes pueden seguir en ese registro, aún sin volcarse (checkpoint) al archivo principal. Recupera sin ellas y te perderás justo los datos más nuevos, los que más te importan.
Mantén el trío junto: el .sqlite, el -waly el -shm, copiados del mismo momento. Abiertos juntos por un SQLite sano, el registro se aplica y la base de datos puede resultar consistente después de todo, sin necesidad de rescate. Cuando está genuinamente corrupta, un rescate que lee también el WAL puede descodificar las páginas confirmadas que contiene. El riesgo opuesto: un -walde otra base de datos, o uno obsoleto de una sesión antigua, fabrica corrupción al emparejarse con el archivo equivocado. Si la procedencia de un archivo auxiliar es incierta, rescata la base de datos por su cuenta y compara.
Preguntas frecuentes
¿Qué significa «database disk image is malformed»?
Significa que SQLite siguió un puntero dentro del archivo y encontró algo que no puede ser una página de base de datos válida: una celda de árbol B que apunta más allá del final de una página, una página cuyo byte de tipo no tiene sentido, una lista de libres que forma un bucle o una fila cuya longitud de carga útil no concuerda con su cabecera. SQLite lanza SQLITE_CORRUPT en cuanto no puede confiar en la estructura. Nombra un síntoma, no una causa, y no te dice cuántas filas están afectadas.
¿Se puede recuperar una base de datos SQLite corrupta?
Normalmente una buena parte, sí. Cada tabla e índice es un árbol B repartido en páginas de tamaño fijo, y una sola página dañada rara vez destruye las demás. Un rescate al estilo .recover recorre las páginas en bruto, descodifica cada celda que puede interpretar y reinserta esas filas en una base de datos nueva, más una tabla lost_and_found para las filas cuya tabla de origen no se pudo identificar.
¿Debería ejecutar la recuperación sobre el archivo original?
Nunca sobre tu única copia. Haz primero una copia byte a byte y rescata a partir de ella, de modo que un intento fallido no te cueste nada. Si hay un archivo -wal o -shm junto a la base de datos, cópialos también, ya que pueden contener los cambios confirmados más recientes. El rescate escribe un archivo de salida totalmente nuevo y deja la entrada intacta, pero una copia de reserva sigue siendo un seguro barato.
¿Por qué los datos recuperados a veces parecen incorrectos?
Porque el rescate descodifica los bytes que físicamente hay en cada celda, sin las comprobaciones que impone una base de datos sana. Pueden reaparecer filas borradas, los valores pueden volver con el tipo equivocado y las filas huérfanas acaban en lost_and_found sin un orden fiable. La salida recuperada siempre es sospechosa: verifícala contra registros que sabes que son correctos en lugar de fiarte sin más.
Lectura relacionada: ¿es seguro subir tus archivos a una herramienta de reparación en línea?