Repáralo ahora
- Suelta el archivo
- La reparación ocurre en local
- Descarga el resultado
Las dos capas de persistencia de Android escriben SQLite corriente: Room genera su esquema sobre el mismo motor que usa el antiguo SQLiteOpenHelper, y ambas dejan un único archivo en /data/data/<paquete>/databases/<nombre>.db. Cuando ese archivo se daña, Room lanza android.database.sqlite.SQLiteDatabaseCorruptException: database disk image is malformed (code 11 SQLITE_CORRUPT) y la app se cierra o muestra una pantalla vacía. Suelta arriba el .db extraído — y añade su archivo lateral -wal en la ranura opcional si lo tienes — y la herramienta escanea las páginas, reconstruye el esquema y vuelve a insertar cada fila que aún puede descodificar en una base de datos nueva, todo dentro de la pestaña de tu navegador. El archivo no se sube nunca.
Dónde guarda una app Android su base de datos — y qué hay dentro
En cualquier dispositivo Android la base de datos vive en el sandbox privado de la app, en /data/data/<paquete>/databases/, normalmente como tres archivos: el principal <nombre>.db, un registro de escritura anticipada <nombre>.db-wal y un índice en memoria compartida <nombre>.db-shm. El archivo principal comienza con la cadena mágica de 16 bytes "SQLite format 3\000"; el tamaño de página de dos bytes está en el desplazamiento 16, y los bytes de versión de lectura/escritura del formato, en los desplazamientos 18 y 19, valen 2 cuando la base de datos está en modo WAL — que Room activa por defecto.
Room añade huellas que te sirven para confirmar que tienes el archivo correcto. Crea una tabla android_metadata con una única fila de configuración regional (por ejemplo en_US) — esa tabla la escribe la clase SQLiteDatabase de Android para toda base de datos de app, use Room o no — y una room_master_table con las columnas id e identity_hash, donde Room guarda el hash del esquema en la fila fija id = 42. Tus tablas de entidad (anotadas con @Entity) y sus índices están junto a ellas, todas listadas en la tabla de esquema sqlite_master (con el alias sqlite_schema en las versiones nuevas de SQLite). Una app con SQLiteOpenHelper puro se salta room_master_table, pero por lo demás es el mismo archivo SQLite estándar.
Cómo se vuelve 'malformed' el archivo — y los errores exactos
Una base de datos SQLite es una matriz plana de páginas de tamaño fijo (4096 bytes por defecto). La página 1 lleva la cabecera y el esquema; cada tabla e índice es un árbol-B cuyas páginas interiores contienen punteros de celda que bajan hasta las páginas hoja, y las hojas guardan tus registros reales. El motor lee la cabecera, encuentra el esquema en sqlite_master y luego sigue esos punteros hasta las filas.
Los punteros son el punto débil. Cuando el proceso de la app lo mata el low-memory killer de Android a mitad de una escritura, el teléfono se queda sin batería, el almacenamiento se llena, o dos procesos abren el mismo archivo sin un bloqueo correcto, una sola página interior o un campo de la cabecera puede quedar incoherente. SQLite se detiene entonces en el primer problema y rechaza todo el archivo con database disk image is malformed — código de resultado 11 (SQLITE_CORRUPT) — aunque las páginas hoja que hay tras la rotura sigan conteniendo registros intactos y autodescriptivos. Un fallo emparentado, file is not a database — código de resultado 26 (SQLITE_NOTADB) — significa que la cadena mágica de 16 bytes de la cabecera es incorrecta: o bien esos bytes se dañaron (las páginas que hay detrás suelen estar bien y son recuperables) o el archivo es realmente otra cosa, como una base de datos cifrada con SQLCipher que se lee como ruido de alta entropía.
Una trampa específica de Android vuelve esto urgente: el DefaultDatabaseErrorHandler.onCorruption() del framework borra el archivo de la base de datos cuando detecta corrupción al abrir. Así que una app que topó con el error "malformed" puede haber eliminado ya el archivo en su siguiente arranque. Copia la base de datos fuera del dispositivo — y trabaja sobre esa copia — antes de volver a abrir la app.
Los archivos laterales -wal y -shm: extrae los tres
Room activa el registro de escritura anticipada por defecto (enableWriteAheadLogging()), lo que significa que los cambios confirmados recientes se guardan de forma provisional en el archivo hermano <nombre>.db-wal hasta que un checkpoint los vuelca de nuevo en el .db principal. Tras un fallo, tus filas más recientes pueden vivir solo en ese WAL. Extrae el archivo principal sin él y a la base de datos recuperada le faltará todo lo escrito desde el último checkpoint — un motivo habitual por el que la gente cree que "se ha perdido la mitad de mis datos".
El WAL no es la base de datos principal: empieza con una cabecera de 32 bytes cuyo número mágico es 0x377f0682 o 0x377f0683 (el bit bajo elige sumas de comprobación de trama big-endian o little-endian), seguida de tramas formadas por una cabecera de trama de 24 bytes más una imagen de página cada una. El archivo -shm es solo un índice en memoria compartida hacia el WAL; se regenera automáticamente y es seguro dejarlo atrás. Así que extrae juntos el .db principal y el -wal: suelta arriba la base de datos y luego añade el -wal en la ranura lateral opcional; sus tramas confirmadas se superponen antes del escaneo, de modo que la recuperación refleja la última transacción confirmada en lugar del último checkpoint.
Sacar la base de datos del dispositivo de forma segura
En una compilación depurable puedes acceder a la carpeta privada con run-as: por ejemplo adb exec-out run-as <paquete> tar c ./databases | tar xv, o copiar los archivos al almacenamiento compartido con run-as <paquete> cp databases/<nombre>.db /sdcard/ y luego adb pull. Las apps de release (no depurables) rechazan run-as, así que necesitas un dispositivo con root o la propia función de exportación de la app. La antigua vía adb backup -f backup.ab <paquete> quedó obsoleta en Android 12 y respeta android:allowBackup, por lo que no es fiable para esto. Sea cual sea el camino, coge <nombre>.db, <nombre>.db-wal y <nombre>.db-shm de una vez para que el WAL coincida con el archivo principal.
Con la copia en tu equipo, la comprobación clásica es sqlite3 name.db "PRAGMA integrity_check;" (o el más rápido quick_check), y el arreglo clásico es el propio comando .recover de SQLite, que lee cada fila descodificable y la escribe en un archivo nuevo. Esta página hace exactamente eso en el navegador: recorre las páginas, reconstruye los árboles-B, recurre a un barrido de páginas en bruto para las filas que la cadena de punteros ya no alcanza, y coloca todo lo que no puede atribuir a una tabla conocida en una tabla lost_and_found en vez de descartarlo — así no se tira nada y no se sube nada.
Qué puede y qué no puede reparar
Puede reparar
- "database disk image is malformed (code 11 SQLITE_CORRUPT)" por una página o un puntero de celda de árbol-B dañados, con páginas hoja intactas detrás
- Una cabecera errónea o dañada (campo de tamaño o de recuento de páginas incorrecto) cuando las páginas posteriores siguen siendo legibles
- Filas en páginas que la cadena de punteros ya no alcanza, extraídas por un escaneo de páginas en bruto hacia una tabla lost_and_found
- Filas sin checkpoint de un archivo lateral
.db-wal suministrado (los datos más recientes tras un fallo) - Bases de datos de Room y SQLiteOpenHelper copiadas del dispositivo antes de que el gestor de errores de la app las borrara
No puede reparar
- Filas sobrescritas físicamente o truncadas del final del archivo — esos bytes ya no existen
- Bases de datos cifradas con SQLCipher (Signal, o apps con Room que usan un SupportFactory) sin la frase de contraseña — las páginas son texto cifrado
- Una base de datos que DefaultDatabaseErrorHandler ya borró — eso es antes un problema de recuperación de datos del dispositivo
- Las afinidades exactas de columna: los valores vuelven como su clase de almacenamiento en disco (NULL/INTEGER/REAL/TEXT/BLOB)
- Un archivo .db de 0 bytes — no hay nada en disco que descodificar
Si una reparación falla, te decimos por qué (datos que faltan frente a estructura dañada), y nunca se te cobra por una reparación fallida.