Skip to content

Commit

Permalink
Clean up SQLite exception handling. #323
Browse files Browse the repository at this point in the history
  • Loading branch information
lemon24 committed Feb 25, 2024
1 parent 7c47755 commit a8ccac3
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 133 deletions.
59 changes: 19 additions & 40 deletions src/reader/_storage/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,29 @@
import sqlite3
from collections.abc import Callable
from collections.abc import Iterable
from functools import partial
from typing import Any
from typing import TypeVar

from . import _sqlite_utils
from ..exceptions import StorageError
from ._sql_utils import paginated_query
from ._sql_utils import Query
from ._sqlite_utils import DBError
from ._sqlite_utils import LocalConnectionFactory
from ._sqlite_utils import setup_db
from ._sqlite_utils import wrap_exceptions


APPLICATION_ID = b'read'

MISSING_MIGRATION_DETAIL = (
"; you may have skipped some required migrations, see "
"https://reader.readthedocs.io/en/latest/changelog.html#removed-migrations-3-0"
)
_T = TypeVar('_T')


_T = TypeVar('_T')
wrap_exceptions = partial(_sqlite_utils.wrap_exceptions, StorageError)


class StorageBase:
#: Private storage API.
chunk_size = 2**8

@wrap_exceptions(StorageError)
@wrap_exceptions(message="while opening database")
def __init__(
self,
path: str,
Expand All @@ -43,59 +38,43 @@ def __init__(
if factory: # pragma: no cover
kwargs['factory'] = factory

with wrap_exceptions(StorageError, "error while opening database"):
self.factory = LocalConnectionFactory(path, **kwargs)
db = self.factory()

with wrap_exceptions(StorageError, "error while setting up database"):
try:
try:
self.setup_db(db)
except BaseException:
db.close()
raise
except DBError as e:
message = str(e)
if 'no migration' in message:
message += MISSING_MIGRATION_DETAIL
raise StorageError(message=message) from None
self.factory = _sqlite_utils.LocalConnectionFactory(path, **kwargs)
db = self.factory()
try:
self.setup_db(db)
except BaseException:
db.close()
raise

self.path = path
self.timeout = timeout

def get_db(self) -> sqlite3.Connection:
"""Private storage API (used by search)."""
try:
return self.factory()
except DBError as e:
raise StorageError(message=str(e)) from None
return self.factory()

@staticmethod
def setup_db(db: sqlite3.Connection) -> None:
"""Private storage API (used by tests)."""
from ._schema import MIGRATION
from . import MINIMUM_SQLITE_VERSION, REQUIRED_SQLITE_FUNCTIONS

return setup_db(
return _sqlite_utils.setup_db(
db,
migration=MIGRATION,
id=APPLICATION_ID,
minimum_sqlite_version=MINIMUM_SQLITE_VERSION,
required_sqlite_functions=REQUIRED_SQLITE_FUNCTIONS,
)

@wrap_exceptions(StorageError)
@wrap_exceptions()
def __enter__(self) -> None:
try:
self.factory.__enter__()
except DBError as e:
raise StorageError(message=str(e)) from None
self.factory.__enter__()

@wrap_exceptions(StorageError)
@wrap_exceptions()
def __exit__(self, *_: Any) -> None:
self.factory.__exit__()

@wrap_exceptions(StorageError)
@wrap_exceptions()
def close(self) -> None:
self.factory.close()

Expand All @@ -106,7 +85,7 @@ def paginated_query(
last: tuple[Any, ...] | None = None,
row_factory: Callable[[tuple[Any, ...]], _T] | None = None,
) -> Iterable[_T]:
with wrap_exceptions(StorageError):
with wrap_exceptions():
yield from paginated_query(
self.get_db(),
make_query,
Expand Down
26 changes: 6 additions & 20 deletions src/reader/_storage/_changes.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,22 @@
from __future__ import annotations

import sqlite3
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Any
from typing import TYPE_CHECKING

from .._types import Action
from .._types import Change
from ..exceptions import ChangeTrackingNotEnabledError
from ..exceptions import StorageError
from ._base import wrap_exceptions
from ._sql_utils import parse_schema
from ._sql_utils import Query
from ._sqlite_utils import ddl_transaction
from ._sqlite_utils import wrap_exceptions

if TYPE_CHECKING: # pragma: no cover
from ._base import StorageBase


@contextmanager
def wrap_changes_exceptions(enabled: bool = True) -> Iterator[None]:
try:
yield
except sqlite3.OperationalError as e:
msg_lower = str(e).lower()
# TODO: check table name and/or set cause
if enabled and 'no such table' in msg_lower:
raise ChangeTrackingNotEnabledError() from None
raise # pragma: no cover
ENABLED_EXC = {'no such table': lambda _: ChangeTrackingNotEnabledError()}


class Changes:
Expand All @@ -37,7 +25,7 @@ class Changes:
def __init__(self, storage: StorageBase):
self.storage = storage

@wrap_exceptions(StorageError)
@wrap_exceptions()
def enable(self) -> None:
with ddl_transaction(self.storage.get_db()) as db:
try:
Expand All @@ -61,7 +49,7 @@ def _enable(cls, db: sqlite3.Connection) -> None:
"""
)

@wrap_exceptions(StorageError)
@wrap_exceptions()
def disable(self) -> None:
with ddl_transaction(self.storage.get_db()) as db:
self._disable(db)
Expand All @@ -74,8 +62,7 @@ def _disable(cls, db: sqlite3.Connection) -> None:
db.execute(f"DROP {object.type} IF EXISTS {object.name}")
db.execute("UPDATE entries SET sequence = NULL")

@wrap_exceptions(StorageError)
@wrap_changes_exceptions()
@wrap_exceptions(ENABLED_EXC)
def get(
self, action: Action | None = None, limit: int | None = None
) -> list[Change]:
Expand All @@ -90,8 +77,7 @@ def get(
rows = self.storage.get_db().execute(str(query), context)
return list(map(change_factory, rows))

@wrap_exceptions(StorageError)
@wrap_changes_exceptions()
@wrap_exceptions(ENABLED_EXC)
def done(self, changes: list[Change]) -> None:
# FIXME: len(changes) <= self.storage.chunk_size
with self.storage.get_db() as db:
Expand Down
6 changes: 5 additions & 1 deletion src/reader/_storage/_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,5 +245,9 @@ def update_from_37_to_38(db: sqlite3.Connection, /) -> None: # pragma: no cover
36: update_from_36_to_37,
37: update_from_37_to_38,
}
MISSING_SUFFIX = (
"; you may have skipped some required migrations, see "
"https://reader.readthedocs.io/en/latest/changelog.html#removed-migrations-3-0"
)

MIGRATION = HeavyMigration(create_all, VERSION, MIGRATIONS)
MIGRATION = HeavyMigration(create_all, VERSION, MIGRATIONS, MISSING_SUFFIX)
Loading

0 comments on commit a8ccac3

Please sign in to comment.