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."""
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()

View File

@ -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.

View File

@ -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'])