Refactor SQL error handling

This renames SqlException to SqlError (to be more consistent with how Python
names exceptions), and adds an utility function which logs a few more useful
details about errors.

See #3004
This commit is contained in:
Florian Bruhin 2017-10-02 09:40:35 +02:00
parent eacdbe132e
commit 5af8a95c82
3 changed files with 40 additions and 19 deletions

View File

@ -425,7 +425,7 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing sql...") log.init.debug("Initializing sql...")
try: try:
sql.init(os.path.join(standarddir.data(), 'history.sqlite')) sql.init(os.path.join(standarddir.data(), 'history.sqlite'))
except sql.SqlException as e: except sql.SqlError as e:
error.handle_fatal_exc(e, args, 'Error initializing SQL', error.handle_fatal_exc(e, args, 'Error initializing SQL',
pre_text='Error initializing SQL') pre_text='Error initializing SQL')
sys.exit(usertypes.Exit.err_init) sys.exit(usertypes.Exit.err_init)

View File

@ -22,12 +22,12 @@
import collections import collections
from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtSql import QSqlDatabase, QSqlQuery from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlError
from qutebrowser.utils import log from qutebrowser.utils import log, debug
class SqlException(Exception): class SqlError(Exception):
"""Raised on an error interacting with the SQL database.""" """Raised on an error interacting with the SQL database."""
@ -38,12 +38,14 @@ def init(db_path):
"""Initialize the SQL database connection.""" """Initialize the SQL database connection."""
database = QSqlDatabase.addDatabase('QSQLITE') database = QSqlDatabase.addDatabase('QSQLITE')
if not database.isValid(): if not database.isValid():
raise SqlException('Failed to add database. ' raise SqlError('Failed to add database. '
'Are sqlite and Qt sqlite support installed?') 'Are sqlite and Qt sqlite support installed?')
database.setDatabaseName(db_path) database.setDatabaseName(db_path)
if not database.open(): if not database.open():
raise SqlException("Failed to open sqlite database at {}: {}" error = database.lastError()
.format(db_path, database.lastError().text())) _log_error(error)
raise SqlError("Failed to open sqlite database at {}: {}"
.format(db_path, error.text()))
def close(): def close():
@ -60,10 +62,32 @@ def version():
close() close()
return ver return ver
return Query("select sqlite_version()").run().value() return Query("select sqlite_version()").run().value()
except SqlException as e: except SqlError as e:
return 'UNAVAILABLE ({})'.format(e) return 'UNAVAILABLE ({})'.format(e)
def _log_error(error):
"""Log informations about a SQL error to the debug log."""
log.sql.debug("SQL error:")
log.sql.debug("type: {}".format(debug.qenum_key(QSqlError, error.type())))
log.sql.debug("database text: {}".format(error.databaseText()))
log.sql.debug("driver text: {}".format(error.driverText()))
log.sql.debug("error code: {}".format(error.nativeErrorCode()))
def _handle_query_error(what, query, error):
"""Handle a sqlite error.
Arguments:
what: What we were doing when the error happened.
query: The query which was executed.
error: The QSqlError object.
"""
_log_error(error)
msg = 'Failed to {} query "{}": "{}"'.format(what, query, error.text())
raise SqlError(msg)
class Query(QSqlQuery): class Query(QSqlQuery):
"""A prepared SQL Query.""" """A prepared SQL Query."""
@ -79,13 +103,12 @@ class Query(QSqlQuery):
super().__init__(QSqlDatabase.database()) super().__init__(QSqlDatabase.database())
log.sql.debug('Preparing SQL query: "{}"'.format(querystr)) log.sql.debug('Preparing SQL query: "{}"'.format(querystr))
if not self.prepare(querystr): if not self.prepare(querystr):
raise SqlException('Failed to prepare query "{}": "{}"'.format( _handle_query_error('prepare', querystr, self.lastError())
querystr, self.lastError().text()))
self.setForwardOnly(forward_only) self.setForwardOnly(forward_only)
def __iter__(self): def __iter__(self):
if not self.isActive(): if not self.isActive():
raise SqlException("Cannot iterate inactive query") raise SqlError("Cannot iterate inactive query")
rec = self.record() rec = self.record()
fields = [rec.fieldName(i) for i in range(rec.count())] fields = [rec.fieldName(i) for i in range(rec.count())]
rowtype = collections.namedtuple('ResultRow', fields) rowtype = collections.namedtuple('ResultRow', fields)
@ -101,14 +124,13 @@ class Query(QSqlQuery):
self.bindValue(':{}'.format(key), val) self.bindValue(':{}'.format(key), val)
log.sql.debug('query bindings: {}'.format(self.boundValues())) log.sql.debug('query bindings: {}'.format(self.boundValues()))
if not self.exec_(): if not self.exec_():
raise SqlException('Failed to exec query "{}": "{}"'.format( _handle_query_error('exec', self.lastQuery(), self.lastError())
self.lastQuery(), self.lastError().text()))
return self return self
def value(self): def value(self):
"""Return the result of a single-value query (e.g. an EXISTS).""" """Return the result of a single-value query (e.g. an EXISTS)."""
if not self.next(): if not self.next():
raise SqlException("No result for single-result query") raise SqlError("No result for single-result query")
return self.record().value(0) return self.record().value(0)
@ -128,7 +150,7 @@ class SqlTable(QObject):
def __init__(self, name, fields, constraints=None, parent=None): def __init__(self, name, fields, constraints=None, parent=None):
"""Create a new table in the sql database. """Create a new table in the sql database.
Raises SqlException if the table already exists. Raises SqlError if the table already exists.
Args: Args:
name: Name of the table. name: Name of the table.
@ -228,8 +250,7 @@ class SqlTable(QObject):
db = QSqlDatabase.database() db = QSqlDatabase.database()
db.transaction() db.transaction()
if not q.execBatch(): if not q.execBatch():
raise SqlException('Failed to exec query "{}": "{}"'.format( _handle_query_error('exec', q.lastQuery(), q.lastError())
q.lastQuery(), q.lastError().text()))
db.commit() db.commit()
self.changed.emit() self.changed.emit()

View File

@ -49,7 +49,7 @@ def test_insert_replace(qtbot):
table.insert({'name': 'one', 'val': 11, 'lucky': True}, replace=True) table.insert({'name': 'one', 'val': 11, 'lucky': True}, replace=True)
assert list(table) == [('one', 11, True)] assert list(table) == [('one', 11, True)]
with pytest.raises(sql.SqlException): with pytest.raises(sql.SqlError):
table.insert({'name': 'one', 'val': 11, 'lucky': True}, replace=False) table.insert({'name': 'one', 'val': 11, 'lucky': True}, replace=False)
@ -85,7 +85,7 @@ def test_insert_batch_replace(qtbot):
('one', 11, True), ('one', 11, True),
('nine', 19, True)] ('nine', 19, True)]
with pytest.raises(sql.SqlException): with pytest.raises(sql.SqlError):
table.insert_batch({'name': ['one', 'nine'], table.insert_batch({'name': ['one', 'nine'],
'val': [11, 19], 'val': [11, 19],
'lucky': [True, True]}) 'lucky': [True, True]})