Skip to content

Commit 40506eb

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. (cherry picked from commit c0ae92b) Co-authored-by: General_K1ng <generak1ng0@gmail.com> Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 57c4601 commit 40506eb

File tree

3 files changed

+62
-5
lines changed

3 files changed

+62
-5
lines changed

Lib/dbm/sqlite3.py

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

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

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

72-
if flag == "rwc":
73-
self._execute(BUILD_TABLE)
76+
if flag == "rwc":
77+
self._execute(BUILD_TABLE)
7478

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

Lib/test/test_dbm_sqlite3.py

Lines changed: 51 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
@@ -14,6 +16,11 @@
1416
from dbm.sqlite3 import _normalize_uri
1517

1618

19+
root_in_posix = False
20+
if hasattr(os, 'geteuid'):
21+
root_in_posix = (os.geteuid() == 0)
22+
23+
1724
class _SQLiteDbmTests(unittest.TestCase):
1825

1926
def setUp(self):
@@ -90,6 +97,50 @@ def test_readonly_iter(self):
9097
self.assertEqual([k for k in self.db], [b"key1", b"key2"])
9198

9299

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

95146
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)