From ee4d6e0396a6b570f4d5592a9c4c1a9fee1027b6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Aug 2022 17:50:53 +0200 Subject: sql: Add *all* primary sqlite result codes For three reasons: - There are only 31 of them, and we don't really expect any more to turn up (last happened in 2013, and we have a test for it happening) - It makes for nicer debug output - It always felt strange to only have a small subset in the enum --- qutebrowser/misc/sql.py | 60 ++++++++++++++++++++++++++------------ scripts/dev/run_vulture.py | 5 +++- tests/unit/misc/test_sql.py | 71 +++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 111 insertions(+), 25 deletions(-) diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py index 6c588c980..2603ce23e 100644 --- a/qutebrowser/misc/sql.py +++ b/qutebrowser/misc/sql.py @@ -19,6 +19,7 @@ """Provides access to sqlite databases.""" +import enum import collections import contextlib import dataclasses @@ -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): @@ -130,9 +151,10 @@ def raise_sqlite_error(msg: str, error: QSqlError) -> None: error_code = error.nativeErrorCode() try: # https://sqlite.org/rescode.html#pve - primary_error_code = int(error_code) & 0xff + primary_error_code = SqliteErrorCode(int(error_code) & 0xff) except ValueError: - primary_error_code = None + # not an int, or unknown error code -> fall back to string + primary_error_code = error_code database_text = error.databaseText() driver_text = error.driverText() diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index 901abe235..1e71b08d2 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -31,7 +31,7 @@ import vulture import qutebrowser.app # pylint: disable=unused-import from qutebrowser.extensions import loader -from qutebrowser.misc import objects +from qutebrowser.misc import objects, sql from qutebrowser.utils import utils, version, qtutils # To run the decorators from there # pylint: disable=unused-import @@ -150,6 +150,9 @@ def whitelist_generator(): # noqa: C901 for name in list(qtutils.LibraryPath): yield f'qutebrowser.utils.qtutils.LibraryPath.{name}' + for name in list(sql.SqliteErrorCode): + yield f'qutebrowser.misc.sql.SqliteErrorCode.{name}' + def filter_func(item): """Check if a missing function should be filtered or not. diff --git a/tests/unit/misc/test_sql.py b/tests/unit/misc/test_sql.py index 3e16ff337..38869f712 100644 --- a/tests/unit/misc/test_sql.py +++ b/tests/unit/misc/test_sql.py @@ -19,6 +19,8 @@ """Test the SQL API.""" +import sys +import sqlite3 import pytest import hypothesis @@ -91,12 +93,15 @@ def test_sqlerror(klass): class TestSqlError: @pytest.mark.parametrize('error_code, exception', [ - (sql.SqliteErrorCode.BUSY, sql.KnownError), - (sql.SqliteErrorCode.CONSTRAINT, sql.BugError), + (sql.SqliteErrorCode.BUSY.value, sql.KnownError), + (sql.SqliteErrorCode.CONSTRAINT.value, sql.BugError), # extended error codes - (sql.SqliteErrorCode.IOERR | (1<<8), sql.KnownError), # SQLITE_IOERR_READ ( - sql.SqliteErrorCode.CONSTRAINT | (1<<8), # SQLITE_CONSTRAINT_CHECK + sql.SqliteErrorCode.IOERR.value | (1 << 8), # SQLITE_IOERR_READ + sql.KnownError + ), + ( + sql.SqliteErrorCode.CONSTRAINT.value | (1 << 8), # SQLITE_CONSTRAINT_CHECK sql.BugError ), ]) @@ -115,7 +120,7 @@ class TestSqlError: 'type: UnknownError', 'database text: db text', 'driver text: driver text', - 'error code: 23 -> 23'] + 'error code: 23 -> SqliteErrorCode.AUTH'] assert caplog.messages == expected @@ -125,6 +130,62 @@ class TestSqlError: err = klass("Message", sql_err) assert err.text() == "db text" + @pytest.mark.parametrize("code", list(sql.SqliteErrorCode)) + @pytest.mark.skipif( + sys.version_info < (3, 11), + reason="sqlite error code constants added in Python 3.11", + ) + def test_sqlite_error_codes(self, code): + """Cross check our error codes with the ones in Python 3.11+. + + See https://github.com/python/cpython/commit/86d8b465231 + """ + pyvalue = getattr(sqlite3, f"SQLITE_{code.name}") + assert pyvalue == code.value + + def test_sqlite_error_codes_reverse(self): + """Check if we have all error codes defined that Python has. + + It would be nice if this was easier (and less guesswork). + However, the error codes are simply added as ints to the sqlite3 module + namespace (PyModule_AddIntConstant), and lots of other constants are there too. + """ + # Start with all SQLITE_* names in the sqlite3 modules + consts = {n for n in dir(sqlite3) if n.startswith("SQLITE_")} + # All error codes we know about (tested above) + consts -= {f"SQLITE_{m.name}" for m in sql.SqliteErrorCode} + # Extended error codes or other constants. From the sqlite docs: + # + # Primary result code symbolic names are of the form "SQLITE_XXXXXX" + # where XXXXXX is a sequence of uppercase alphabetic characters. + # Extended result code names are of the form "SQLITE_XXXXXX_YYYYYYY" + # where the XXXXXX part is the corresponding primary result code and the + # YYYYYYY is an extension that further classifies the result code. + consts -= {c for c in consts if c.count("_") >= 2} + # All remaining sqlite constants which are *not* error codes. + consts -= { + "SQLITE_ANALYZE", + "SQLITE_ATTACH", + "SQLITE_DELETE", + "SQLITE_DENY", + "SQLITE_DETACH", + "SQLITE_FUNCTION", + "SQLITE_IGNORE", + "SQLITE_INSERT", + "SQLITE_PRAGMA", + "SQLITE_READ", + "SQLITE_RECURSIVE", + "SQLITE_REINDEX", + "SQLITE_SAVEPOINT", + "SQLITE_SELECT", + "SQLITE_TRANSACTION", + "SQLITE_UPDATE", + } + # If there is anything remaining here, either a new Python version added a new + # sqlite constant which is *not* an error, or there was a new error code added. + # Either add it to the set above, or to SqliteErrorCode. + assert not consts + def test_init_table(database): database.table('Foo', ['name', 'val', 'lucky']) -- cgit v1.2.3-54-g00ecf