今すぐ修復
- ファイルをドロップ
- 修復はローカルで実行
- 結果をダウンロード
SQLite が — あるいは SQLite の上に構築されたあらゆるもの、つまりブラウザの履歴ストア、スマートフォンのバックアップ、メッセージングアプリのローカルデータベース、デスクトップアプリの設定などが — "database disk image is malformed"(データベースのディスクイメージが不正な形式です)と報告するとき、それは SQLITE_CORRUPT(結果コード 11)に行き当たっています。あなたのテーブルにインデックスを張る b-tree をたどっていて、型バイト・セルポインタ・レコードヘッダーのいずれかが形式と矛盾するページに着地したため、ゴミを返す代わりに停止したのです。上に .db、.sqlite、.sqlite3 のファイルをドロップすると、ツールはそれをブラウザ内でページ単位に読み取り、生き残っている b-tree 構造をたどり、残りをポインタなしのスキャンで洗い出し、デコードできるすべての行を、開くことのできる真新しいデータベースへ書き込みます — ファイルがあなたの端末から一切出ることなく。
ページレベルで「malformed」が意味すること
SQLite のデータベースは、固定サイズのページに切り分けられた 1 つのファイルです — ページサイズは 512 から 65536 バイトの間の 2 のべき乗で、ヘッダーのオフセット 16 に 16 ビット値として格納されます(ディスク上の値 1 は、仕様における 65536 の代わりの表現です)。どのファイルも 16 バイトのマジック文字列 SQLite format 3\0 で始まり、続く 100 バイトのヘッダーが、ページサイズ、オフセット 56 のテキストエンコーディング(1 = UTF-8、2 = UTF-16le、3 = UTF-16be)、オフセット 28 のヘッダー内ページ数、そしてフリーリスト(freelist)を記録します。ページサイズを読み違えると、最初のページ以降のすべてのページが誤ったオフセットに落ちます — これは、1 行も触れられていないのにファイルが「malformed(不正な形式)」になる原因の 1 つです。
その内部では、データは b-tree の中にあります。各ページは型バイトで始まります。0x0d はテーブルのリーフ(実際の行を保持します)、0x05 はテーブルの内部ページ(子ページへのポインタだけを保持します)、そして 0x0a/0x02 はインデックス側のそれに相当します。ページ 1 は特別です — その b-tree ヘッダーは 100 バイトのデータベースヘッダーの後ろに置かれ、sqlite_master のルートになります。これは行が (type, name, tbl_name, rootpage, sql) であるスキーマテーブルです。あなたのテーブルの 1 つを読むために、SQLite はそこでその rootpage を引き、次に内部ページをリーフまで下向きにたどり、各セルから可変長整数(varint)のペイロード長、varint の rowid、レコード本体を読み取ります。
クエリはこのツリーをたどるため、たった 1 つの壊れたリンクが、その下にあるすべてを汚染します。ゼロで埋められた内部ページ、ページの末尾を越えて指すセルポインタ、ループして戻る最右ポインタ、あるいはシリアル型のつじつまが合わないレコードヘッダー — これらのいずれかがあると、SQLite は損傷を踏んだ瞬間に「database disk image is malformed」を発します。健全なリーフページ上の行は手つかずです。SQLite は単に、壊れたインデックスを通してそこに到達できないだけです。PRAGMA integrity_check も同じ種類の障害を報告します。
生のページ走査がどのようにあなたの行を救い出すのか
この解決策は、SQLite 自身の .recover シェルコマンドが行うことをそのままなぞりますが、すべてブラウザ内で実行されます。まずツールはページ 1 から sqlite_master を読み、各テーブルの名前、列(その CREATE TABLE の SQL から解析します)、そして rootpage を把握し、次にそのルートから各 b-tree をたどります — ポインタが無事なときの、速く正確な経路です。走査が欠落した、あるいは無効なページに行き当たると、例外を投げるのではなく「不完全」へと段階的に降格し、その破綻までに集めたすべての行を保持します。
続いて、単なる再オープンを上回る部分がやってきます — ポインタなしの掃引(スイープ)です。スキャナはすべてのポインタを無視し、ファイル内のあらゆるテーブルリーフページ(型 0x0d)をレコードの袋として扱い、各セルをそのバイトから直接デコードします — varint のヘッダー長、シリアル型の配列、そして各値です(シリアル型 0 は NULL、8/9 はリテラルの 0 と 1、7 は float64、12 を超える偶数/奇数のコードは BLOB/TEXT です)。大きな値のオーバーフロー(overflow)連鎖は、ページからページへとたどられます。ゼロで埋められた内部ページ、フリーリストに載ったページ、あるいは削除されたテーブルがあっても行が生き残るのは、まさにこの仕組みのおかげです。何もそれを指していなくても、リーフは依然としてデータを保持しています。列数が既知のテーブルに一意に一致する行は、そのテーブルへ戻して収められます。本当に帰属先を特定できない行は、pgno、cellidx、nfield、rowid、c0…cN の列を持つ lost_and_found テーブルに書き込まれます。こうして、デコードできるものは何一つ捨てられません。
最後にツールは、sqlite3.wasm(/engines/sqlite/ から同一オリジンで読み込まれます)を使って新しいデータベースを構築し、各テーブルに対して寛容な CREATE TABLE を発行します — 元の列名と、すべての INTEGER PRIMARY KEY は保持され、rowid のエイリアスが往復して保たれますが、復活させた行を弾いてしまう NOT NULL/UNIQUE/CHECK/外部キーの制約は付けません — そして復元した各行を INSERT OR IGNORE し、結果をきれいな .db としてエクスポートします。兄弟の -wal(先行書き込みログ)ファイルが提供されれば、そのコミット済みフレームが先に重ね合わされ、復元は最後にコミットされたトランザクションを反映します。
データが本当に途切れる場所 — そして、なぜ何もアップロードされないのか
復元は、存在するものには届きますが、ディスクがもはや保持していないものを作り出すことはできません。ファイルが切り詰められた場合 — 途中で止まったコピーや同期が、ページサイズの正確な倍数ではないサイズを残した場合 — 切断点より後ろのすべてのページは物理的に失われており、あなたが得られるのは、その前に完全に書き込まれた行です。無事なページ上にありながら、レコードヘッダーがそれ自体で矛盾しているセル(典型的なビット腐敗(bit-rot))は、推測されるのではなく、破棄されて数え上げられます。暗号化されたデータベース(SQLCipher や SEE 拡張)には平文のマジックがありません — その最初のページは暗号文です — そのため、鍵がなければたどるものが何もなく、復元は試みられません。
SQLite 自身が言い張る、2 つの正直な注意点があります。削除された行が再び現れることがあります。ポインタなしのスキャンはフリーリストや未 vacuum のページを読むため、消えたと思っていたレコードが出力に再浮上することがあります。また、復元された値は、宣言された列のアフィニティ(affinity)ではなく、ディスク上のストレージクラスを反映します — 列の見かけ上の型が変わることがあります。SQLite 自身の指針は率直です。破損したデータベースから引き出したデータは常に疑わしい、と。頼りにする前に、信頼できる情報源と突き合わせて検証してください。
これらのすべては、あなたのブラウザのタブ内で実行されます — スキャナは依存関係のない TypeScript で、書き込み側は WASM なので、サーバーとのやり取りは一切ありません。これは、データベースがパスワードの保管庫、チャット履歴、健康データ、あるいはアプリの非公開ストアであるときに重要です。Network タブを開けば、そのファイルの 0 バイトしかあなたのマシンから出ていないことを確認できます。
修復できるものとできないもの
修復できる
- 損傷した b-tree のページやポインタが原因で SQLITE_CORRUPT /「database disk image is malformed」を発するデータベース
- 壊れた内部ページや不正な rootpage によって到達不能になった、健全なテーブルリーフページ上の行
- ゼロで埋められた内部ページ、フリーリストに載ったページ、削除されたテーブル上の行 — ポインタなしのリーフスキャンで復元
- オーバーフローページの連鎖にあふれ出た大きな値 — 走査中に再組み立て
- 兄弟の -wal ファイルにあるチェックポイント未処理の変更 — スキャン前に重ね合わせ
修復できない
- 切り詰められたファイルの切断点より後ろの行 — それらのページは物理的に失われています
- レコードヘッダーが破損しすぎてデコードできないセル(bit-rot)— 推測されず、破棄されます
- 鍵のない暗号化された SQLCipher/SEE データベース(ページが暗号文です)
- 元の UNIQUE/CHECK/外部キーの保証 — 復元は制約を緩めて、復活させた行が弾かれないようにします
- 0 バイトのファイル、あるいは SQLite のマジックもデコード可能なリーフページも持たないファイル
修復が失敗した場合、その理由(データの欠落か構造の破損か)をお伝えします。失敗した修復に料金が請求されることはありません。