How do you detect a damaged database or file, and what do you do about it?
Predict first
What does integrity_check report before and after damage?
import Foundation
import CryptoKit
import SQLite3
let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let path = dir.appendingPathComponent("store.db").path
var h: OpaquePointer?
sqlite3_open(path, &h)
sqlite3_exec(h, "CREATE TABLE t(id INTEGER PRIMARY KEY, v TEXT); CREATE INDEX i ON t(v);", nil, nil, nil)
for i in 1...2000 { sqlite3_exec(h, "INSERT INTO t(v) VALUES('value-\(i)')", nil, nil, nil) }
sqlite3_close(h)
func check(_ path: String) -> String {
var db: OpaquePointer?
guard sqlite3_open(path, &db) == SQLITE_OK else { return "open failed" }
defer { sqlite3_close(db) }
var st: OpaquePointer?
guard sqlite3_prepare_v2(db, "PRAGMA integrity_check", -1, &st, nil) == SQLITE_OK else { return "unreadable" }
defer { sqlite3_finalize(st) }
return sqlite3_step(st) == SQLITE_ROW ? String(cString: sqlite3_column_text(st, 0)) : "no rows"
}
print("healthy:", check(path))
var data = try Data(contentsOf: URL(fileURLWithPath: path))
let before = SHA256.hash(data: data)
for i in 0..<64 { data[8192 + i * 3] ^= 0xFF }
try data.write(to: URL(fileURLWithPath: path))
print("checksum changed:", before != SHA256.hash(data: data))
print("integrity_check after damage reports ok:", check(path) == "ok")
- healthy: ok / checksum changed: true / integrity_check after damage reports ok: false
- healthy: ok / … reports ok: true
- healthy: not ok
Mental model. A tamper seal on a medicine bottle: you cannot prevent every accident, but you can tell at once when something is wrong, and you keep a spare bottle for when it is.
Corruption is rare but real (a full disk, a bad write, a failing storage controller, a copy of a WAL database without its log, a bug). Detect it at three levels. For SQLite, PRAGMA integrity_check verifies the structure of every table and index and returns a single row “ok” for a healthy database; in the check it returns “ok” first, and after flipping bytes inside a data page it returns error descriptions (“*** in database main *** Tree … page …”) instead. It reads the whole file, so run it on demand (after a crash report pointing at the store, in a diagnostic tool, or during a migration), not on every launch; PRAGMA quick_check is cheaper and skips some checks. For your own files (downloads, exported documents, caches), store a checksum (SHA-256) next to the data and compare after reading: the check shows the digest changes when the content changes. For structured files include a version and a length so truncation is detectable.
The response matters more than the detection. Keep a last known good copy: take a backup with SQLite’s online backup API (or VACUUM INTO) before risky migrations, and keep the previous file until the new one verifies. On failure, do not crash-loop: quarantine the bad file (rename it, keep it for diagnostics), try to recover what you can (.recover in the SQLite shell, or re-reading rows that still parse), then rebuild from the server or an iCloud copy and tell the user honestly what was lost. Prevent the common causes: atomic replacement for files (Q339), a single process writing the database, never copying a WAL database as one file, enough free space checks before big writes, and fsync-level durability only where you need it (PRAGMA synchronous). Record corruption events with a counter in analytics: a spike usually points at a bug or a device cohort.
A damaged page makes integrity_check return error rows, and the file checksum changes.
Common traps
- Running
integrity_checkon every launch. It reads the entire database; use it on demand. - Deleting a damaged store immediately. Quarantine it first; you may be able to recover data and you will need it to diagnose the cause.
They might follow up with
- How do I make a safe copy of a live SQLite database? The backup API or
VACUUM INTO, not a file copy of an open WAL database. - What does
PRAGMA synchronoustrade? Durability against speed;FULLsurvives power loss,NORMALwith WAL is a common balance.
Related questions
- 13.8.2 What are the FileManager basics for saving a document: creating directories, replacing a file, and the…
- 13.11.1 Which OSStatus values matter when you use SecItem functions, and how do you write a correct save (upsert) of…
- 13.2.1 How do SQLite transactions, journal modes (WAL) and constraints work, and which defaults surprise people?
- 13.6.1 How do you protect data from crashes mid-write, corruption and backups: atomic writes, journaling, exclusion…
← Which OSStatus values matter when you use SecItem functions, and how… Implement an LRU cache with O(1) get and set: which data structures… →