Skip to content

Commit 57e428a

Browse files
Erlend Egeberg Aaslandasvetlov
Erlend Egeberg Aasland
authored andcommitted
bpo-45138: Expand traced SQL statements in sqlite3 trace callback (GH-28240)
1 parent 1762d16 commit 57e428a

File tree

5 files changed

+108
-15
lines changed

5 files changed

+108
-15
lines changed

Doc/library/sqlite3.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,9 @@ Connection Objects
560560

561561
Passing :const:`None` as *trace_callback* will disable the trace callback.
562562

563+
For SQLite 3.14.0 and newer, bound parameters are expanded in the passed
564+
statement string.
565+
563566
.. note::
564567
Exceptions raised in the trace callback are not propagated. As a
565568
development and debugging aid, use
@@ -568,6 +571,9 @@ Connection Objects
568571

569572
.. versionadded:: 3.3
570573

574+
.. versionchanged:: 3.11
575+
Added support for expanded SQL statements.
576+
571577

572578
.. method:: enable_load_extension(enabled)
573579

Doc/whatsnew/3.11.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ sqlite3
322322
Instead we leave it to the SQLite library to handle these cases.
323323
(Contributed by Erlend E. Aasland in :issue:`44092`.)
324324

325+
* For SQLite 3.14.0 and newer, bound parameters are expanded in the statement
326+
string passed to the trace callback. See :meth:`~sqlite3.Connection.set_trace_callback`.
327+
(Contributed by Erlend E. Aasland in :issue:`45138`.)
328+
325329

326330
sys
327331
---

Lib/test/test_sqlite3/test_hooks.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@
2020
# misrepresented as being the original software.
2121
# 3. This notice may not be removed or altered from any source distribution.
2222

23-
import unittest
23+
import contextlib
2424
import sqlite3 as sqlite
25+
import unittest
2526

2627
from test.support.os_helper import TESTFN, unlink
28+
29+
from test.test_sqlite3.test_dbapi import memory_database, cx_limit
2730
from test.test_sqlite3.test_userfunctions import with_tracebacks
2831

32+
2933
class CollationTests(unittest.TestCase):
3034
def test_create_collation_not_string(self):
3135
con = sqlite.connect(":memory:")
@@ -224,6 +228,16 @@ def bad_progress():
224228

225229

226230
class TraceCallbackTests(unittest.TestCase):
231+
@contextlib.contextmanager
232+
def check_stmt_trace(self, cx, expected):
233+
try:
234+
traced = []
235+
cx.set_trace_callback(lambda stmt: traced.append(stmt))
236+
yield
237+
finally:
238+
self.assertEqual(traced, expected)
239+
cx.set_trace_callback(None)
240+
227241
def test_trace_callback_used(self):
228242
"""
229243
Test that the trace callback is invoked once it is set.
@@ -289,6 +303,51 @@ def trace(statement):
289303
con2.close()
290304
self.assertEqual(traced_statements, queries)
291305

306+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 14, 0),
307+
"Requires SQLite 3.14.0 or newer")
308+
def test_trace_expanded_sql(self):
309+
expected = [
310+
"create table t(t)",
311+
"BEGIN ",
312+
"insert into t values(0)",
313+
"insert into t values(1)",
314+
"insert into t values(2)",
315+
"COMMIT",
316+
]
317+
with memory_database() as cx, self.check_stmt_trace(cx, expected):
318+
with cx:
319+
cx.execute("create table t(t)")
320+
cx.executemany("insert into t values(?)", ((v,) for v in range(3)))
321+
322+
@with_tracebacks(
323+
sqlite.DataError,
324+
regex="Expanded SQL string exceeds the maximum string length"
325+
)
326+
def test_trace_too_much_expanded_sql(self):
327+
# If the expanded string is too large, we'll fall back to the
328+
# unexpanded SQL statement. The resulting string length is limited by
329+
# SQLITE_LIMIT_LENGTH.
330+
template = "select 'b' as \"a\" from sqlite_master where \"a\"="
331+
category = sqlite.SQLITE_LIMIT_LENGTH
332+
with memory_database() as cx, cx_limit(cx, category=category) as lim:
333+
nextra = lim - (len(template) + 2) - 1
334+
ok_param = "a" * nextra
335+
bad_param = "a" * (nextra + 1)
336+
337+
unexpanded_query = template + "?"
338+
with self.check_stmt_trace(cx, [unexpanded_query]):
339+
cx.execute(unexpanded_query, (bad_param,))
340+
341+
expanded_query = f"{template}'{ok_param}'"
342+
with self.check_stmt_trace(cx, [expanded_query]):
343+
cx.execute(unexpanded_query, (ok_param,))
344+
345+
@with_tracebacks(ZeroDivisionError, regex="division by zero")
346+
def test_trace_bad_handler(self):
347+
with memory_database() as cx:
348+
cx.set_trace_callback(lambda stmt: 5/0)
349+
cx.execute("select 1")
350+
292351

293352
if __name__ == "__main__":
294353
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
For SQLite 3.14.0 and newer, bound parameters are expanded in the statement
2+
string passed to the :mod:`sqlite3` trace callback. Patch by Erlend E.
3+
Aasland.

Modules/_sqlite/connection.c

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,11 +1079,10 @@ progress_callback(void *ctx)
10791079
* to ensure future compatibility.
10801080
*/
10811081
static int
1082-
trace_callback(unsigned int type, void *ctx, void *prepared_statement,
1083-
void *statement_string)
1082+
trace_callback(unsigned int type, void *ctx, void *stmt, void *sql)
10841083
#else
10851084
static void
1086-
trace_callback(void *ctx, const char *statement_string)
1085+
trace_callback(void *ctx, const char *sql)
10871086
#endif
10881087
{
10891088
#ifdef HAVE_TRACE_V2
@@ -1094,24 +1093,46 @@ trace_callback(void *ctx, const char *statement_string)
10941093

10951094
PyGILState_STATE gilstate = PyGILState_Ensure();
10961095

1097-
PyObject *py_statement = NULL;
1098-
PyObject *ret = NULL;
1099-
py_statement = PyUnicode_DecodeUTF8(statement_string,
1100-
strlen(statement_string), "replace");
11011096
assert(ctx != NULL);
1097+
PyObject *py_statement = NULL;
1098+
#ifdef HAVE_TRACE_V2
1099+
assert(stmt != NULL);
1100+
const char *expanded_sql = sqlite3_expanded_sql((sqlite3_stmt *)stmt);
1101+
if (expanded_sql == NULL) {
1102+
sqlite3 *db = sqlite3_db_handle((sqlite3_stmt *)stmt);
1103+
if (sqlite3_errcode(db) == SQLITE_NOMEM) {
1104+
(void)PyErr_NoMemory();
1105+
goto exit;
1106+
}
1107+
1108+
pysqlite_state *state = ((callback_context *)ctx)->state;
1109+
assert(state != NULL);
1110+
PyErr_SetString(state->DataError,
1111+
"Expanded SQL string exceeds the maximum string "
1112+
"length");
1113+
print_or_clear_traceback((callback_context *)ctx);
1114+
1115+
// Fall back to unexpanded sql
1116+
py_statement = PyUnicode_FromString((const char *)sql);
1117+
}
1118+
else {
1119+
py_statement = PyUnicode_FromString(expanded_sql);
1120+
sqlite3_free((void *)expanded_sql);
1121+
}
1122+
#else
1123+
py_statement = PyUnicode_FromString(sql);
1124+
#endif
11021125
if (py_statement) {
11031126
PyObject *callable = ((callback_context *)ctx)->callable;
1104-
ret = PyObject_CallOneArg(callable, py_statement);
1127+
PyObject *ret = PyObject_CallOneArg(callable, py_statement);
11051128
Py_DECREF(py_statement);
1129+
Py_XDECREF(ret);
11061130
}
11071131

1108-
if (ret) {
1109-
Py_DECREF(ret);
1110-
}
1111-
else {
1112-
print_or_clear_traceback(ctx);
1132+
exit:
1133+
if (PyErr_Occurred()) {
1134+
print_or_clear_traceback((callback_context *)ctx);
11131135
}
1114-
11151136
PyGILState_Release(gilstate);
11161137
#ifdef HAVE_TRACE_V2
11171138
return 0;

0 commit comments

Comments
 (0)