Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion Lib/test/test_sqlite3/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import sqlite3 as sqlite
import unittest

from test.support import import_helper
from test.support import captured_stderr, force_not_colorized, import_helper
from test.support.os_helper import TESTFN, unlink

from .util import memory_database, cx_limit, with_tracebacks
Expand Down Expand Up @@ -106,6 +106,50 @@ def mycoll(x, y):
self.assertEqual(result, [('c',), ('b',), ('a',)],
msg="the expected order was not returned")

@force_not_colorized
def test_collation_error_then_step_failure_while_fetching(self):
# gh-161837: A collation can set an exception. Then a later error can
# make the same sqlite3_step() fail. The fetch must raise
# OperationalError for that failure, not SystemError.
def mycoll(x, y):
if x == "boom":
raise KeyError("collation raised")
return (x > y) - (x < y)
self.con.create_collation("mycoll", mycoll)
self.con.execute("create table t(x, y)")
# abs() of the smallest 64-bit integer causes "integer overflow".
self.con.executemany("insert into t values (?, ?)",
[(1, "a"), (-2**63, "boom")])
sql = """
select case when y = 'q' collate mycoll then abs(x) else 0 end
from t order by rowid
"""

def fetch_with_fetchone():
cur = self.con.execute(sql)
self.assertEqual(cur.fetchone(), (0,))
cur.fetchone()

def fetch_by_iteration():
list(self.con.execute(sql))

for fetch in fetch_with_fetchone, fetch_by_iteration:
for tracebacks in False, True:
with self.subTest(fetch=fetch.__name__, tracebacks=tracebacks):
sqlite.enable_callback_tracebacks(tracebacks)
try:
with captured_stderr() as err:
with self.assertRaisesRegex(sqlite.OperationalError,
"integer overflow"):
fetch()
finally:
sqlite.enable_callback_tracebacks(False)
if tracebacks:
self.assertIn("KeyError: 'collation raised'",
err.getvalue())
else:
self.assertEqual(err.getvalue(), "")

def test_collation_register_twice(self):
"""
Register two different collation functions under the same name.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix a :exc:`SystemError` in :mod:`sqlite3`. The error occurs when a
collation callback raises an exception and the same step then fails during a
fetch. In debug builds, an assertion fails. Now the module raises
:exc:`~sqlite3.OperationalError` for the step failure, as
:meth:`~sqlite3.Cursor.execute` does.
26 changes: 18 additions & 8 deletions Modules/_sqlite/cursor.c
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,22 @@ stmt_step(sqlite3_stmt *statement)
return rc;
}

/* A user-defined callback (for example, a collation) can set an exception
* during a failed sqlite3_step(). Report that exception, then clear it.
* The caller can then raise the DB-API exception for the failure. */
static void
clear_callback_error(pysqlite_state *state)
{
if (PyErr_Occurred()) {
if (state->enable_callback_tracebacks) {
PyErr_Print();
}
else {
PyErr_Clear();
}
}
}

static int
bind_param(pysqlite_state *state, pysqlite_Statement *self, int pos,
PyObject *parameter)
Expand Down Expand Up @@ -911,14 +927,7 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation

rc = stmt_step(self->statement->st);
if (rc != SQLITE_DONE && rc != SQLITE_ROW) {
if (PyErr_Occurred()) {
/* there was an error that occurred in a user-defined callback */
if (state->enable_callback_tracebacks) {
PyErr_Print();
} else {
PyErr_Clear();
}
}
clear_callback_error(state);
set_error_from_db(state, self->connection->db);
goto error;
}
Expand Down Expand Up @@ -1177,6 +1186,7 @@ pysqlite_cursor_iternext(PyObject *op)
}
}
else if (rc != SQLITE_ROW) {
clear_callback_error(self->connection->state);
rc = set_error_from_db(self->connection->state, self->connection->db);
int reset_rc = stmt_reset(self->statement);
Py_CLEAR(self->statement);
Expand Down
Loading