PRAGMA integrity_check said ok. There are no tables in the file.
Six files, one machine, SQLite 3.45.1. PRAGMA integrity_check answered ok to a
0-byte file, to a 4 KB copy of a database whose 500 rows had all been committed into a
-wal that was never copied, and to a file with one byte changed inside a text column. It answered
database disk image is malformed to exactly one of the six: the file that was simply truncated.
“
PRAGMA integrity_checkanswers one question: are the pages in this file consistent with each other. It returnsokfor a 0-byte file, and for a backup whose every table lives in a-walyou left behind. It cannot tell you that the file is not the database.”Measured on SQLite 3.45.1, 2026-11-09. Script to reproduce all six files below.
What it said, and what was actually in the file
| File | PRAGMA integrity_check | What the file really held |
|---|---|---|
0 bytes — cp of a file that had not been created yet |
ok |
0 tables. select count(*) from sqlite_master → 0 |
4,096 B — cp live.db backup.db while a WAL-mode app was running, 500 rows committed |
ok |
sqlite_master → []; select count(*) from orders →
no such table: orders |
| 8,192 B — same copy, but one commit had already been checkpointed: the live database held 101 rows | ok |
select count(*) from users → 100. The last commit is gone and nothing anywhere
reports an error |
| 651,264 B — a 160-page database with its last page cut off | database disk image is malformed |
caught. This is the only one of the six the pragma gets right |
| 4,096 B of zeros | file is not a database |
caught by the open itself, before the pragma runs |
| 655,360 B — one byte changed inside a 200-byte text cell (offset 20,700) | ok |
3,000 rows still read; one row's text is now different from the original, and no query notices |
Why ok is the honest answer
A SQLite file states its own size in three places: the 4-byte page_count field at offset 28, the
length of the file, and — when a -wal exists — the database size written by the last
commit frame. integrity_check verifies that the pages inside the file agree with each other. A
0-byte file has no pages to disagree with, so it is consistent. A one-page stub whose tables were committed into
a -wal is a correct one-page file; every byte in it is valid. Both are green.
This is why the standard advice — open the backup, run PRAGMA integrity_check, trust the
answer — passes exactly the files you need to worry about. The failure is not that the pragma is wrong. It
is that it was asked the wrong question: is this file self-consistent, not is this file the database.
The two questions have different answers whenever a -wal is involved, whenever the copy is a file
that was never written, and whenever the damage is inside a cell rather than in the page structure.
The check that answers the other question, without opening the file
Four numbers decide it, and all four come out of the first 100 bytes plus two stat calls:
import os, struct, sys
p = sys.argv[1]
h = open(p, "rb").read(100)
if len(h) < 100 or h[:16] != b"SQLite format 3\x00":
sys.exit("%s: no SQLite header (%d bytes)" % (p, os.path.getsize(p)))
ps = struct.unpack(">H", h[16:18])[0] or 65536
declared = struct.unpack(">I", h[28:32])[0]
actual = (os.path.getsize(p) + ps - 1) // ps
print("%s: page_size=%d journal=%s declared_pages=%d file_pages=%d wal=%d B"
% (p, ps, "WAL" if h[18:20] == b"\x02\x02" else "rollback",
declared, actual, os.path.getsize(p + "-wal") if os.path.exists(p + "-wal") else 0))
Run on the six files above, it prints:
empty.db: no SQLite header (0 bytes) a.db: page_size=4096 journal=WAL declared_pages=1 file_pages=1 wal=0 B b.db: page_size=4096 journal=WAL declared_pages=2 file_pages=2 wal=0 B trunc.db: page_size=4096 journal=rollback declared_pages=160 file_pages=159 wal=0 B zeros.db: no SQLite header (4096 bytes) clean.db: page_size=4096 journal=rollback declared_pages=160 file_pages=160 wal=0 B
Read it in this order: no header → the copy captured nothing, or captured the wrong file. journal=WAL
with wal=0 B and a small file_pages → the rows are in a -wal that is
not here. declared_pages greater than file_pages → truncation, the one case the
pragma also catches. All three numbers agreeing on a file you copied by hand still only means internally
consistent: it does not mean it is the copy you wanted.
Reproduce all six files
python3 - <<'EOF'
import shutil, sqlite3, os
os.makedirs("/tmp/six", exist_ok=True); os.chdir("/tmp/six")
def ro(p, sql):
c = sqlite3.connect("file:%s?mode=ro" % p, uri=True)
try: return c.execute(sql).fetchall()
except Exception as e: return "%s: %s" % (type(e).__name__, e)
finally: c.close()
# 1) 0-byte file
open("empty.db", "wb").close()
print("empty.db", ro("empty.db", "PRAGMA integrity_check"))
# 2) all 500 rows committed into the -wal, only the .db copied
c = sqlite3.connect("live.db"); c.execute("PRAGMA journal_mode=WAL")
c.execute("CREATE TABLE orders(id INTEGER PRIMARY KEY, amount INT)")
c.executemany("INSERT INTO orders(amount) VALUES(?)", [(i*7,) for i in range(500)])
c.commit(); shutil.copyfile("live.db", "stub.db")
print("stub.db", os.path.getsize("stub.db"), "B wal", os.path.getsize("live.db-wal"),
ro("stub.db", "PRAGMA integrity_check"), ro("stub.db", "select count(*) from orders"))
c.close()
# 3) truncated: 160 pages cut to 159, header still says 160
c = sqlite3.connect("big.db"); c.execute("PRAGMA journal_mode=DELETE")
c.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, payload TEXT)")
c.executemany("INSERT INTO t(payload) VALUES(?)", [("x"*200,) for _ in range(3000)]); c.commit(); c.close()
shutil.copyfile("big.db", "cut.db")
open("cut.db", "r+b").truncate(os.path.getsize("big.db") - 4096)
print("cut.db", os.path.getsize("cut.db"), "B", ro("cut.db", "PRAGMA integrity_check"))
EOF
What this checker does, and does not, add
The same six files, run through the checker linked at the bottom of this page (subprocess, JSON on stdout):
empty.db 0 B verdict=critical NOT_SQLITE a.db 4,096 B verdict=critical WAL_MODE_BUT_NO_WAL b.db 8,192 B verdict=critical WAL_MODE_BUT_NO_WAL trunc.db 651,264 B verdict=critical HEADER_LONGER_THAN_FILE zeros.db 4,096 B verdict=critical NOT_SQLITE bitflip.db 655,360 B verdict=ok (nothing found)
Honest limits, because the interesting part is where the checks disagree:
- It separates the five cases the pragma blurs — a file with no header, a WAL stub, a stale
.db/-walpair, a truncated file — into different verdicts instead of one message about a disk image. - It does not catch the sixth case. A single changed byte inside a cell leaves the page structure
intact; this checker, like
PRAGMA integrity_check, reportsok. It counts pages and walks the freelist; it does not verify cell contents against anything. - It reports
MAIN_DB_STALEwhen the newest commit sits in the-walyou attached, which is the one judgement that saves the half of the database you were about to discard.
Check one .db file (max 20 MB; add the -wal if you have it)
Read-only byte inspection: page counts, WAL commit, freelist residue. The file is deleted before the answer is written, and nothing about its content is logged.