Skip to content

Commit 0eec627

Browse files
serhiy-storchakaErlend Egeberg Aasland
and
Erlend Egeberg Aasland
authored
bpo-44859: Improve error handling in sqlite3 and and raise more accurate exceptions. (GH-27654)
* MemoryError is now raised instead of sqlite3.Warning when memory is not enough for encoding a statement to UTF-8 in Connection.__call__() and Cursor.execute(). * UnicodEncodeError is now raised instead of sqlite3.Warning when the statement contains surrogate characters in Connection.__call__() and Cursor.execute(). * TypeError is now raised instead of ValueError for non-string script argument in Cursor.executescript(). * ValueError is now raised for script containing the null character instead of truncating it in Cursor.executescript(). * Correctly handle exceptions raised when getting boolean value of the result of the progress handler. * Add many tests covering different corner cases. Co-authored-by: Erlend Egeberg Aasland <[email protected]>
1 parent ebecffd commit 0eec627

File tree

10 files changed

+226
-52
lines changed

10 files changed

+226
-52
lines changed

Lib/sqlite3/test/dbapi.py

+30-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import threading
2727
import unittest
2828

29-
from test.support import check_disallow_instantiation, threading_helper
29+
from test.support import check_disallow_instantiation, threading_helper, bigmemtest
3030
from test.support.os_helper import TESTFN, unlink
3131

3232

@@ -758,9 +758,35 @@ def test_script_error_normal(self):
758758
def test_cursor_executescript_as_bytes(self):
759759
con = sqlite.connect(":memory:")
760760
cur = con.cursor()
761-
with self.assertRaises(ValueError) as cm:
761+
with self.assertRaises(TypeError):
762762
cur.executescript(b"create table test(foo); insert into test(foo) values (5);")
763-
self.assertEqual(str(cm.exception), 'script argument must be unicode.')
763+
764+
def test_cursor_executescript_with_null_characters(self):
765+
con = sqlite.connect(":memory:")
766+
cur = con.cursor()
767+
with self.assertRaises(ValueError):
768+
cur.executescript("""
769+
create table a(i);\0
770+
insert into a(i) values (5);
771+
""")
772+
773+
def test_cursor_executescript_with_surrogates(self):
774+
con = sqlite.connect(":memory:")
775+
cur = con.cursor()
776+
with self.assertRaises(UnicodeEncodeError):
777+
cur.executescript("""
778+
create table a(s);
779+
insert into a(s) values ('\ud8ff');
780+
""")
781+
782+
@unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform')
783+
@bigmemtest(size=2**31, memuse=3, dry_run=False)
784+
def test_cursor_executescript_too_large_script(self, maxsize):
785+
con = sqlite.connect(":memory:")
786+
cur = con.cursor()
787+
for size in 2**31-1, 2**31:
788+
with self.assertRaises(sqlite.DataError):
789+
cur.executescript("create table a(s);".ljust(size))
764790

765791
def test_connection_execute(self):
766792
con = sqlite.connect(":memory:")
@@ -969,6 +995,7 @@ def suite():
969995
CursorTests,
970996
ExtensionTests,
971997
ModuleTests,
998+
OpenTests,
972999
SqliteOnConflictTests,
9731000
ThreadTests,
9741001
UninitialisedConnectionTests,

Lib/sqlite3/test/hooks.py

+27-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import sqlite3 as sqlite
2525

2626
from test.support.os_helper import TESTFN, unlink
27-
27+
from .userfunctions import with_tracebacks
2828

2929
class CollationTests(unittest.TestCase):
3030
def test_create_collation_not_string(self):
@@ -145,7 +145,6 @@ def progress():
145145
""")
146146
self.assertTrue(progress_calls)
147147

148-
149148
def test_opcode_count(self):
150149
"""
151150
Test that the opcode argument is respected.
@@ -198,6 +197,32 @@ def progress():
198197
con.execute("select 1 union select 2 union select 3").fetchall()
199198
self.assertEqual(action, 0, "progress handler was not cleared")
200199

200+
@with_tracebacks(['bad_progress', 'ZeroDivisionError'])
201+
def test_error_in_progress_handler(self):
202+
con = sqlite.connect(":memory:")
203+
def bad_progress():
204+
1 / 0
205+
con.set_progress_handler(bad_progress, 1)
206+
with self.assertRaises(sqlite.OperationalError):
207+
con.execute("""
208+
create table foo(a, b)
209+
""")
210+
211+
@with_tracebacks(['__bool__', 'ZeroDivisionError'])
212+
def test_error_in_progress_handler_result(self):
213+
con = sqlite.connect(":memory:")
214+
class BadBool:
215+
def __bool__(self):
216+
1 / 0
217+
def bad_progress():
218+
return BadBool()
219+
con.set_progress_handler(bad_progress, 1)
220+
with self.assertRaises(sqlite.OperationalError):
221+
con.execute("""
222+
create table foo(a, b)
223+
""")
224+
225+
201226
class TraceCallbackTests(unittest.TestCase):
202227
def test_trace_callback_used(self):
203228
"""

Lib/sqlite3/test/regression.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# 3. This notice may not be removed or altered from any source distribution.
2222

2323
import datetime
24+
import sys
2425
import unittest
2526
import sqlite3 as sqlite
2627
import weakref
@@ -273,7 +274,7 @@ def test_connection_call(self):
273274
Call a connection with a non-string SQL request: check error handling
274275
of the statement constructor.
275276
"""
276-
self.assertRaises(TypeError, self.con, 1)
277+
self.assertRaises(TypeError, self.con, b"select 1")
277278

278279
def test_collation(self):
279280
def collation_cb(a, b):
@@ -344,6 +345,26 @@ def test_null_character(self):
344345
self.assertRaises(ValueError, cur.execute, " \0select 2")
345346
self.assertRaises(ValueError, cur.execute, "select 2\0")
346347

348+
def test_surrogates(self):
349+
con = sqlite.connect(":memory:")
350+
self.assertRaises(UnicodeEncodeError, con, "select '\ud8ff'")
351+
self.assertRaises(UnicodeEncodeError, con, "select '\udcff'")
352+
cur = con.cursor()
353+
self.assertRaises(UnicodeEncodeError, cur.execute, "select '\ud8ff'")
354+
self.assertRaises(UnicodeEncodeError, cur.execute, "select '\udcff'")
355+
356+
@unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform')
357+
@support.bigmemtest(size=2**31, memuse=4, dry_run=False)
358+
def test_large_sql(self, maxsize):
359+
# Test two cases: size+1 > INT_MAX and size+1 <= INT_MAX.
360+
for size in (2**31, 2**31-2):
361+
con = sqlite.connect(":memory:")
362+
sql = "select 1".ljust(size)
363+
self.assertRaises(sqlite.DataError, con, sql)
364+
cur = con.cursor()
365+
self.assertRaises(sqlite.DataError, cur.execute, sql)
366+
del sql
367+
347368
def test_commit_cursor_reset(self):
348369
"""
349370
Connection.commit() did reset cursors, which made sqlite3

Lib/sqlite3/test/types.py

+50-2
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@
2323
import datetime
2424
import unittest
2525
import sqlite3 as sqlite
26+
import sys
2627
try:
2728
import zlib
2829
except ImportError:
2930
zlib = None
3031

32+
from test import support
33+
3134

3235
class SqliteTypeTests(unittest.TestCase):
3336
def setUp(self):
@@ -45,14 +48,20 @@ def test_string(self):
4548
row = self.cur.fetchone()
4649
self.assertEqual(row[0], "Österreich")
4750

51+
def test_string_with_null_character(self):
52+
self.cur.execute("insert into test(s) values (?)", ("a\0b",))
53+
self.cur.execute("select s from test")
54+
row = self.cur.fetchone()
55+
self.assertEqual(row[0], "a\0b")
56+
4857
def test_small_int(self):
4958
self.cur.execute("insert into test(i) values (?)", (42,))
5059
self.cur.execute("select i from test")
5160
row = self.cur.fetchone()
5261
self.assertEqual(row[0], 42)
5362

5463
def test_large_int(self):
55-
num = 2**40
64+
num = 123456789123456789
5665
self.cur.execute("insert into test(i) values (?)", (num,))
5766
self.cur.execute("select i from test")
5867
row = self.cur.fetchone()
@@ -78,6 +87,45 @@ def test_unicode_execute(self):
7887
row = self.cur.fetchone()
7988
self.assertEqual(row[0], "Österreich")
8089

90+
def test_too_large_int(self):
91+
for value in 2**63, -2**63-1, 2**64:
92+
with self.assertRaises(OverflowError):
93+
self.cur.execute("insert into test(i) values (?)", (value,))
94+
self.cur.execute("select i from test")
95+
row = self.cur.fetchone()
96+
self.assertIsNone(row)
97+
98+
def test_string_with_surrogates(self):
99+
for value in 0xd8ff, 0xdcff:
100+
with self.assertRaises(UnicodeEncodeError):
101+
self.cur.execute("insert into test(s) values (?)", (chr(value),))
102+
self.cur.execute("select s from test")
103+
row = self.cur.fetchone()
104+
self.assertIsNone(row)
105+
106+
@unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform')
107+
@support.bigmemtest(size=2**31, memuse=4, dry_run=False)
108+
def test_too_large_string(self, maxsize):
109+
with self.assertRaises(sqlite.InterfaceError):
110+
self.cur.execute("insert into test(s) values (?)", ('x'*(2**31-1),))
111+
with self.assertRaises(OverflowError):
112+
self.cur.execute("insert into test(s) values (?)", ('x'*(2**31),))
113+
self.cur.execute("select 1 from test")
114+
row = self.cur.fetchone()
115+
self.assertIsNone(row)
116+
117+
@unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform')
118+
@support.bigmemtest(size=2**31, memuse=3, dry_run=False)
119+
def test_too_large_blob(self, maxsize):
120+
with self.assertRaises(sqlite.InterfaceError):
121+
self.cur.execute("insert into test(s) values (?)", (b'x'*(2**31-1),))
122+
with self.assertRaises(OverflowError):
123+
self.cur.execute("insert into test(s) values (?)", (b'x'*(2**31),))
124+
self.cur.execute("select 1 from test")
125+
row = self.cur.fetchone()
126+
self.assertIsNone(row)
127+
128+
81129
class DeclTypesTests(unittest.TestCase):
82130
class Foo:
83131
def __init__(self, _val):
@@ -163,7 +211,7 @@ def test_small_int(self):
163211

164212
def test_large_int(self):
165213
# default
166-
num = 2**40
214+
num = 123456789123456789
167215
self.cur.execute("insert into test(i) values (?)", (num,))
168216
self.cur.execute("select i from test")
169217
row = self.cur.fetchone()

Lib/sqlite3/test/userfunctions.py

+37-11
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,37 @@
3333
from test.support import bigmemtest
3434

3535

36-
def with_tracebacks(strings):
36+
def with_tracebacks(strings, traceback=True):
3737
"""Convenience decorator for testing callback tracebacks."""
38-
strings.append('Traceback')
38+
if traceback:
39+
strings.append('Traceback')
3940

4041
def decorator(func):
4142
@functools.wraps(func)
4243
def wrapper(self, *args, **kwargs):
4344
# First, run the test with traceback enabled.
44-
sqlite.enable_callback_tracebacks(True)
45-
buf = io.StringIO()
46-
with contextlib.redirect_stderr(buf):
45+
with check_tracebacks(self, strings):
4746
func(self, *args, **kwargs)
48-
tb = buf.getvalue()
49-
for s in strings:
50-
self.assertIn(s, tb)
5147

5248
# Then run the test with traceback disabled.
53-
sqlite.enable_callback_tracebacks(False)
5449
func(self, *args, **kwargs)
5550
return wrapper
5651
return decorator
5752

53+
@contextlib.contextmanager
54+
def check_tracebacks(self, strings):
55+
"""Convenience context manager for testing callback tracebacks."""
56+
sqlite.enable_callback_tracebacks(True)
57+
try:
58+
buf = io.StringIO()
59+
with contextlib.redirect_stderr(buf):
60+
yield
61+
tb = buf.getvalue()
62+
for s in strings:
63+
self.assertIn(s, tb)
64+
finally:
65+
sqlite.enable_callback_tracebacks(False)
66+
5867
def func_returntext():
5968
return "foo"
6069
def func_returntextwithnull():
@@ -408,9 +417,26 @@ def md5sum(t):
408417
del x,y
409418
gc.collect()
410419

420+
def test_func_return_too_large_int(self):
421+
cur = self.con.cursor()
422+
for value in 2**63, -2**63-1, 2**64:
423+
self.con.create_function("largeint", 0, lambda value=value: value)
424+
with check_tracebacks(self, ['OverflowError']):
425+
with self.assertRaises(sqlite.DataError):
426+
cur.execute("select largeint()")
427+
428+
def test_func_return_text_with_surrogates(self):
429+
cur = self.con.cursor()
430+
self.con.create_function("pychr", 1, chr)
431+
for value in 0xd8ff, 0xdcff:
432+
with check_tracebacks(self,
433+
['UnicodeEncodeError', 'surrogates not allowed']):
434+
with self.assertRaises(sqlite.OperationalError):
435+
cur.execute("select pychr(?)", (value,))
436+
411437
@unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform')
412438
@bigmemtest(size=2**31, memuse=3, dry_run=False)
413-
def test_large_text(self, size):
439+
def test_func_return_too_large_text(self, size):
414440
cur = self.con.cursor()
415441
for size in 2**31-1, 2**31:
416442
self.con.create_function("largetext", 0, lambda size=size: "b" * size)
@@ -419,7 +445,7 @@ def test_large_text(self, size):
419445

420446
@unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform')
421447
@bigmemtest(size=2**31, memuse=2, dry_run=False)
422-
def test_large_blob(self, size):
448+
def test_func_return_too_large_blob(self, size):
423449
cur = self.con.cursor()
424450
for size in 2**31-1, 2**31:
425451
self.con.create_function("largeblob", 0, lambda size=size: b"b" * size)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Improve error handling in :mod:`sqlite3` and raise more accurate exceptions.
2+
3+
* :exc:`MemoryError` is now raised instead of :exc:`sqlite3.Warning` when memory is not enough for encoding a statement to UTF-8 in ``Connection.__call__()`` and ``Cursor.execute()``.
4+
* :exc:`UnicodEncodeError` is now raised instead of :exc:`sqlite3.Warning` when the statement contains surrogate characters in ``Connection.__call__()`` and ``Cursor.execute()``.
5+
* :exc:`TypeError` is now raised instead of :exc:`ValueError` for non-string script argument in ``Cursor.executescript()``.
6+
* :exc:`ValueError` is now raised for script containing the null character instead of truncating it in ``Cursor.executescript()``.
7+
* Correctly handle exceptions raised when getting boolean value of the result of the progress handler.
8+
* Add many tests covering different corner cases.

Modules/_sqlite/clinic/cursor.c.h

+30-1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,35 @@ PyDoc_STRVAR(pysqlite_cursor_executescript__doc__,
119119
#define PYSQLITE_CURSOR_EXECUTESCRIPT_METHODDEF \
120120
{"executescript", (PyCFunction)pysqlite_cursor_executescript, METH_O, pysqlite_cursor_executescript__doc__},
121121

122+
static PyObject *
123+
pysqlite_cursor_executescript_impl(pysqlite_Cursor *self,
124+
const char *sql_script);
125+
126+
static PyObject *
127+
pysqlite_cursor_executescript(pysqlite_Cursor *self, PyObject *arg)
128+
{
129+
PyObject *return_value = NULL;
130+
const char *sql_script;
131+
132+
if (!PyUnicode_Check(arg)) {
133+
_PyArg_BadArgument("executescript", "argument", "str", arg);
134+
goto exit;
135+
}
136+
Py_ssize_t sql_script_length;
137+
sql_script = PyUnicode_AsUTF8AndSize(arg, &sql_script_length);
138+
if (sql_script == NULL) {
139+
goto exit;
140+
}
141+
if (strlen(sql_script) != (size_t)sql_script_length) {
142+
PyErr_SetString(PyExc_ValueError, "embedded null character");
143+
goto exit;
144+
}
145+
return_value = pysqlite_cursor_executescript_impl(self, sql_script);
146+
147+
exit:
148+
return return_value;
149+
}
150+
122151
PyDoc_STRVAR(pysqlite_cursor_fetchone__doc__,
123152
"fetchone($self, /)\n"
124153
"--\n"
@@ -270,4 +299,4 @@ pysqlite_cursor_close(pysqlite_Cursor *self, PyTypeObject *cls, PyObject *const
270299
exit:
271300
return return_value;
272301
}
273-
/*[clinic end generated code: output=7b216aba2439f5cf input=a9049054013a1b77]*/
302+
/*[clinic end generated code: output=ace31a7481aa3f41 input=a9049054013a1b77]*/

0 commit comments

Comments
 (0)