diff options
Diffstat (limited to 'qutebrowser/misc/sql.py')
-rw-r--r-- | qutebrowser/misc/sql.py | 104 |
1 files changed, 74 insertions, 30 deletions
diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py index 062da39de..28a97fd77 100644 --- a/qutebrowser/misc/sql.py +++ b/qutebrowser/misc/sql.py @@ -19,16 +19,17 @@ """Provides access to sqlite databases.""" +import enum import collections import contextlib import dataclasses import types -from typing import Any, Dict, Iterator, List, Mapping, MutableSequence, Optional, Type +from typing import Any, Dict, Iterator, List, Mapping, MutableSequence, Optional, Type, Union -from PyQt5.QtCore import QObject, pyqtSignal -from PyQt5.QtSql import QSqlDatabase, QSqlError, QSqlQuery +from qutebrowser.qt.core import QObject, pyqtSignal +from qutebrowser.qt.sql import QSqlDatabase, QSqlError, QSqlQuery -from qutebrowser.qt import sip +from qutebrowser.qt import sip, machinery from qutebrowser.utils import debug, log @@ -69,24 +70,45 @@ class UserVersion: return f'{self.major}.{self.minor}' -class SqliteErrorCode: +class SqliteErrorCode(enum.Enum): + """Primary error codes as used by sqlite. - """Error codes as used by sqlite. - - See https://sqlite.org/rescode.html - note we only define the codes we use - in qutebrowser here. + See https://sqlite.org/rescode.html """ - ERROR = '1' # generic error code - BUSY = '5' # database is locked - READONLY = '8' # attempt to write a readonly database - IOERR = '10' # disk I/O error - CORRUPT = '11' # database disk image is malformed - FULL = '13' # database or disk is full - CANTOPEN = '14' # unable to open database file - PROTOCOL = '15' # locking protocol error - CONSTRAINT = '19' # UNIQUE constraint failed - NOTADB = '26' # file is not a database + # pylint: disable=invalid-name + + OK = 0 # Successful result + ERROR = 1 # Generic error + INTERNAL = 2 # Internal logic error in SQLite + PERM = 3 # Access permission denied + ABORT = 4 # Callback routine requested an abort + BUSY = 5 # The database file is locked + LOCKED = 6 # A table in the database is locked + NOMEM = 7 # A malloc() failed + READONLY = 8 # Attempt to write a readonly database + INTERRUPT = 9 # Operation terminated by sqlite3_interrupt()*/ + IOERR = 10 # Some kind of disk I/O error occurred + CORRUPT = 11 # The database disk image is malformed + NOTFOUND = 12 # Unknown opcode in sqlite3_file_control() + FULL = 13 # Insertion failed because database is full + CANTOPEN = 14 # Unable to open the database file + PROTOCOL = 15 # Database lock protocol error + EMPTY = 16 # Internal use only + SCHEMA = 17 # The database schema changed + TOOBIG = 18 # String or BLOB exceeds size limit + CONSTRAINT = 19 # Abort due to constraint violation + MISMATCH = 20 # Data type mismatch + MISUSE = 21 # Library used incorrectly + NOLFS = 22 # Uses OS features not supported on host + AUTH = 23 # Authorization denied + FORMAT = 24 # Not used + RANGE = 25 # 2nd parameter to sqlite3_bind out of range + NOTADB = 26 # File opened that is not a database file + NOTICE = 27 # Notifications from sqlite3_log() + WARNING = 28 # Warnings from sqlite3_log() + ROW = 100 # sqlite3_step() has another row ready + DONE = 101 # sqlite3_step() has finished executing class Error(Exception): @@ -104,8 +126,7 @@ class Error(Exception): """ if self.error is None: return str(self) - else: - return self.error.databaseText() + return self.error.databaseText() class KnownError(Error): @@ -128,6 +149,14 @@ class BugError(Error): def raise_sqlite_error(msg: str, error: QSqlError) -> None: """Raise either a BugError or KnownError.""" error_code = error.nativeErrorCode() + primary_error_code: Union[SqliteErrorCode, str] + try: + # https://sqlite.org/rescode.html#pve + primary_error_code = SqliteErrorCode(int(error_code) & 0xff) + except ValueError: + # not an int, or unknown error code -> fall back to string + primary_error_code = error_code + database_text = error.databaseText() driver_text = error.driverText() @@ -135,7 +164,7 @@ def raise_sqlite_error(msg: str, error: QSqlError) -> None: log.sql.debug(f"type: {debug.qenum_key(QSqlError, error.type())}") log.sql.debug(f"database text: {database_text}") log.sql.debug(f"driver text: {driver_text}") - log.sql.debug(f"error code: {error_code}") + log.sql.debug(f"error code: {error_code} -> {primary_error_code}") known_errors = [ SqliteErrorCode.BUSY, @@ -151,12 +180,12 @@ def raise_sqlite_error(msg: str, error: QSqlError) -> None: # https://github.com/qutebrowser/qutebrowser/issues/4681 # If the query we built was too long too_long_err = ( - error_code == SqliteErrorCode.ERROR and + primary_error_code == SqliteErrorCode.ERROR and (database_text.startswith("Expression tree is too large") or database_text in ["too many SQL variables", "LIKE or GLOB pattern too complex"])) - if error_code in known_errors or too_long_err: + if primary_error_code in known_errors or too_long_err: raise KnownError(msg, error) raise BugError(msg, error) @@ -299,6 +328,7 @@ class Query: ok = self.query.prepare(querystr) self._check_ok('prepare', ok) self.query.setForwardOnly(forward_only) + self._placeholders: List[str] = [] def __iter__(self) -> Iterator[Any]: if not self.query.isActive(): @@ -319,15 +349,26 @@ class Query: msg = f'Failed to {step} query "{query}": "{error.text()}"' raise_sqlite_error(msg, error) + def _validate_bound_values(self): + """Make sure all placeholders are bound.""" + qt_bound_values = self.query.boundValues() + if machinery.IS_QT5: + # Qt 5: Returns a dict + values = list(qt_bound_values.values()) + else: + # Qt 6: Returns a list + values = qt_bound_values + + if None in values: + raise BugError("Missing bound values!") + def _bind_values(self, values: Mapping[str, Any]) -> Dict[str, Any]: + self._placeholders = list(values) for key, val in values.items(): self.query.bindValue(f':{key}', val) - bound_values = self.bound_values() - if None in bound_values.values(): - raise BugError("Missing bound values!") - - return bound_values + self._validate_bound_values() + return self.bound_values() def run(self, **values: Any) -> 'Query': """Execute the prepared query.""" @@ -378,7 +419,10 @@ class Query: return rows def bound_values(self) -> Dict[str, Any]: - return self.query.boundValues() + return { + f":{key}": self.query.boundValue(f":{key}") + for key in self._placeholders + } class SqlTable(QObject): |