今すぐ修復
- ファイルをドロップ
- 修復はローカルで実行
- 結果をダウンロード
Androidの2つの永続化レイヤーはどちらもごく普通のSQLiteを書き込みます。Roomは古くからあるSQLiteOpenHelperと同じエンジンの上に自身のスキーマを生成し、どちらも/data/data/<package>/databases/<name>.dbに単一のファイルを作ります。このファイルが破損すると、Roomはandroid.database.sqlite.SQLiteDatabaseCorruptException: database disk image is malformed (code 11 SQLITE_CORRUPT)を投げ、アプリは強制終了するか空の画面を表示します。取り出した.dbを上にドロップし — 手元にあれば-walサイドカーをオプションの枠に追加してください — すると、このツールがページを走査してスキーマを再構築し、まだデコードできる行をすべて新しいデータベースに挿入し直します。すべてブラウザのタブ内で完結します。ファイルがアップロードされることは一切ありません。
Androidアプリがデータベースを保存する場所 — そして中身は何か
どのAndroid端末でも、データベースはアプリ専用のサンドボックス内の/data/data/<package>/databases/に置かれ、通常は3つのファイルで構成されます。本体の<name>.db、ライトアヘッドログの<name>.db-wal、そして共有メモリのインデックスである<name>.db-shmです。本体ファイルは16バイトのマジック文字列"SQLite format 3\000"で始まります。2バイトのページサイズはバイトオフセット16に置かれ、フォーマットの読み取り/書き込みバージョンを示すバイトはオフセット18と19にあって、データベースがWALモードのときは2になります — RoomはこのWALモードを既定で有効にします。
Roomは、正しいファイルを持っているか確認するのに使える手がかりを付け加えます。1行だけロケール情報(たとえばen_US)を持つandroid_metadataテーブルを作成し — このテーブルはRoomかどうかにかかわらず、あらゆるアプリのDBに対してAndroidのSQLiteDatabaseが書き込みます — さらにidとidentity_hashという列を持つroom_master_tableを作り、Roomはスキーマのハッシュを固定行id = 42に保存します。あなたの(@Entityを付けた)エンティティテーブルとそのインデックスもその隣に並び、いずれもスキーマテーブルsqlite_master(新しいSQLiteではsqlite_schemaという別名)に一覧されます。素のSQLiteOpenHelperを使うアプリはroom_master_tableを持ちませんが、それ以外は同じ標準的なSQLiteファイルです。
ファイルが 'malformed' になる仕組み — そして正確なエラー
SQLiteデータベースは、固定サイズのページ(既定では4096バイト)が並んだ平坦な配列です。ページ1はヘッダーとスキーマを収め、各テーブルとインデックスはB木で、その内部ページはリーフページまで下るセルポインタを保持し、リーフが実際のレコードを保持します。エンジンはヘッダーを読み、sqlite_masterの中にスキーマを見つけ、それらのポインタをたどって行にたどり着きます。
弱点はこのポインタです。アプリのプロセスが書き込みの途中でAndroidのlow-memory killerに殺されたり、端末の電源が落ちたり、ストレージが一杯になったり、2つのプロセスが正しいロックなしに同じファイルを開いたりすると、たった1つの内部ページやヘッダーのフィールドが矛盾した状態のまま残ることがあります。するとSQLiteは最初の問題で止まり、ファイル全体をdatabase disk image is malformed — 結果コード11 (SQLITE_CORRUPT) — として拒否します。破損箇所の先にあるリーフページには、無傷で自己記述的なレコードがまだ残っているにもかかわらずです。これと近い障害であるfile is not a database — 結果コード26 (SQLITE_NOTADB) — は、16バイトのヘッダーのマジックが正しくないことを意味します。ヘッダーのバイトだけが壊れた(その先のページは無事で復元可能なことが多い)か、あるいはファイルが本当に別物 — たとえば高エントロピーのノイズに見えるSQLCipherで暗号化されたデータベース — であるかのどちらかです。
Android特有のある落とし穴が、この作業を急ぐ理由になります。フレームワークのDefaultDatabaseErrorHandler.onCorruption()は、オープン時に破損を検出するとデータベースファイルを削除します。つまり "malformed" エラーに遭遇したアプリは、次回の起動時にすでにファイルを消してしまっているかもしれません。アプリをもう一度開く前に、データベースを端末の外にコピーし — そのコピーの上で作業してください。
-wal と -shm のサイドカー: 3つとも取り出す
Roomは既定でライトアヘッドロギングを有効にします(enableWriteAheadLogging())。つまり、最近コミットされた変更は、チェックポイントが本体の.dbに畳み込むまでの間、兄弟ファイルの<name>.db-walに一時的に置かれます。クラッシュの後では、あなたの最新の行がそのWALの中だけに存在していることがあります。本体ファイルだけをそれなしで取り出すと、復元したデータベースには最後のチェックポイント以降に書かれたものがすべて欠けてしまいます — 「データが半分消えた」と人が思い込む、よくある原因です。
WALは本体のデータベースではありません。32バイトのヘッダーで始まり、そのマジックナンバーは0x377f0682または0x377f0683です(下位ビットがフレームのチェックサムをビッグエンディアンにするかリトルエンディアンにするかを選びます)。続いてフレームが並び、各フレームは24バイトのフレームヘッダーと1枚のページイメージからなります。-shmファイルはWALへの共有メモリのインデックスにすぎず、自動的に再生成されるので置いていっても安全です。ですから本体の.dbと-walは一緒に取り出してください。データベースを上にドロップし、次に-walをオプションのサイドカー枠に追加すると、コミット済みのフレームが走査の前に重ね合わされ、復元は最後のチェックポイントではなく最後にコミットされたトランザクションを反映します。
データベースを端末から安全に取り出す
デバッグ可能な(debuggable)ビルドなら、run-asでプライベートなフォルダにアクセスできます。たとえばadb exec-out run-as <package> tar c ./databases | tar xv、あるいはrun-as <package> cp databases/<name>.db /sdcard/でファイルを共有ストレージにコピーしてからadb pullする方法です。リリース(デバッグ不可)ビルドのアプリはrun-asを拒否するので、root化した端末かアプリ自身のエクスポート機能が必要になります。古いadb backup -f backup.ab <package>という経路はAndroid 12で非推奨になり、しかもandroid:allowBackupを尊重するため、この用途では当てになりません。どの経路をとるにせよ、WALが本体ファイルと一致するように、<name>.db、<name>.db-wal、<name>.db-shmを一度にまとめて取得してください。
コピーが手元のマシンに来たら、定番の確認はsqlite3 name.db "PRAGMA integrity_check;"(またはより速いquick_check)で、定番の修復はSQLite自身の.recoverコマンドです。これはデコードできる行をすべて読み出し、まっさらな新しいファイルに書き出します。このページはまさにそれをブラウザの中で行います。ページを走査し、B木を再構築し、ポインタの連鎖がもう届かない行のためには生のページスキャンにフォールバックし、既知のテーブルに帰属させられないものは捨てるのではなくlost_and_foundテーブルに入れます — こうして何も捨てられず、何もアップロードされません。
修復できるものとできないもの
修復できる
- "database disk image is malformed (code 11 SQLITE_CORRUPT)" — 破損したページやB木のセルポインタが原因で、その先のリーフページは無傷な場合
- 誤った、または壊れたヘッダー(ページサイズやページ数のフィールドが不正)で、その後のページはまだ読める場合
- ポインタの連鎖がもう届かないページ上の行を、生のページスキャンでlost_and_foundテーブルに取り出す
- 提供された
.db-walサイドカーにある未チェックポイントの行(クラッシュ後の最新データ) - アプリのエラーハンドラが削除する前に端末から取り出した、RoomやSQLiteOpenHelperのデータベース
修復できない
- 物理的に上書きされた行や、ファイル末尾から切り詰められた行 — それらのバイトはもう存在しません
- パスフレーズのないSQLCipher暗号化データベース(Signal、またはSupportFactoryを使うRoomアプリ) — ページは暗号文です
- DefaultDatabaseErrorHandlerがすでに削除してしまったデータベース — それはまず端末側のデータ復旧の問題です
- 正確な列アフィニティ: 値はディスク上のストレージクラス(NULL/INTEGER/REAL/TEXT/BLOB)のまま戻ります
- 0バイトの.dbファイル — ディスク上にデコードすべきものが何もありません
修復が失敗した場合、その理由(データの欠落か構造の破損か)をお伝えします。失敗した修復に料金が請求されることはありません。