Your SQLite backup is empty because the rows were in the -wal file.
A 4 KB “backup” of a database with 440 rows in it is not a corrupted backup. It is a
correct copy of the one page that was in the main file at that instant, with every committed row
still sitting in the -wal you left behind.
What the numbers look like
backup_naive.db (4.0 KB) page_size=4096 journal_mode=WAL header_pages=1 file_pages=1 tail=0 pages freelist=0 change_counter=1 valid_for=1 !! WAL_MODE_BUT_NO_WAL: header says journal_mode=WAL, there is no -wal file, and the main file holds only 1 page(s): if you expect tables in here, they are not in this file
The same file, opened by sqlite 3.45.1:
sqlite_master : [] <- not one table select count(*) from users : OperationalError: no such table: users
And the pair taken properly — the .db still says 1 page, the data is in the
-wal:
crash.db (4.0 KB) -wal=61832 bytes frames=15 valid=15 commit_pages=13 !! MAIN_DB_STALE: the newest commit lives in the -wal (the commit sets db size to 13), but this .db file still says 1 page — copying only the .db loses those commits sqlite: select count(*) from users -> 500
Why “the backup verified” is not enough
PRAGMA integrity_check on the 4 KB stub returns ok. It is checking
whether the file is internally consistent, not whether it is the database you asked for. Every
verification step that opens the file with sqlite3 will agree that a one-page empty database is a
valid one-page empty database. Measured across six
files here: the pragma said ok to a 0-byte file too.
The right way to take the copy
# online backup API — consistent, single file, no -wal needed afterwards sqlite3 app.db ".backup /backup/app-$(date +%F).db" # or, for a database you can open read-only sqlite3 app.db "VACUUM INTO '/backup/app-$(date +%F).db'"
If you must copy files instead of using the API, copy app.db,
app.db-wal and app.db-shm in one go and keep them together — that triple
is the database. A cp of the first file alone is a snapshot of a moving target.
Check the copy you already have
Attach the .db and the -wal below: if the newest commit is still in the
log, it says so and tells you to keep the pair instead of restoring the stub.
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.