自作のツールの状態を1か所で見るダッシュボードを作っていて、ほかのツールの SQLite の DB を直接読んで状態を出しています。決めていた条件は「相手のツールには何も書き込まない(読むだけ)」です。そのため、mode=ro(読み取り専用)で開いていました。
ところが、相手の DB の横に -wal と -shm というファイルができて、残っていました。読み取り専用のつもりで、相手のフォルダにファイルを増やしていたことになります。
原因は、相手の DB が WAL モードだったことでした。書き込み側が止まっているときに mode=ro で読むと、SQLite がこの2つのファイルを作ります。書き込み側が止まっていて -wal も -shm もないときは、immutable=1 を付けて開けば、ファイルは作られません。
解決方法
WAL モードの DB で、-wal と -shm がどちらもないときだけ immutable=1 を付けます。それ以外は普通に mode=ro で開きます。
import sqlite3
from pathlib import Path
def connect_readonly(path):
"""ほかのプロセスの SQLite を、ファイルを増やさずに読み取り専用で開く"""
path = Path(path).resolve()
with path.open("rb") as f:
header = f.read(20)
is_wal = header[18:20] == b"\x02\x02" # ヘッダーの 18・19 バイト目が 2 なら WAL モード
has_sidecar = Path(f"{path}-wal").exists() or Path(f"{path}-shm").exists()
uri = path.as_uri() + "?mode=ro"
if is_wal and not has_sidecar:
# 書き込み側が止まっていて、内容はすべて DB 本体に入っている
uri += "&immutable=1"
return sqlite3.connect(uri, uri=True)
- WAL モードで、
-wal・-shmがない:書き込み側が止まっています。最後の接続が閉じるときに、WAL の中身は DB 本体に書き戻されています。DB 本体だけを読めばよいので、immutable=1で開きます - WAL モードで、
-wal・-shmがある:書き込み側が動いています。ファイルはもうあるので、mode=roで開いても新しいファイルは作られません - WAL モードではない:
-wal・-shmはもともと使わないので、mode=roで開きます。書き込みの途中でもファイルがないので、ここでimmutable=1を付けると危険です
手元で試したところ、3つのどの状態でも正しい件数が読め、ファイルも増えませんでした。
immutable=1 は、書き込み側が動いているときには使えません。 書き込み中(WAL にだけ更新がある状態)に immutable=1 で開くと、WAL の中身が見えず、テーブルを作ったことすら見えませんでした。
sqlite3.OperationalError: no such table: t
SQLite の公式ドキュメントにも、immutable を付けたファイルが実際には変わると、間違った結果や SQLITE_CORRUPT になることがあると書かれています。そのため、「WAL モードで -wal・-shm がないときだけ」に限っています。
SQLite の URI で指定できる開き方
sqlite3.connect("file:app.db?mode=ro", uri=True) のように、URI の後ろに付けて指定します。公式ドキュメント(Uniform Resource Identifiers)の説明をまとめると、次のとおりです。
| 指定 | 意味 |
|---|---|
mode=ro |
読み取り専用で開く |
mode=rw |
読み書きできるように開く(ファイルがなければエラー) |
mode=rwc |
読み書きできるように開き、ファイルがなければ作る |
mode=memory |
ディスクを使わない、メモリの中だけの DB にする |
immutable=1 |
DB のファイルは読み取り専用のメディアにあって、ほかのプロセスからも変わらない、と SQLite に伝える。いつも読み取り専用で開き、ロックと変更の確認をしない |
mode=ro は「この接続から DB を書き換えない」という意味で、「ファイルを一切作らない」という意味ではありません。ファイルを作らないのは immutable=1 のほうですが、そのぶん、ほかのプロセスが書き換えていないことは自分で保証する必要があります。
起きたこと
ダッシュボードが読んでいた DB の1つは、1時間に1回だけ動く処理が書き込むものでした。読むときはたいてい書き込み側が止まっているので、ダッシュボードで画面を開くたびに、ファイルが作られうる状態でした。
最小の手順で再現すると、次のようになります。
import sqlite3
w = sqlite3.connect("app.db")
w.execute("PRAGMA journal_mode=WAL")
w.execute("CREATE TABLE t(x)")
w.execute("INSERT INTO t VALUES (1)")
w.commit()
w.close() # ここで -wal・-shm は消える
r = sqlite3.connect("file:app.db?mode=ro", uri=True)
r.execute("SELECT count(*) FROM t").fetchone()
r.close() # -wal・-shm が残る
| タイミング | フォルダの中 |
|---|---|
| 書き込み側が開いている | app.db・app.db-shm・app.db-wal |
| 書き込み側を閉じた | app.db |
mode=ro で接続しただけ |
app.db |
mode=ro で SELECT した |
app.db・app.db-shm・app.db-wal |
mode=ro を閉じたあと |
app.db・app.db-shm・app.db-wal(-wal は 0 バイト、-shm は 32KB) |
ファイルができるのは、接続したときではなく、最初に読んだときでした。
原因
WAL モードでは、読む側も -shm(共有メモリの索引)と -wal を使います。
SQLite の公式ドキュメント(Write-Ahead Logging の「Read-Only Databases」)には、書き込み権限のない WAL の DB を読めるのは、次のどれかに当てはまるときだと書かれています。
-shmと-walがすでにあって、読める- DB のあるフォルダに書き込み権限があり、
-shmと-walを作れる immutableを付けて開いている
mode=ro で開いたときは 2 に当てはまるので、SQLite が -shm と -wal を作ります。手元では、読み取り専用の接続を閉じたあとも、2つとも残りました。
実際、フォルダを読み取り専用にして同じことをすると、ファイルを作れずにエラーになりました。immutable=1 なら、この場合も読めました。
sqlite3.OperationalError: attempt to write a readonly database
残るすき間
-wal・-shm があるかを確かめてから開くまでの間に、書き込み側が終わってファイルを消すと、mode=ro の接続がまたファイルを作ってしまいます。Python の標準の sqlite3 では、「ファイルを作らない」ように SQLite のファイルの扱い(VFS)を差し替えることができないので、このすき間は防げません。
私は、読む前と読んだあとで、DB 本体と -wal・-shm が変わっていないかを比べ、変わっていたらその結果を捨てるようにしました。防ぐのではなく、気づいて捨てる方針です。
macOS 標準の Python では、ファイルを作らずにエラーになる
macOS に最初から入っている /usr/bin/python3(Apple の SQLite)で読むと、動きが違いました。-wal・-shm がない WAL の DB を mode=ro で開くと、ファイルは作らずにエラーになります。immutable=1 なら読めました。
sqlite3.OperationalError: unable to open database file
また、Apple の SQLite で書き込む側は、閉じても -wal・-shm を消さずに残しました。Bun のドキュメントによると、Apple の SQLite は、閉じても -wal・-shm を残す設定(persistent WAL)でビルドされているそうです。書き込み側も読む側も Apple の SQLite なら、ファイルがいつも残っているので、この問題は表に出にくいと思います。
読む側がどちらの SQLite でも、上の解決方法のコードで読めます。
確かめた環境
| Python | SQLite | 書き込み側が止まっているときに mode=ro で読むと |
|---|---|---|
| 3.14.2(Homebrew) | 3.51.1 | -wal・-shm が作られて残る |
| 3.9.6(macOS 標準) | 3.51.0(Apple) | ファイルは作らずに unable to open database file |
macOS 26.1 で確かめました。
参考
- Write-Ahead Logging:Read-Only Databases(SQLite)
- Uniform Resource Identifiers(SQLite)
- SQLite(Bun のドキュメント)
あわせて読みたい
