Handle some sqlite errors gracefully
We mark some SQL errors as "environmental", and then show those as error messages instead of raising an exception. Fixes #3004 Workaround for #2930
This commit is contained in:
parent
6498273b31
commit
b608259751
@ -422,13 +422,19 @@ def _init_modules(args, crash_handler):
|
|||||||
readline_bridge = readline.ReadlineBridge()
|
readline_bridge = readline.ReadlineBridge()
|
||||||
objreg.register('readline-bridge', readline_bridge)
|
objreg.register('readline-bridge', readline_bridge)
|
||||||
|
|
||||||
log.init.debug("Initializing sql...")
|
|
||||||
try:
|
try:
|
||||||
|
log.init.debug("Initializing sql...")
|
||||||
sql.init(os.path.join(standarddir.data(), 'history.sqlite'))
|
sql.init(os.path.join(standarddir.data(), 'history.sqlite'))
|
||||||
|
|
||||||
|
log.init.debug("Initializing web history...")
|
||||||
|
history.init(qApp)
|
||||||
except sql.SqlError as e:
|
except sql.SqlError as e:
|
||||||
error.handle_fatal_exc(e, args, 'Error initializing SQL',
|
if e.environmental:
|
||||||
pre_text='Error initializing SQL')
|
error.handle_fatal_exc(e, args, 'Error initializing SQL',
|
||||||
sys.exit(usertypes.Exit.err_init)
|
pre_text='Error initializing SQL')
|
||||||
|
sys.exit(usertypes.Exit.err_init)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
log.init.debug("Initializing completion...")
|
log.init.debug("Initializing completion...")
|
||||||
completiondelegate.init()
|
completiondelegate.init()
|
||||||
@ -436,9 +442,6 @@ def _init_modules(args, crash_handler):
|
|||||||
log.init.debug("Initializing command history...")
|
log.init.debug("Initializing command history...")
|
||||||
cmdhistory.init()
|
cmdhistory.init()
|
||||||
|
|
||||||
log.init.debug("Initializing web history...")
|
|
||||||
history.init(qApp)
|
|
||||||
|
|
||||||
log.init.debug("Initializing crashlog...")
|
log.init.debug("Initializing crashlog...")
|
||||||
if not args.no_err_windows:
|
if not args.no_err_windows:
|
||||||
crash_handler.handle_segfault()
|
crash_handler.handle_segfault()
|
||||||
|
@ -190,15 +190,24 @@ class WebHistory(sql.SqlTable):
|
|||||||
return
|
return
|
||||||
|
|
||||||
atime = int(atime) if (atime is not None) else int(time.time())
|
atime = int(atime) if (atime is not None) else int(time.time())
|
||||||
self.insert({'url': self._format_url(url),
|
|
||||||
'title': title,
|
try:
|
||||||
'atime': atime,
|
self.insert({'url': self._format_url(url),
|
||||||
'redirect': redirect})
|
'title': title,
|
||||||
if not redirect:
|
'atime': atime,
|
||||||
self.completion.insert({'url': self._format_completion_url(url),
|
'redirect': redirect})
|
||||||
'title': title,
|
if not redirect:
|
||||||
'last_atime': atime},
|
self.completion.insert({
|
||||||
replace=True)
|
'url': self._format_completion_url(url),
|
||||||
|
'title': title,
|
||||||
|
'last_atime': atime
|
||||||
|
}, replace=True)
|
||||||
|
except sql.SqlError as e:
|
||||||
|
if e.environmental:
|
||||||
|
message.error("Failed to write history: {}".format(
|
||||||
|
e.error.databaseText()))
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
def _parse_entry(self, line):
|
def _parse_entry(self, line):
|
||||||
"""Parse a history line like '12345 http://example.com title'."""
|
"""Parse a history line like '12345 http://example.com title'."""
|
||||||
|
@ -29,9 +29,59 @@ from qutebrowser.utils import log, debug
|
|||||||
|
|
||||||
class SqlError(Exception):
|
class SqlError(Exception):
|
||||||
|
|
||||||
"""Raised on an error interacting with the SQL database."""
|
"""Raised on an error interacting with the SQL database.
|
||||||
|
|
||||||
pass
|
Attributes:
|
||||||
|
environmental: Whether the error is likely caused by the environment
|
||||||
|
and not a qutebrowser bug.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, msg, environmental=False):
|
||||||
|
super().__init__(msg)
|
||||||
|
self.environmental = environmental
|
||||||
|
|
||||||
|
|
||||||
|
class SqliteError(SqlError):
|
||||||
|
|
||||||
|
"""A SQL error with a QSqlError available.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
error: The QSqlError object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, msg, error):
|
||||||
|
super().__init__(msg)
|
||||||
|
self.error = error
|
||||||
|
|
||||||
|
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()))
|
||||||
|
|
||||||
|
# https://sqlite.org/rescode.html
|
||||||
|
environmental_errors = [
|
||||||
|
# SQLITE_LOCKED,
|
||||||
|
# https://github.com/qutebrowser/qutebrowser/issues/2930
|
||||||
|
'9',
|
||||||
|
# SQLITE_FULL,
|
||||||
|
# https://github.com/qutebrowser/qutebrowser/issues/3004
|
||||||
|
'13',
|
||||||
|
]
|
||||||
|
self.environmental = error.nativeErrorCode() in environmental_errors
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_query(cls, what, query, error):
|
||||||
|
"""Construct an error from a failed query.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
what: What we were doing when the error happened.
|
||||||
|
query: The query which was executed.
|
||||||
|
error: The QSqlError object.
|
||||||
|
"""
|
||||||
|
msg = 'Failed to {} query "{}": "{}"'.format(what, query, error.text())
|
||||||
|
return cls(msg, error)
|
||||||
|
|
||||||
|
|
||||||
def init(db_path):
|
def init(db_path):
|
||||||
@ -39,13 +89,13 @@ def init(db_path):
|
|||||||
database = QSqlDatabase.addDatabase('QSQLITE')
|
database = QSqlDatabase.addDatabase('QSQLITE')
|
||||||
if not database.isValid():
|
if not database.isValid():
|
||||||
raise SqlError('Failed to add database. '
|
raise SqlError('Failed to add database. '
|
||||||
'Are sqlite and Qt sqlite support installed?')
|
'Are sqlite and Qt sqlite support installed?',
|
||||||
|
environmental=True)
|
||||||
database.setDatabaseName(db_path)
|
database.setDatabaseName(db_path)
|
||||||
if not database.open():
|
if not database.open():
|
||||||
error = database.lastError()
|
error = database.lastError()
|
||||||
_log_error(error)
|
raise SqliteError("Failed to open sqlite database at {}: {}"
|
||||||
raise SqlError("Failed to open sqlite database at {}: {}"
|
.format(db_path, error.text()), error)
|
||||||
.format(db_path, error.text()))
|
|
||||||
|
|
||||||
|
|
||||||
def close():
|
def close():
|
||||||
@ -66,28 +116,6 @@ def version():
|
|||||||
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."""
|
||||||
@ -103,7 +131,7 @@ 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):
|
||||||
_handle_query_error('prepare', querystr, self.lastError())
|
raise SqliteError.from_query('prepare', querystr, self.lastError())
|
||||||
self.setForwardOnly(forward_only)
|
self.setForwardOnly(forward_only)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
@ -124,7 +152,8 @@ 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_():
|
||||||
_handle_query_error('exec', self.lastQuery(), self.lastError())
|
raise SqliteError.from_query('exec', self.lastQuery(),
|
||||||
|
self.lastError())
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def value(self):
|
def value(self):
|
||||||
@ -250,7 +279,7 @@ class SqlTable(QObject):
|
|||||||
db = QSqlDatabase.database()
|
db = QSqlDatabase.database()
|
||||||
db.transaction()
|
db.transaction()
|
||||||
if not q.execBatch():
|
if not q.execBatch():
|
||||||
_handle_query_error('exec', q.lastQuery(), q.lastError())
|
raise SqliteError.from_query('exec', q.lastQuery(), q.lastError())
|
||||||
db.commit()
|
db.commit()
|
||||||
self.changed.emit()
|
self.changed.emit()
|
||||||
|
|
||||||
|
@ -22,10 +22,56 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from qutebrowser.misc import sql
|
from qutebrowser.misc import sql
|
||||||
|
|
||||||
|
from PyQt5.QtSql import QSqlError
|
||||||
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.usefixtures('init_sql')
|
pytestmark = pytest.mark.usefixtures('init_sql')
|
||||||
|
|
||||||
|
|
||||||
|
def test_sqlerror():
|
||||||
|
err = sql.SqlError("Hello World", environmental=True)
|
||||||
|
assert str(err) == "Hello World"
|
||||||
|
assert err.environmental
|
||||||
|
|
||||||
|
|
||||||
|
class TestSqliteError:
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('error_code, environmental', [
|
||||||
|
('9', True), # SQLITE_LOCKED
|
||||||
|
('19', False), # SQLITE_CONSTRAINT
|
||||||
|
])
|
||||||
|
def test_environmental(self, error_code, environmental):
|
||||||
|
sql_err = QSqlError("driver text", "db text", QSqlError.UnknownError,
|
||||||
|
error_code)
|
||||||
|
err = sql.SqliteError("Message", sql_err)
|
||||||
|
assert err.environmental == environmental
|
||||||
|
|
||||||
|
def test_logging(self, caplog):
|
||||||
|
sql_err = QSqlError("driver text", "db text", QSqlError.UnknownError,
|
||||||
|
'23')
|
||||||
|
sql.SqliteError("Message", sql_err)
|
||||||
|
lines = [r.message for r in caplog.records]
|
||||||
|
expected = ['SQL error:',
|
||||||
|
'type: UnknownError',
|
||||||
|
'database text: db text',
|
||||||
|
'driver text: driver text',
|
||||||
|
'error code: 23']
|
||||||
|
|
||||||
|
assert lines == expected
|
||||||
|
|
||||||
|
def test_from_query(self):
|
||||||
|
sql_err = QSqlError("driver text", "db text")
|
||||||
|
err = sql.SqliteError.from_query(
|
||||||
|
what='test', query='SELECT * from foo;', error=sql_err)
|
||||||
|
expected = ('Failed to test query "SELECT * from foo;": '
|
||||||
|
'"db text driver text"')
|
||||||
|
assert str(err) == expected
|
||||||
|
|
||||||
|
def test_subclass(self):
|
||||||
|
with pytest.raises(sql.SqlError):
|
||||||
|
raise sql.SqliteError("text", QSqlError())
|
||||||
|
|
||||||
|
|
||||||
def test_init():
|
def test_init():
|
||||||
sql.SqlTable('Foo', ['name', 'val', 'lucky'])
|
sql.SqlTable('Foo', ['name', 'val', 'lucky'])
|
||||||
# should not error if table already exists
|
# should not error if table already exists
|
||||||
|
Loading…
Reference in New Issue
Block a user