Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7eb087a
Improve error message when database directory is not writable
VedantMadane Jan 16, 2026
c6acfe8
Address review feedback: soften wording, include original error
VedantMadane Jan 16, 2026
0fde4cf
Remove readonly check - only triggers on write ops, not open
VedantMadane Jan 16, 2026
0fcdb02
docs: Add changelog entry for database permission error fix
VedantMadane Feb 10, 2026
acb57a7
test: Add tests for database error handling to improve coverage (reso…
VedantMadane Mar 1, 2026
c836955
Fix test failures: correct path literal and formatting issues
VedantMadane Jan 26, 2026
55332f8
Fix TypeError: use fsdecode to handle bytes temp_dir path
VedantMadane Jan 26, 2026
4c7fb3d
Address code review: soften error message and improve path handling
VedantMadane Jan 26, 2026
c4aa76f
Update changelog to reflect code review improvements
VedantMadane Feb 10, 2026
61b677c
Increase SQLite busy timeout to 30s to resolve 'database is locked' e…
VedantMadane Mar 1, 2026
3fe085d
Fix mypy type error: use bytes literal for db_dir fallback
VedantMadane Mar 5, 2026
5414b01
Merge branch 'master' into fix-db-permission-error
VedantMadane Mar 8, 2026
7b84d82
Merge branch 'master' into fix-db-permission-error
VedantMadane Mar 10, 2026
1ee3e4c
Merge upstream master into fix-db-permission-error
VedantMadane Mar 22, 2026
394b388
chore: drop stale ruff ignore for renamed test_field_diff path
VedantMadane Mar 22, 2026
f240fd9
docs: move database error changelog entry under Unreleased
VedantMadane Mar 22, 2026
1f37d6c
docs: remove duplicate 2.6.0 changelog line (fixes Check docs CI)
VedantMadane Mar 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion beets/dbcore/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -1115,7 +1115,7 @@ class Database:
data is written in a transaction.
"""

def __init__(self, path, timeout: float = 5.0):
def __init__(self, path, timeout: float = 30.0):
if sqlite3.threadsafety == 0:
raise RuntimeError(
"sqlite3 must be compiled with multi-threading support"
Expand Down
20 changes: 18 additions & 2 deletions beets/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,9 +891,25 @@ def _open_library(config: confuse.LazyConfig) -> library.Library:
lib.get_item(0) # Test database connection.
except (sqlite3.OperationalError, sqlite3.DatabaseError) as db_error:
log.debug("{}", traceback.format_exc())
# Check for permission-related errors and provide a helpful message
error_str = str(db_error).lower()
dbpath_display = util.displayable_path(dbpath)
if "unable to open" in error_str:
# Normalize path and get directory
normalized_path = os.path.abspath(dbpath)
db_dir = os.path.dirname(normalized_path)
# Handle edge case where path has no directory component
if not db_dir:
db_dir = b"."
raise UserError(
f"database file {dbpath_display} could not be opened. "
f"This may be due to a permissions issue. If the database "
f"does not exist yet, please check that the file or directory "
f"{util.displayable_path(db_dir)} is writable "
f"(original error: {db_error})."
)
raise UserError(
f"database file {util.displayable_path(dbpath)} cannot not be"
f" opened: {db_error}"
f"database file {dbpath_display} could not be opened: {db_error}"
)
log.debug(
"library database: {}\nlibrary directory: {}",
Expand Down
6 changes: 6 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ Bug fixes
- :ref:`import-cmd` Autotagging by explicit release or recording IDs now keeps
candidates from all enabled metadata sources instead of dropping matches when
different providers share the same ID. :bug:`6178` :bug:`6181`
- Improved error message when database cannot be opened. When SQLite fails to
open the database with 'unable to open' error, beets now provides a helpful
message suggesting it may be a permissions issue and recommends checking that
the file or directory is writable. The original SQLite error is included for
debugging. Also fixed typo in error message ('cannot not' to 'could not').
:bug:`1676`
- :doc:`plugins/mbsync` and :doc:`plugins/missing` now use each item's stored
``data_source`` for ID lookups, with a fallback to ``MusicBrainz``.
- :doc:`plugins/musicbrainz`: Use ``va_name`` config for ``albumartist_sort``,
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ ignore = [
"beets/**" = ["PT"]
"test/plugins/test_ftintitle.py" = ["E501"]
"test/test_util.py" = ["E501"]
"test/ui/test_ui_init.py" = ["PT"]
"test/util/test_diff.py" = ["E501"]
"test/util/test_id_extractors.py" = ["E501"]
"test/**" = ["RUF001"] # we use Unicode characters in tests
Expand Down
55 changes: 54 additions & 1 deletion test/ui/test_ui_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@

import os
import shutil
import sqlite3
import unittest
from copy import deepcopy
from random import random
from unittest import mock

from beets import config, ui
from beets import config, library, ui
from beets.test import _common
from beets.test.helper import BeetsTestCase, IOMixin

Expand Down Expand Up @@ -119,3 +121,54 @@ def test_create_no(self):
if lib:
lib._close()
raise OSError("Parent directories should not be created.")


class DatabaseErrorTest(BeetsTestCase):
"""Test database error handling with improved error messages."""

def test_database_error_with_unable_to_open(self):
"""Test error message when database fails with 'unable to open' error."""
test_config = deepcopy(config)
test_config["library"] = _common.os.fsdecode(
os.path.join(self.temp_dir, b"test.db")
)

# Mock Library to raise OperationalError with "unable to open"
with mock.patch.object(
library,
"Library",
side_effect=sqlite3.OperationalError(
"unable to open database file"
),
):
with self.assertRaises(ui.UserError) as cm:
ui._open_library(test_config)

error_message = str(cm.exception)
# Should mention permissions and directory
self.assertIn("directory", error_message.lower())
self.assertIn("writable", error_message.lower())
self.assertIn("permissions", error_message.lower())

def test_database_error_fallback(self):
"""Test fallback error message for other database errors."""
test_config = deepcopy(config)
test_config["library"] = _common.os.fsdecode(
os.path.join(self.temp_dir, b"test.db")
)

# Mock Library to raise a different OperationalError
with mock.patch.object(
library,
"Library",
side_effect=sqlite3.OperationalError("disk I/O error"),
):
with self.assertRaises(ui.UserError) as cm:
ui._open_library(test_config)

error_message = str(cm.exception)
# Should contain the error but not the permissions message
self.assertIn("could not be opened", error_message)
self.assertIn("disk I/O error", error_message)
# Should NOT have the permissions-related message
self.assertNotIn("permissions", error_message.lower())
Loading