Skip to content

Commit c0ae92b

Browse files
gh-135386: Fix "unable to open database file" errors on readonly DB (GH-135566)
Add immutable=1 flag for read-only SQLite access to avoid WAL/SHM errors on readonly DB. Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent f27af8b commit c0ae92b

File tree

3 files changed

+56
-5
lines changed

3 files changed

+56
-5
lines changed

Lib/dbm/sqlite3.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,22 @@ def __init__(self, path, /, *, flag, mode):
6060
# We use the URI format when opening the database.
6161
uri = _normalize_uri(path)
6262
uri = f"{uri}?mode={flag}"
63+
if flag == "ro":
64+
# Add immutable=1 to allow read-only SQLite access even if wal/shm missing
65+
uri += "&immutable=1"
6366

6467
try:
6568
self._cx = sqlite3.connect(uri, autocommit=True, uri=True)
6669
except sqlite3.Error as exc:
6770
raise error(str(exc))
6871

69-
# This is an optimization only; it's ok if it fails.
70-
with suppress(sqlite3.OperationalError):
71-
self._cx.execute("PRAGMA journal_mode = wal")
72+
if flag != "ro":
73+
# This is an optimization only; it's ok if it fails.
74+
with suppress(sqlite3.OperationalError):
75+
self._cx.execute("PRAGMA journal_mode = wal")
7276

73-
if flag == "rwc":
74-
self._execute(BUILD_TABLE)
77+
if flag == "rwc":
78+
self._execute(BUILD_TABLE)
7579

7680
def _execute(self, *args, **kwargs):
7781
if not self._cx:

Lib/test/test_dbm_sqlite3.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
import stat
13
import sys
24
import unittest
35
from contextlib import closing
@@ -90,6 +92,49 @@ def test_readonly_iter(self):
9092
self.assertEqual([k for k in self.db], [b"key1", b"key2"])
9193

9294

95+
class ReadOnlyFilesystem(unittest.TestCase):
96+
97+
def setUp(self):
98+
self.test_dir = os_helper.TESTFN
99+
self.addCleanup(os_helper.rmtree, self.test_dir)
100+
os.mkdir(self.test_dir)
101+
self.db_path = os.path.join(self.test_dir, "test.db")
102+
103+
db = dbm_sqlite3.open(self.db_path, "c")
104+
db[b"key"] = b"value"
105+
db.close()
106+
107+
def test_readonly_file_read(self):
108+
os.chmod(self.db_path, stat.S_IREAD)
109+
with dbm_sqlite3.open(self.db_path, "r") as db:
110+
self.assertEqual(db[b"key"], b"value")
111+
112+
def test_readonly_file_write(self):
113+
os.chmod(self.db_path, stat.S_IREAD)
114+
with dbm_sqlite3.open(self.db_path, "w") as db:
115+
with self.assertRaises(dbm_sqlite3.error):
116+
db[b"newkey"] = b"newvalue"
117+
118+
def test_readonly_dir_read(self):
119+
os.chmod(self.test_dir, stat.S_IREAD | stat.S_IEXEC)
120+
with dbm_sqlite3.open(self.db_path, "r") as db:
121+
self.assertEqual(db[b"key"], b"value")
122+
123+
def test_readonly_dir_write(self):
124+
os.chmod(self.test_dir, stat.S_IREAD | stat.S_IEXEC)
125+
with dbm_sqlite3.open(self.db_path, "w") as db:
126+
try:
127+
db[b"newkey"] = b"newvalue"
128+
modified = True # on Windows and macOS
129+
except dbm_sqlite3.error:
130+
modified = False
131+
with dbm_sqlite3.open(self.db_path, "r") as db:
132+
if modified:
133+
self.assertEqual(db[b"newkey"], b"newvalue")
134+
else:
135+
self.assertNotIn(b"newkey", db)
136+
137+
93138
class ReadWrite(_SQLiteDbmTests):
94139

95140
def setUp(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix opening a :mod:`dbm.sqlite3` database for reading from read-only file
2+
or directory.

0 commit comments

Comments
 (0)