diff --git a/Lib/dbm/sqlite3.py b/Lib/dbm/sqlite3.py index b296a1bcd1bbfa..42c04878d2f85c 100644 --- a/Lib/dbm/sqlite3.py +++ b/Lib/dbm/sqlite3.py @@ -60,6 +60,9 @@ 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 += "&immutable=1" try: self._cx = sqlite3.connect(uri, autocommit=True, uri=True) @@ -67,11 +70,12 @@ def __init__(self, path, /, *, flag, mode): raise error(str(exc)) # This is an optimization only; it's ok if it fails. - with suppress(sqlite3.OperationalError): - self._cx.execute("PRAGMA journal_mode = wal") + if flag != "ro": + 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: diff --git a/Lib/test/test_dbm_sqlite3.py b/Lib/test/test_dbm_sqlite3.py index 9216da8a63f957..321ad41c7f36bc 100644 --- a/Lib/test/test_dbm_sqlite3.py +++ b/Lib/test/test_dbm_sqlite3.py @@ -1,3 +1,5 @@ +import os +import stat import sys import unittest from contextlib import closing @@ -89,6 +91,88 @@ def test_readonly_keys(self): def test_readonly_iter(self): self.assertEqual([k for k in self.db], [b"key1", b"key2"]) +class Immutable(unittest.TestCase): + def setUp(self): + self.filename = os_helper.TESTFN + + db = dbm_sqlite3.open(self.filename, "c") + db[b"key"] = b"value" + db.close() + + self.db = dbm_sqlite3.open(self.filename, "r") + + def tearDown(self): + self.db.close() + for suffix in "", "-wal", "-shm": + os_helper.unlink(self.filename + suffix) + + def test_readonly_open_without_wal_shm(self): + wal_path = self.filename + "-wal" + shm_path = self.filename + "-shm" + + self.assertFalse(os.path.exists(wal_path)) + self.assertFalse(os.path.exists(shm_path)) + + self.assertEqual(self.db[b"key"], b"value") + + +class ReadOnlyFilesystem(unittest.TestCase): + + def setUp(self): + self.test_dir = os_helper.TESTFN + os.mkdir(self.test_dir) + self.db_path = os.path.join(self.test_dir, "test.db") + + db = dbm_sqlite3.open(self.db_path, "c") + db[b"key"] = b"value" + db.close() + + def tearDown(self): + os.chmod(self.db_path, stat.S_IWRITE) + os.chmod(self.test_dir, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD) + os_helper.rmtree(self.test_dir) + + def test_open_readonly_dir_success_ro(self): + files = os.listdir(self.test_dir) + self.assertEqual(sorted(files), ["test.db"]) + + os.chmod(self.test_dir, stat.S_IREAD | stat.S_IEXEC) + with dbm_sqlite3.open(self.db_path, "r") as db: + self.assertEqual(db[b"key"], b"value") + + def test_open_readonly_file_success(self): + os.chmod(self.db_path, stat.S_IREAD) + with dbm_sqlite3.open(self.db_path, "r") as db: + self.assertEqual(db[b"key"], b"value") + + def test_open_readonly_file_fail_rw(self): + os.chmod(self.db_path, stat.S_IREAD) + with dbm_sqlite3.open(self.db_path, "w") as db: + with self.assertRaises(OSError): + db[b"newkey"] = b"newvalue" + @unittest.skipUnless(sys.platform == "darwin", "SQLite fallback behavior differs on non-macOS") + def test_open_readonly_dir_fail_rw_missing_wal_shm(self): + for suffix in ("-wal", "-shm"): + os_helper.unlink(self.db_path + suffix) + + os.chmod(self.test_dir, stat.S_IREAD | stat.S_IEXEC) + + with self.assertRaises(OSError): + with dbm_sqlite3.open(self.db_path, "w") as db: + db[b"newkey"] = b"newvalue" + + @unittest.skipUnless(sys.platform == "darwin", "SQLite fallback behavior differs on non-macOS") + def test_open_readonly_dir_fail_rw_with_writable_db(self): + os.chmod(self.db_path, stat.S_IREAD | stat.S_IWRITE) + for suffix in ("-wal", "-shm"): + os_helper.unlink(self.db_path + suffix) + + os.chmod(self.test_dir, stat.S_IREAD | stat.S_IEXEC) + + with self.assertRaises(OSError): + with dbm_sqlite3.open(self.db_path, "w") as db: + db[b"newkey"] = b"newvalue" + 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.