Recupera la base de datos SQLite corrupta de una app Android

Las apps de Android guardan su estado en SQLite, así que un cierre inesperado al arrancar suele ser una sola página o un puntero rotos en un archivo .db — no datos perdidos. Room y SQLiteOpenHelper escriben una base de datos SQLite 3 estándar, y las filas casi siempre siguen en sus páginas. Reconstruirlas a partir de una copia que extrajiste del dispositivo no implica jamás entregar esa base de datos a un servidor.

Tu archivo nunca sale de tu dispositivo — la reparación ocurre en tu navegador. 0 bytes subidos.

Your data is the big block — usually intact. What breaks is the small index. Repair rebuilds it.

Suelta un archivo aquí o exploraToca para elegir un archivo

ZIP · Office · PDF · vídeo · JPG · PNG · RAR/7z · SQLite — reparados aquí mismo, en tu navegador. No se sube nada

0 bytes subidosGratis: 3 reparaciones/día — hasta 500 MB de vídeo, 100 MB de documentos y archivos, 50 MB de fotos. Descarga gratuita. Una reparación 5,90 €. Sin cuenta.

Repáralo ahora

  1. Suelta el archivo
  2. La reparación ocurre en local
  3. 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.

Preguntas frecuentes

Mi app borró su propia base de datos tras el fallo, ¿por qué?

El DefaultDatabaseErrorHandler.onCorruption() de Android borra el archivo cuando detecta SQLITE_CORRUPT al abrir, así que un segundo arranque puede eliminarlo. Por eso la solución es copiar primero el .db (y su -wal) fuera del dispositivo con adb run-as y recuperar desde la copia — nunca desde el archivo en vivo que la app sigue abriendo.

Extraje el .db pero faltan datos recientes. ¿Qué ha pasado?

Room funciona en modo WAL por defecto, así que las filas confirmadas más recientes están en <nombre>.db-wal, todavía sin volcar al archivo principal. Extrae los tres archivos — .db, -wal y -shm — y añade el -wal en la ranura lateral opcional; sus tramas confirmadas se superponen antes del escaneo para que se incluya la última transacción.

¿Funciona con una base de datos de Room o solo con SQLite en bruto?

Con ambas — por debajo Room es SQLite normal. La recuperación reconstruye android_metadata, room_master_table (incluido el identity_hash guardado en id = 42) y tus tablas @Entity. Si el hash del esquema llega intacto, Room vuelve a abrir el archivo reconstruido sin quejarse de que no puede verificar la integridad de los datos.

En mi caso el error dice 'file is not a database'. ¿Es recuperable?

A veces. Eso es SQLITE_NOTADB (código 26): la cadena mágica de 16 bytes de la cabecera es incorrecta. Si solo se dañaron los bytes de la cabecera, las páginas de detrás suelen estar intactas y son recuperables. Si todo el archivo es ruido de alta entropía, es una base de datos cifrada (SQLCipher) o no es una base de datos en absoluto — eso no se puede recuperar sin la clave. El diagnóstico distingue ambos casos.

¿Se sube mi base de datos para repararla?

No. La base de datos de una app es el archivo que menos querrías en el servidor de otra persona — puede contener cada registro de usuario, mensaje y token que la app guardó. Aquí el .db se lee desde tu disco y se reconstruye en tu navegador; puedes observar la pestaña Red y confirmar que salen 0 bytes.

Relacionado: Repara una base de datos SQLite (cualquier origen) · Repara un vídeo de Android · Repara un libro de Excel · Verifica la promesa de cero subidas