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
This commit is contained in:
Florian Bruhin 2022-08-26 17:50:53 +02:00
parent 6ffc5174ea
commit ee4d6e0396
3 changed files with 111 additions and 25 deletions

View File

@ -19,6 +19,7 @@
"""Provides access to sqlite databases.""" """Provides access to sqlite databases."""
import enum
import collections import collections
import contextlib import contextlib
import dataclasses import dataclasses
@ -69,24 +70,45 @@ class UserVersion:
return f'{self.major}.{self.minor}' 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
See https://sqlite.org/rescode.html - note we only define the codes we use
in qutebrowser here.
""" """
ERROR = 1 # generic error code # pylint: disable=invalid-name
BUSY = 5 # database is locked
READONLY = 8 # attempt to write a readonly database OK = 0 # Successful result
IOERR = 10 # disk I/O error ERROR = 1 # Generic error
CORRUPT = 11 # database disk image is malformed INTERNAL = 2 # Internal logic error in SQLite
FULL = 13 # database or disk is full PERM = 3 # Access permission denied
CANTOPEN = 14 # unable to open database file ABORT = 4 # Callback routine requested an abort
PROTOCOL = 15 # locking protocol error BUSY = 5 # The database file is locked
CONSTRAINT = 19 # UNIQUE constraint failed LOCKED = 6 # A table in the database is locked
NOTADB = 26 # file is not a database 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): class Error(Exception):
@ -104,8 +126,7 @@ class Error(Exception):
""" """
if self.error is None: if self.error is None:
return str(self) return str(self)
else: return self.error.databaseText()
return self.error.databaseText()
class KnownError(Error): class KnownError(Error):
@ -130,9 +151,10 @@ def raise_sqlite_error(msg: str, error: QSqlError) -> None:
error_code = error.nativeErrorCode() error_code = error.nativeErrorCode()
try: try:
# https://sqlite.org/rescode.html#pve # https://sqlite.org/rescode.html#pve
primary_error_code = int(error_code) & 0xff primary_error_code = SqliteErrorCode(int(error_code) & 0xff)
except ValueError: 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() database_text = error.databaseText()
driver_text = error.driverText() driver_text = error.driverText()

View File

@ -31,7 +31,7 @@ import vulture
import qutebrowser.app # pylint: disable=unused-import import qutebrowser.app # pylint: disable=unused-import
from qutebrowser.extensions import loader from qutebrowser.extensions import loader
from qutebrowser.misc import objects from qutebrowser.misc import objects, sql
from qutebrowser.utils import utils, version, qtutils from qutebrowser.utils import utils, version, qtutils
# To run the decorators from there # To run the decorators from there
# pylint: disable=unused-import # pylint: disable=unused-import
@ -150,6 +150,9 @@ def whitelist_generator(): # noqa: C901
for name in list(qtutils.LibraryPath): for name in list(qtutils.LibraryPath):
yield f'qutebrowser.utils.qtutils.LibraryPath.{name}' yield f'qutebrowser.utils.qtutils.LibraryPath.{name}'
for name in list(sql.SqliteErrorCode):
yield f'qutebrowser.misc.sql.SqliteErrorCode.{name}'
def filter_func(item): def filter_func(item):
"""Check if a missing function should be filtered or not. """Check if a missing function should be filtered or not.

View File

@ -19,6 +19,8 @@
"""Test the SQL API.""" """Test the SQL API."""
import sys
import sqlite3
import pytest import pytest
import hypothesis import hypothesis
@ -91,12 +93,15 @@ def test_sqlerror(klass):
class TestSqlError: class TestSqlError:
@pytest.mark.parametrize('error_code, exception', [ @pytest.mark.parametrize('error_code, exception', [
(sql.SqliteErrorCode.BUSY, sql.KnownError), (sql.SqliteErrorCode.BUSY.value, sql.KnownError),
(sql.SqliteErrorCode.CONSTRAINT, sql.BugError), (sql.SqliteErrorCode.CONSTRAINT.value, sql.BugError),
# extended error codes # 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 sql.BugError
), ),
]) ])
@ -115,7 +120,7 @@ class TestSqlError:
'type: UnknownError', 'type: UnknownError',
'database text: db text', 'database text: db text',
'driver text: driver text', 'driver text: driver text',
'error code: 23 -> 23'] 'error code: 23 -> SqliteErrorCode.AUTH']
assert caplog.messages == expected assert caplog.messages == expected
@ -125,6 +130,62 @@ class TestSqlError:
err = klass("Message", sql_err) err = klass("Message", sql_err)
assert err.text() == "db text" 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): def test_init_table(database):
database.table('Foo', ['name', 'val', 'lucky']) database.table('Foo', ['name', 'val', 'lucky'])