dbcheck check a file "disk image is malformed" empty backup deleted rows "database is locked" integrity_check says ok

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.