From 761f178651b032de505ca4ccc8d012c38beeab25 Mon Sep 17 00:00:00 2001 From: General_K1ng Date: Mon, 16 Jun 2025 22:39:22 +0800 Subject: [PATCH 1/3] Fix: add immutable=1 flag for read-only SQLite access to avoid WAL/SHM errors on readonly DB --- Lib/dbm/sqlite3.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Lib/dbm/sqlite3.py b/Lib/dbm/sqlite3.py index b296a1bcd1bbfa..7ee35d04399420 100644 --- a/Lib/dbm/sqlite3.py +++ b/Lib/dbm/sqlite3.py @@ -59,19 +59,24 @@ def __init__(self, path, /, *, flag, mode): # We use the URI format when opening the database. uri = _normalize_uri(path) - uri = f"{uri}?mode={flag}" + if flag == "ro": + # Add immutable=1 to allow read-only SQLite access even if wal/shm missing + uri = f"{uri}?mode={flag}&immutable=1" + else: + uri = f"{uri}?mode={flag}" try: self._cx = sqlite3.connect(uri, autocommit=True, uri=True) except sqlite3.Error as exc: raise error(str(exc)) - + self._readonly = (flag == "ro") # This is an optimization only; it's ok if it fails. - with suppress(sqlite3.OperationalError): - self._cx.execute("PRAGMA journal_mode = wal") + if not self._readonly: + with suppress(sqlite3.OperationalError): + self._cx.execute("PRAGMA journal_mode = OFF") - if flag == "rwc": - self._execute(BUILD_TABLE) + if flag == "rwc": + self._execute(BUILD_TABLE) def _execute(self, *args, **kwargs): if not self._cx: From 3e3a3ba6c6f1a8ea747e08b3d1ad6b098366d4b6 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:00:14 +0000 Subject: [PATCH 2/3] gh-135386: Fix dbm.sqlite3 readonly open error by using immutable=1 --- Lib/dbm/sqlite3.py | 1 + Lib/test/test_dbm_sqlite3.py | 17 +++++++++++++++++ ...25-06-16-15-00-13.gh-issue-135386.lNrxLc.rst | 1 + 3 files changed, 19 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-06-16-15-00-13.gh-issue-135386.lNrxLc.rst diff --git a/Lib/dbm/sqlite3.py b/Lib/dbm/sqlite3.py index 7ee35d04399420..9ca9f8f999e207 100644 --- a/Lib/dbm/sqlite3.py +++ b/Lib/dbm/sqlite3.py @@ -69,6 +69,7 @@ def __init__(self, path, /, *, flag, mode): self._cx = sqlite3.connect(uri, autocommit=True, uri=True) except sqlite3.Error as exc: raise error(str(exc)) + self._readonly = (flag == "ro") # This is an optimization only; it's ok if it fails. if not self._readonly: diff --git a/Lib/test/test_dbm_sqlite3.py b/Lib/test/test_dbm_sqlite3.py index 9216da8a63f957..1553e28cfee197 100644 --- a/Lib/test/test_dbm_sqlite3.py +++ b/Lib/test/test_dbm_sqlite3.py @@ -1,3 +1,4 @@ +import os import sys import unittest from contextlib import closing @@ -89,6 +90,22 @@ def test_readonly_keys(self): def test_readonly_iter(self): self.assertEqual([k for k in self.db], [b"key1", b"key2"]) + def test_readonly_open_without_wal_shm(self): + wal_path = self.filename + "-wal" + shm_path = self.filename + "-shm" + + for suffix in wal_path, shm_path: + try: + os.remove(suffix) + except FileNotFoundError: + pass + + os.chmod(self.filename, 0o444) + + with dbm_sqlite3.open(self.filename, "r") as db: + self.assertEqual(db[b"key1"], b"value1") + self.assertEqual(db[b"key2"], b"value2") + class ReadWrite(_SQLiteDbmTests): diff --git a/Misc/NEWS.d/next/Library/2025-06-16-15-00-13.gh-issue-135386.lNrxLc.rst b/Misc/NEWS.d/next/Library/2025-06-16-15-00-13.gh-issue-135386.lNrxLc.rst new file mode 100644 index 00000000000000..d3f81cb9201aaf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-16-15-00-13.gh-issue-135386.lNrxLc.rst @@ -0,0 +1 @@ +Fix :exc:`sqlite3.OperationalError` error when using :func:`dbm.open` with a read-only file object. From 7f988ddabc6df30213af62294b26fbbe57c9c0a7 Mon Sep 17 00:00:00 2001 From: General_K1ng Date: Tue, 17 Jun 2025 21:56:37 +0800 Subject: [PATCH 3/3] Add test: readonly sqlite db without wal/shm (skip on Windows) --- Lib/test/test_dbm_sqlite3.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_dbm_sqlite3.py b/Lib/test/test_dbm_sqlite3.py index 1553e28cfee197..467ada060cfe77 100644 --- a/Lib/test/test_dbm_sqlite3.py +++ b/Lib/test/test_dbm_sqlite3.py @@ -90,6 +90,7 @@ def test_readonly_keys(self): def test_readonly_iter(self): self.assertEqual([k for k in self.db], [b"key1", b"key2"]) + @unittest.skipIf(sys.platform.startswith("win"), "incompatible with Windows file locking") def test_readonly_open_without_wal_shm(self): wal_path = self.filename + "-wal" shm_path = self.filename + "-shm"