summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <me@the-compiler.org>2022-08-26 17:50:53 +0200
committerFlorian Bruhin <me@the-compiler.org>2022-08-26 19:11:15 +0200
commitee4d6e0396a6b570f4d5592a9c4c1a9fee1027b6 (patch)
tree16cdb2c8bdbedfa15d578b1e7bdc2f3798dd5ec6
parent6ffc5174eade1d99f3069e4128af7026e69f9ab1 (diff)
downloadqutebrowser-ee4d6e0396a6b570f4d5592a9c4c1a9fee1027b6.tar.gz
qutebrowser-ee4d6e0396a6b570f4d5592a9c4c1a9fee1027b6.zip
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
-rw-r--r--qutebrowser/misc/sql.py60
-rwxr-xr-xscripts/dev/run_vulture.py5
-rw-r--r--tests/unit/misc/test_sql.py71
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'])