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
22 changes: 22 additions & 0 deletions Lib/test/test_sqlite3/test_userfunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,28 @@ def test_win_error_on_create(self):
with self.assertRaisesRegex(sqlite.ProgrammingError, "not -100"):
self.con.create_window_function("shouldfail", -100, WindowSumInt)

def test_win_value_on_empty_frame(self):
# gh-153800: when a window frame is empty for a given output row,
# SQLite may invoke the value() (or inverse()) callback before step()
# has ever run for that aggregate instance. This must not crash.
class WindowConst:
def step(self, value): pass
def inverse(self, value): pass
def value(self): return 42
def finalize(self): return 42

self.con.create_window_function("winconst", 1, WindowConst)
# The frame "1 preceding and 1 preceding" is empty for the first row,
# so value() is called before any step() call for that row.
self.cur.execute("""
select x, winconst(y) over (
order by x rows between 1 preceding and 1 preceding
) from test order by x
""")
self.assertEqual(self.cur.fetchall(),
[("a", 42), ("b", 42), ("c", 42),
("d", 42), ("e", 42)])

@with_tracebacks(BadWindow)
def test_win_exception_in_method(self):
for meth in "__init__", "step", "value", "inverse":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix a crash in :mod:`sqlite3` window functions. The ``value()`` and
``inverse()`` aggregate callbacks no longer dereference a ``NULL`` pointer
when SQLite invokes them for an empty window frame before ``step()`` has run
for that aggregate instance; the aggregate instance is now created lazily, as
it already was for ``step()``.
70 changes: 47 additions & 23 deletions Modules/_sqlite/connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -978,35 +978,56 @@ func_callback(sqlite3_context *context, int argc, sqlite3_value **argv)
PyGILState_Release(threadstate);
}

/* Fetch the aggregate instance for a window/aggregate callback, lazily
* creating it on first use. SQLite may invoke the value() or inverse()
* callback before step() has run for a given aggregate instance -- e.g. when
* the window frame is empty for an output row -- in which case the aggregate
* context is still zero-initialised (gh-153800). Returns a borrowed reference
* owned by the aggregate context, or NULL with a SQLite error set on failure.
*/
static PyObject *
get_or_create_aggregate_instance(sqlite3_context *context,
callback_context *ctx)
{
PyObject **aggregate_instance;

aggregate_instance = (PyObject **)sqlite3_aggregate_context(context,
sizeof(PyObject *));
if (aggregate_instance == NULL) {
(void)PyErr_NoMemory();
set_sqlite_error(context, "unable to allocate SQLite aggregate context");
return NULL;
}
if (*aggregate_instance == NULL) {
*aggregate_instance = PyObject_CallNoArgs(ctx->callable);
if (*aggregate_instance == NULL) {
set_sqlite_error(context,
"user-defined aggregate's '__init__' method raised error");
return NULL;
}
}
return *aggregate_instance;
}

static void
step_callback(sqlite3_context *context, int argc, sqlite3_value **params)
{
PyGILState_STATE threadstate = PyGILState_Ensure();

PyObject* args;
PyObject* function_result = NULL;
PyObject** aggregate_instance;
PyObject* aggregate_instance;
PyObject* stepmethod = NULL;

callback_context *ctx = (callback_context *)sqlite3_user_data(context);
incref_callback_context(ctx);

aggregate_instance = (PyObject**)sqlite3_aggregate_context(context, sizeof(PyObject*));
aggregate_instance = get_or_create_aggregate_instance(context, ctx);
if (aggregate_instance == NULL) {
(void)PyErr_NoMemory();
set_sqlite_error(context, "unable to allocate SQLite aggregate context");
goto error;
}
if (*aggregate_instance == NULL) {
*aggregate_instance = PyObject_CallNoArgs(ctx->callable);
if (!*aggregate_instance) {
set_sqlite_error(context,
"user-defined aggregate's '__init__' method raised error");
goto error;
}
}

stepmethod = PyObject_GetAttr(*aggregate_instance, ctx->state->str_step);
stepmethod = PyObject_GetAttr(aggregate_instance, ctx->state->str_step);
if (!stepmethod) {
set_sqlite_error(context,
"user-defined aggregate's 'step' method not defined");
Expand Down Expand Up @@ -1249,12 +1270,13 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params)
callback_context *ctx = (callback_context *)sqlite3_user_data(context);
incref_callback_context(ctx);

int size = sizeof(PyObject *);
PyObject **cls = (PyObject **)sqlite3_aggregate_context(context, size);
assert(cls != NULL);
assert(*cls != NULL);
PyObject *method = NULL;
PyObject *aggregate_instance = get_or_create_aggregate_instance(context, ctx);
if (aggregate_instance == NULL) {
goto exit;
}

PyObject *method = PyObject_GetAttr(*cls, ctx->state->str_inverse);
method = PyObject_GetAttr(aggregate_instance, ctx->state->str_inverse);
if (method == NULL) {
set_sqlite_error(context,
"user-defined aggregate's 'inverse' method not defined");
Expand Down Expand Up @@ -1298,12 +1320,14 @@ value_callback(sqlite3_context *context)
callback_context *ctx = (callback_context *)sqlite3_user_data(context);
incref_callback_context(ctx);

int size = sizeof(PyObject *);
PyObject **cls = (PyObject **)sqlite3_aggregate_context(context, size);
assert(cls != NULL);
assert(*cls != NULL);
PyObject *aggregate_instance = get_or_create_aggregate_instance(context, ctx);
if (aggregate_instance == NULL) {
decref_callback_context(ctx);
PyGILState_Release(gilstate);
return;
}

PyObject *res = PyObject_CallMethodNoArgs(*cls, ctx->state->str_value);
PyObject *res = PyObject_CallMethodNoArgs(aggregate_instance, ctx->state->str_value);
decref_callback_context(ctx);

if (res == NULL) {
Expand Down
Loading