From bbef5a46f2272c44c5fa46c77ff5a376b2228708 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 24 May 2020 22:50:36 +0200 Subject: [PATCH 01/40] Add support for sqlite3 aggregate window functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://docs.python.org/3/library/exceptions.html#NotImplementedError: It should not be used to indicate that an operator or method is not meant to be supported at all – in that case either leave the operator / method undefined or, if a subclass, set it to None. --- Doc/includes/sqlite3/sumintwindow.py | 46 +++++ Doc/library/sqlite3.rst | 46 +++++ Lib/sqlite3/test/userfunctions.py | 80 ++++++++ Modules/_sqlite/clinic/connection.c.h | 109 ++++++++++- Modules/_sqlite/connection.c | 263 ++++++++++++++++++++++++-- 5 files changed, 531 insertions(+), 13 deletions(-) create mode 100644 Doc/includes/sqlite3/sumintwindow.py diff --git a/Doc/includes/sqlite3/sumintwindow.py b/Doc/includes/sqlite3/sumintwindow.py new file mode 100644 index 00000000000000..f68ade87e51899 --- /dev/null +++ b/Doc/includes/sqlite3/sumintwindow.py @@ -0,0 +1,46 @@ +# Example taken from https://www.sqlite.org/windowfunctions.html#udfwinfunc +import sqlite3 + + +class WindowSumInt: + def __init__(self): + self.count = 0 + + def step(self, value): + """This method is invoked to add a row to the current window.""" + self.count += value + + def value(self): + """This method is invoked to return the current value of the aggregate.""" + return self.count + + def inverse(self, value): + """This method is invoked to remove a row from the current window.""" + self.count -= value + + def finalize(self): + """This method is invoked to return the current value of the aggregate. + + Any clean-up actions should be placed here. + """ + return self.count + + +con = sqlite3.connect(":memory:") +cur = con.execute("create table test(x, y)") +values = [ + ("a", 4), + ("b", 5), + ("c", 3), + ("d", 8), + ("e", 1), +] +cur.executemany("insert into test values(?, ?)", values) +con.create_window_function("sumint", 1, WindowSumInt) +cur.execute(""" + select x, sumint(y) over ( + order by x rows between 1 preceding and 1 following + ) as sum_y + from test order by x +""") +print(cur.fetchall()) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 6bdf4ed0d81bcc..47549eac0fb622 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -379,6 +379,52 @@ Connection Objects .. literalinclude:: ../includes/sqlite3/mysumaggr.py + .. method:: create_window_function(name, num_params, aggregate_class, /, *, + deterministic=False, innocuous=False, + directonly=False) + + Creates a user-defined aggregate window function. Aggregate window + functions are supported by SQLite 3.25.0 and higher. + :exc:`NotSupportedError` will be raised if used with older + versions. + + The aggregate class must implement ``step`` and ``inverse`` + methods, which accept the number of parameters *num_params* (if + *num_params* is -1, the function may take any number of arguments), + and ``finalize`` and ``value`` methods which return the final and + the current result of the aggregate. + + The ``finalize`` and ``value`` methods can return any of the types + supported by SQLite: :class:`bytes`, :class:`str`, :class:`int`, + :class:`float` and :const:`None`. + + If *deterministic* is :const:`True`, the created function is marked as + `deterministic `_, which + allows SQLite to perform additional optimizations. This flag is + supported by SQLite 3.8.3 or higher. :exc:`NotSupportedError` will + be raised if used with older versions. + + If *innocuous* is :const:`True`, the created function is marked as + `innocuous `_, which + indicates to SQLite that it is unlikely to cause problems, even if + misused. This flag is supported by SQLite 3.31.0 or higher. + :exc:`NotSupportedError` will be raised if used with older + versions. + + If *directonly* is :const:`True`, the created function is marked as + `directonly `_, which + means that it may only be invoked from top-level SQL. This flag + is an SQLite security feature that is recommended for all + user-defined SQL functions. This flag is supported by SQLite + 3.30.0 or higher. :exc:`NotSupportedError` will be raised if used + with older versions. + + .. versionadded:: 3.10 + + Example: + + .. literalinclude:: ../includes/sqlite3/sumintwindow.py + .. method:: create_collation(name, callable) Creates a collation with the specified *name* and *callable*. The callable will diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index 148d9f596a91c8..3a34e4bde97c6c 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -321,6 +321,85 @@ def test_func_deterministic_keyword_only(self): self.con.create_function("deterministic", 0, int, True) +class WindowSumInt: + def __init__(self): + self.count = 0 + + def step(self, value): + self.count += value + + def value(self): + return self.count + + def inverse(self, value): + self.count -= value + + def finalize(self): + return self.count + + +@unittest.skipIf(sqlite.sqlite_version_info < (3, 25, 0), + "Requires SQLite 3.25.0 or newer") +class WindowFunctionTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + + # Test case taken from https://www.sqlite.org/windowfunctions.html#udfwinfunc + values = [ + ("a", 4), + ("b", 5), + ("c", 3), + ("d", 8), + ("e", 1), + ] + self.cur = self.con.execute("create table test(x, y)") + self.cur.executemany("insert into test values(?, ?)", values) + self.expected = [ + ("a", 9), + ("b", 12), + ("c", 16), + ("d", 12), + ("e", 9), + ] + self.query = (""" + select x, %s(y) over ( + order by x rows between 1 preceding and 1 following + ) as sum_y + from test order by x + """) + self.con.create_window_function("sumint", 1, WindowSumInt) + + def test_sum_int(self): + self.cur.execute(self.query % "sumint") + self.assertEqual(self.cur.fetchall(), self.expected) + + def test_error_on_create(self): + with self.assertRaises(sqlite.OperationalError): + self.con.create_window_function("shouldfail", -100, WindowSumInt) + + def test_exception_in_method(self): + for meth in ["step", "value", "inverse"]: + with unittest.mock.patch.object(WindowSumInt, meth, + side_effect=Exception): + func = f"exc_{meth}" + self.con.create_window_function(func, 1, WindowSumInt) + with self.assertRaises(sqlite.OperationalError): + self.cur.execute(self.query % func) + ret = self.cur.fetchall() + + def test_clear_function(self): + self.con.create_window_function("sumint", 1, None) + with self.assertRaises(sqlite.OperationalError): + self.cur.execute(self.query % "sumint") + + def test_redefine_function(self): + class Redefined(WindowSumInt): + pass + self.con.create_window_function("sumint", 1, Redefined) + self.cur.execute(self.query % "sumint") + self.assertEqual(self.cur.fetchall(), self.expected) + + class AggregateTests(unittest.TestCase): def setUp(self): self.con = sqlite.connect(":memory:") @@ -510,6 +589,7 @@ def suite(): AuthorizerRaiseExceptionTests, AuthorizerTests, FunctionTests, + WindowFunctionTests, ] return unittest.TestSuite( [unittest.TestLoader().loadTestsFromTestCase(t) for t in tests] diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h index f231ecc2ae78be..d3c0f86c8b04dc 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -156,6 +156,109 @@ pysqlite_connection_create_function(pysqlite_Connection *self, PyObject *const * return return_value; } +#if defined(HAVE_WINDOW_FUNCTIONS) + +PyDoc_STRVAR(pysqlite_connection_create_window_function__doc__, +"create_window_function($self, name, num_params, aggregate_class, /, *,\n" +" deterministic=False, directonly=False,\n" +" innocuous=False)\n" +"--\n" +"\n" +"Creates or redefines an aggregate window function. Non-standard.\n" +"\n" +" name\n" +" The name of the SQL aggregate window function to be created or\n" +" redefined.\n" +" num_params\n" +" The number of arguments that the SQL aggregate window function\n" +" takes.\n" +" aggregate_class\n" +" A class with step(), finalize(), value(), and inverse() methods.\n" +" Set to None to clear the window function."); + +#define PYSQLITE_CONNECTION_CREATE_WINDOW_FUNCTION_METHODDEF \ + {"create_window_function", (PyCFunction)(void(*)(void))pysqlite_connection_create_window_function, METH_FASTCALL|METH_KEYWORDS, pysqlite_connection_create_window_function__doc__}, + +static PyObject * +pysqlite_connection_create_window_function_impl(pysqlite_Connection *self, + const char *name, + int num_params, + PyObject *aggregate_class, + int deterministic, + int directonly, + int innocuous); + +static PyObject * +pysqlite_connection_create_window_function(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + static const char * const _keywords[] = {"", "", "", "deterministic", "directonly", "innocuous", NULL}; + static _PyArg_Parser _parser = {NULL, _keywords, "create_window_function", 0}; + PyObject *argsbuf[6]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 3; + const char *name; + int num_params; + PyObject *aggregate_class; + int deterministic = 0; + int directonly = 0; + int innocuous = 0; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 3, 3, 0, argsbuf); + if (!args) { + goto exit; + } + if (!PyUnicode_Check(args[0])) { + _PyArg_BadArgument("create_window_function", "argument 1", "str", args[0]); + goto exit; + } + Py_ssize_t name_length; + name = PyUnicode_AsUTF8AndSize(args[0], &name_length); + if (name == NULL) { + goto exit; + } + if (strlen(name) != (size_t)name_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + num_params = _PyLong_AsInt(args[1]); + if (num_params == -1 && PyErr_Occurred()) { + goto exit; + } + aggregate_class = args[2]; + if (!noptargs) { + goto skip_optional_kwonly; + } + if (args[3]) { + deterministic = PyObject_IsTrue(args[3]); + if (deterministic < 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (args[4]) { + directonly = PyObject_IsTrue(args[4]); + if (directonly < 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + innocuous = PyObject_IsTrue(args[5]); + if (innocuous < 0) { + goto exit; + } +skip_optional_kwonly: + return_value = pysqlite_connection_create_window_function_impl(self, name, num_params, aggregate_class, deterministic, directonly, innocuous); + +exit: + return return_value; +} + +#endif /* defined(HAVE_WINDOW_FUNCTIONS) */ + PyDoc_STRVAR(pysqlite_connection_create_aggregate__doc__, "create_aggregate($self, /, name, n_arg, aggregate_class)\n" "--\n" @@ -703,6 +806,10 @@ pysqlite_connection_exit(pysqlite_Connection *self, PyObject *const *args, Py_ss return return_value; } +#ifndef PYSQLITE_CONNECTION_CREATE_WINDOW_FUNCTION_METHODDEF + #define PYSQLITE_CONNECTION_CREATE_WINDOW_FUNCTION_METHODDEF +#endif /* !defined(PYSQLITE_CONNECTION_CREATE_WINDOW_FUNCTION_METHODDEF) */ + #ifndef PYSQLITE_CONNECTION_ENABLE_LOAD_EXTENSION_METHODDEF #define PYSQLITE_CONNECTION_ENABLE_LOAD_EXTENSION_METHODDEF #endif /* !defined(PYSQLITE_CONNECTION_ENABLE_LOAD_EXTENSION_METHODDEF) */ @@ -710,4 +817,4 @@ pysqlite_connection_exit(pysqlite_Connection *self, PyObject *const *args, Py_ss #ifndef PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF #define PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF #endif /* !defined(PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF) */ -/*[clinic end generated code: output=c1bf09db3bcd0105 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=54e136772a234bed input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 93365495f58856..6cd7539a2602be 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -37,6 +37,10 @@ #define HAVE_TRACE_V2 #endif +#if SQLITE_VERSION_NUMBER >= 3025000 +#define HAVE_WINDOW_FUNCTIONS +#endif + #include "clinic/connection.c.h" /*[clinic input] module _sqlite3 @@ -821,6 +825,69 @@ static void _destructor(void* args) Py_DECREF((PyObject*)args); } +#define NOT_SUPPORTED(name, ver) \ +do { \ + if (is_set) { \ + PyErr_SetString(pysqlite_NotSupportedError, \ + name "=True requires SQLite " ver "or higher"); \ + return -1; \ + } \ +} while (0) + +#define SET_FLAG(fl) \ +do { \ + assert(flags != NULL); \ + if (is_set) { \ + *flags |= fl; \ + } \ +} while (0) + +static int +add_deterministic_flag_if_supported(int *flags, int is_set) +{ +#if SQLITE_VERSION_NUMBER < 3008003 + NOT_SUPPORTED("deterministic", "3.8.3"); +#else + if (sqlite3_libversion_number() < 3008003) { + NOT_SUPPORTED("deterministic", "3.8.3"); + } + SET_FLAG(SQLITE_DETERMINISTIC); +#endif + return 0; +} + +static int +add_innocuous_flag_if_supported(int *flags, int is_set) +{ +#if SQLITE_VERSION_NUMBER < 3031000 + NOT_SUPPORTED("innocuous", "3.31.0"); +#else + if (sqlite3_libversion_number() < 3031000) { + NOT_SUPPORTED("innocuous", "3.31.0"); + } + SET_FLAG(SQLITE_INNOCUOUS); +#endif + return 0; +} + +static int +add_directonly_flag_if_supported(int *flags, int is_set) +{ +#if SQLITE_VERSION_NUMBER < 3030000 + NOT_SUPPORTED("directonly", "3.30.0"); +#else + if (sqlite3_libversion_number() < 3030000) { + NOT_SUPPORTED("directonly", "3.30.0"); + } + SET_FLAG(SQLITE_DIRECTONLY); +#endif + return 0; +} + +#undef SET_FLAG +#undef NOT_SUPPORTED + + /*[clinic input] _sqlite3.Connection.create_function as pysqlite_connection_create_function @@ -846,19 +913,8 @@ pysqlite_connection_create_function_impl(pysqlite_Connection *self, return NULL; } - if (deterministic) { -#if SQLITE_VERSION_NUMBER < 3008003 - PyErr_SetString(pysqlite_NotSupportedError, - "deterministic=True requires SQLite 3.8.3 or higher"); + if (add_deterministic_flag_if_supported(&flags, deterministic) < 0) { return NULL; -#else - if (sqlite3_libversion_number() < 3008003) { - PyErr_SetString(pysqlite_NotSupportedError, - "deterministic=True requires SQLite 3.8.3 or higher"); - return NULL; - } - flags |= SQLITE_DETERMINISTIC; -#endif } rc = sqlite3_create_function_v2(self->db, name, @@ -878,6 +934,188 @@ pysqlite_connection_create_function_impl(pysqlite_Connection *self, Py_RETURN_NONE; } +#ifdef HAVE_WINDOW_FUNCTIONS +/* + * Regarding the 'inverse' aggregate callback: + * This method is only required by window aggregate functions, not + * ordinary aggregate function implementations. It is invoked to remove + * a row from the current window. The function arguments, if any, + * correspond to the row being removed. + */ +static void +inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) +{ + PyGILState_STATE gilstate = PyGILState_Ensure(); + + PyObject **cls = (PyObject **)sqlite3_aggregate_context(context, + sizeof(PyObject *)); + assert(cls != NULL); + assert(*cls != NULL); + PyObject *method = PyObject_GetAttrString(*cls, "inverse"); + if (method == NULL) { + sqlite3_result_error(context, + "user-defined aggregate's 'inverse' method" + " not defined", + -1); + goto error; + } + + PyObject *args = _pysqlite_build_py_params(context, argc, params); + if (args == NULL) { + sqlite3_result_error(context, + "unable to build arguments for user-defined" + " aggregate's 'inverse' method", + -1); + Py_DECREF(method); + goto error; + } + + PyObject *res = PyObject_CallObject(method, args); + Py_DECREF(method); + Py_DECREF(args); + if (res == NULL) { + sqlite3_result_error(context, + "user-defined aggregate's 'inverse' method" + " raised error", + -1); + goto error; + } + Py_DECREF(res); + +error: + if (PyErr_Occurred()) { + if (_pysqlite_enable_callback_tracebacks) { + PyErr_Print(); + } else { + PyErr_Clear(); + } + } + PyGILState_Release(gilstate); +} + +/* + * Regarding the 'value' aggregate callback: + * This method is only required by window aggregate functions, not + * ordinary aggregate function implementations. It is invoked to return + * the current value of the aggregate. + */ +static void +value_callback(sqlite3_context *context) +{ + PyGILState_STATE gilstate = PyGILState_Ensure(); + + PyObject **cls = NULL; // Aggregate class instance. + cls = (PyObject **)sqlite3_aggregate_context(context, sizeof(PyObject *)); + assert(cls != NULL); + assert(*cls != NULL); + _Py_IDENTIFIER(value); + PyObject *res = _PyObject_CallMethodIdNoArgs(*cls, &PyId_value); + if (res == NULL) { + sqlite3_result_error(context, + "user-defined aggregate's 'value' method" + " raised error", + -1); + goto error; + } + + int rc = _pysqlite_set_result(context, res); + Py_DECREF(res); + if (rc != 0) { + sqlite3_result_error(context, + "unable to set result from user-defined" + " aggregate's 'value' method", + -1); + goto error; + } + +error: + if (PyErr_Occurred()) { + if (_pysqlite_enable_callback_tracebacks) { + PyErr_Print(); + } else { + PyErr_Clear(); + } + } + PyGILState_Release(gilstate); +} + +/*[clinic input] +_sqlite3.Connection.create_window_function as pysqlite_connection_create_window_function + + name: str + The name of the SQL aggregate window function to be created or + redefined. + num_params: int + The number of arguments that the SQL aggregate window function + takes. + aggregate_class: object + A class with step(), finalize(), value(), and inverse() methods. + Set to None to clear the window function. + / + * + deterministic: bool = False + directonly: bool = False + innocuous: bool = False + +Creates or redefines an aggregate window function. Non-standard. +[clinic start generated code]*/ + +static PyObject * +pysqlite_connection_create_window_function_impl(pysqlite_Connection *self, + const char *name, + int num_params, + PyObject *aggregate_class, + int deterministic, + int directonly, + int innocuous) +/*[clinic end generated code: output=fbbfad556264ea1e input=877311e540865c3b]*/ +{ + if (sqlite3_libversion_number() < 3025000) { + PyErr_SetString(pysqlite_NotSupportedError, + "create_window_function() requires " + "SQLite 3.25.0 or higher"); + return NULL; + } + + if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) { + return NULL; + } + + int flags = SQLITE_UTF8; + if (add_deterministic_flag_if_supported(&flags, deterministic) < 0) { + return NULL; + } + if (add_directonly_flag_if_supported(&flags, directonly) < 0) { + return NULL; + } + if (add_innocuous_flag_if_supported(&flags, innocuous) < 0) { + return NULL; + } + + int rc; + if (Py_IsNone(aggregate_class)) { + rc = sqlite3_create_window_function(self->db, name, num_params, flags, + 0, 0, 0, 0, 0, 0); + } + else { + rc = sqlite3_create_window_function(self->db, name, num_params, flags, + Py_NewRef(aggregate_class), + &_pysqlite_step_callback, + &_pysqlite_final_callback, + &value_callback, + &inverse_callback, + &_destructor); + } + + if (rc != SQLITE_OK) { + PyErr_SetString(pysqlite_OperationalError, + "Error creating window function"); + return NULL; + } + Py_RETURN_NONE; +} +#endif + /*[clinic input] _sqlite3.Connection.create_aggregate as pysqlite_connection_create_aggregate @@ -1858,6 +2096,7 @@ static PyMethodDef connection_methods[] = { PYSQLITE_CONNECTION_CREATE_AGGREGATE_METHODDEF PYSQLITE_CONNECTION_CREATE_COLLATION_METHODDEF PYSQLITE_CONNECTION_CREATE_FUNCTION_METHODDEF + PYSQLITE_CONNECTION_CREATE_WINDOW_FUNCTION_METHODDEF PYSQLITE_CONNECTION_CURSOR_METHODDEF PYSQLITE_CONNECTION_ENABLE_LOAD_EXTENSION_METHODDEF PYSQLITE_CONNECTION_ENTER_METHODDEF From 93bfb308285741d0b520f944a153d5f7d82de149 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 24 May 2020 23:52:14 +0200 Subject: [PATCH 02/40] Add NEWS entry --- .../NEWS.d/next/Library/2020-05-24-23-52-03.bpo-40617.lycF9q.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2020-05-24-23-52-03.bpo-40617.lycF9q.rst diff --git a/Misc/NEWS.d/next/Library/2020-05-24-23-52-03.bpo-40617.lycF9q.rst b/Misc/NEWS.d/next/Library/2020-05-24-23-52-03.bpo-40617.lycF9q.rst new file mode 100644 index 00000000000000..6fdcaf4ef73604 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-24-23-52-03.bpo-40617.lycF9q.rst @@ -0,0 +1 @@ +Add support for aggregate window functions in :mod:`sqlite3`. Patch by Erlend E. Aasland. From 7dfeb135a8fa45d742f0717282a32f0590670273 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 10 Mar 2021 11:55:15 +0100 Subject: [PATCH 03/40] Add What's New --- Doc/whatsnew/3.10.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 1c2919a06c4d32..d7b4fc011cbf78 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -1043,6 +1043,13 @@ Add :data:`sys.stdlib_module_names`, containing the list of the standard library module names. (Contributed by Victor Stinner in :issue:`42955`.) +sqlite3 +------- + +Added :meth:`~sqlite3.Connection.create_window_function` to create aggregate +window functions. +(Contributed by Erlend E. Aasland in :issue:`34916`.) + _thread ------- From 00fb71c8b1c0be7e66166a527be86a4c84a4a012 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 14 Jul 2021 23:03:01 +0200 Subject: [PATCH 04/40] Move What's New from 3.10 to 3.11 --- Doc/whatsnew/3.10.rst | 7 ------- Doc/whatsnew/3.11.rst | 4 ++++ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 4e45fbd413a641..cd3db5550a6bf4 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -1295,13 +1295,6 @@ Add :data:`sys.stdlib_module_names`, containing the list of the standard library module names. (Contributed by Victor Stinner in :issue:`42955`.) -sqlite3 -------- - -Added :meth:`~sqlite3.Connection.create_window_function` to create aggregate -window functions. -(Contributed by Erlend E. Aasland in :issue:`34916`.) - _thread ------- diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 7d2e4e81269e93..724d5cf7a2a610 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -205,6 +205,10 @@ sqlite3 :meth:`~sqlite3.Connection.set_authorizer`. (Contributed by Erlend E. Aasland in :issue:`44491`.) +* Added :meth:`~sqlite3.Connection.create_window_function` to create aggregate + window functions. + (Contributed by Erlend E. Aasland in :issue:`34916`.) + Removed ======= From 8ad68e265bcd305a561c956a3a308a85eb705032 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Thu, 29 Jul 2021 13:24:50 +0200 Subject: [PATCH 05/40] Add traceback test --- Lib/sqlite3/test/userfunctions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index 02810cda1a7c94..9ee274601826e1 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -422,6 +422,7 @@ def test_error_on_create(self): with self.assertRaises(sqlite.OperationalError): self.con.create_window_function("shouldfail", -100, WindowSumInt) + @with_tracebacks(['raise effect']) def test_exception_in_method(self): for meth in ["step", "value", "inverse"]: with unittest.mock.patch.object(WindowSumInt, meth, From 60ac350578cc04dadb752f5b0ef221eeea9400e0 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 30 Jul 2021 15:05:34 +0200 Subject: [PATCH 06/40] Improve coverage --- Lib/sqlite3/test/userfunctions.py | 63 +++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index 9ee274601826e1..804b2bde68c95f 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -419,24 +419,63 @@ def test_sum_int(self): self.assertEqual(self.cur.fetchall(), self.expected) def test_error_on_create(self): - with self.assertRaises(sqlite.OperationalError): - self.con.create_window_function("shouldfail", -100, WindowSumInt) + self.assertRaises(sqlite.OperationalError, + self.con.create_window_function, + "shouldfail", -100, WindowSumInt) @with_tracebacks(['raise effect']) def test_exception_in_method(self): - for meth in ["step", "value", "inverse"]: - with unittest.mock.patch.object(WindowSumInt, meth, - side_effect=Exception): - func = f"exc_{meth}" - self.con.create_window_function(func, 1, WindowSumInt) - with self.assertRaises(sqlite.OperationalError): - self.cur.execute(self.query % func) - ret = self.cur.fetchall() + # Fixme: "finalize" does not raise correct exception yet + for meth in ["__init__", "step", "value", "inverse"]: + with self.subTest(meth=meth): + with unittest.mock.patch.object(WindowSumInt, meth, + side_effect=Exception): + name = f"exc_{meth}" + self.con.create_window_function(name, 1, WindowSumInt) + err_str = f"'{meth}' method raised error" + with self.assertRaisesRegex(sqlite.OperationalError, + err_str): + self.cur.execute(self.query % name) + ret = self.cur.fetchall() + + def test_missing_method(self): + class MissingValue: + def __init__(self): pass + def step(self, x): pass + def inverse(self, x): pass + class MissingInverse: + def __init__(self): pass + def step(self, x): pass + def value(self): return 42 + class MissingStep: + def __init__(self): pass + def value(self): return 42 + def inverse(self, x): pass + class MissingFinalize: + def __init__(self): pass + def step(self, x): pass + def value(self): return 42 + def inverse(self, x): pass + # Fixme: step, value and finalize does not raise correct exceptions + dataset = ( + #("step", MissingStep), + #("value", MissingValue), + ("inverse", MissingInverse), + #("finalize", MissingFinalize), + ) + for meth, cls in dataset: + with self.subTest(meth=meth, cls=cls): + self.con.create_window_function(meth, 1, cls) + self.addCleanup(self.con.create_window_function, meth, 1, None) + with self.assertRaisesRegex(sqlite.OperationalError, + f"'{meth}' method not defined"): + self.cur.execute(self.query % meth) + self.cur.fetchall() def test_clear_function(self): self.con.create_window_function("sumint", 1, None) - with self.assertRaises(sqlite.OperationalError): - self.cur.execute(self.query % "sumint") + self.assertRaises(sqlite.OperationalError, self.cur.execute, + self.query % "sumint") def test_redefine_function(self): class Redefined(WindowSumInt): From a7eac0285d7370ff9f014d62fd4d8bb5360f5ba3 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sat, 31 Jul 2021 21:33:18 +0200 Subject: [PATCH 07/40] Fix segfault with missing step method As a side effect: missing step methods now generate OperationalError iso. AttributeError. --- Lib/sqlite3/test/userfunctions.py | 9 +++++---- Modules/_sqlite/connection.c | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index 804b2bde68c95f..f5cf5b1e1dbe36 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -456,9 +456,10 @@ def __init__(self): pass def step(self, x): pass def value(self): return 42 def inverse(self, x): pass + # Fixme: step, value and finalize does not raise correct exceptions dataset = ( - #("step", MissingStep), + ("step", MissingStep), #("value", MissingValue), ("inverse", MissingInverse), #("finalize", MissingFinalize), @@ -521,9 +522,9 @@ def test_aggr_error_on_create(self): def test_aggr_no_step(self): cur = self.con.cursor() - with self.assertRaises(AttributeError) as cm: - cur.execute("select nostep(t) from test") - self.assertEqual(str(cm.exception), "'AggrNoStep' object has no attribute 'step'") + self.assertRaisesRegex(sqlite.OperationalError, + "'step' method not defined", cur.execute, + "select nostep(t) from test") def test_aggr_no_finalize(self): cur = self.con.cursor() diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 372848155dd254..593caef4c63f06 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -691,6 +691,10 @@ static void _pysqlite_step_callback(sqlite3_context *context, int argc, sqlite3_ stepmethod = PyObject_GetAttrString(*aggregate_instance, "step"); if (!stepmethod) { + sqlite3_result_error(context, + "user-defined aggregate's 'step' method " + "not defined", + -1); goto error; } @@ -703,6 +707,15 @@ static void _pysqlite_step_callback(sqlite3_context *context, int argc, sqlite3_ Py_DECREF(args); if (!function_result) { + sqlite3_result_error(context, + "user-defined aggregate's 'step' method " + "raised error", + -1); + goto error; + } + +error: + if (PyErr_Occurred()) { pysqlite_state *state = pysqlite_get_state(NULL); if (state->enable_callback_tracebacks) { PyErr_Print(); @@ -710,10 +723,7 @@ static void _pysqlite_step_callback(sqlite3_context *context, int argc, sqlite3_ else { PyErr_Clear(); } - sqlite3_result_error(context, "user-defined aggregate's 'step' method raised error", -1); } - -error: Py_XDECREF(stepmethod); Py_XDECREF(function_result); From 0ba3ea1639baa8a3e71c06b6940fdf80a4091dfd Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sat, 31 Jul 2021 22:17:04 +0200 Subject: [PATCH 08/40] Improve coverage --- Lib/sqlite3/test/userfunctions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index f5cf5b1e1dbe36..032627db4fb30b 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -459,17 +459,17 @@ def inverse(self, x): pass # Fixme: step, value and finalize does not raise correct exceptions dataset = ( - ("step", MissingStep), - #("value", MissingValue), - ("inverse", MissingInverse), + ("step", MissingStep, "not defined"), + ("value", MissingValue, "raised error"), + ("inverse", MissingInverse, "not defined"), #("finalize", MissingFinalize), ) - for meth, cls in dataset: - with self.subTest(meth=meth, cls=cls): + for meth, cls, err in dataset: + with self.subTest(meth=meth, cls=cls, err=err): self.con.create_window_function(meth, 1, cls) self.addCleanup(self.con.create_window_function, meth, 1, None) with self.assertRaisesRegex(sqlite.OperationalError, - f"'{meth}' method not defined"): + f"'{meth}' method {err}"): self.cur.execute(self.query % meth) self.cur.fetchall() From 7d4f71cab2512fc2596621d4df984e4435c136d6 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sat, 31 Jul 2021 22:18:00 +0200 Subject: [PATCH 09/40] Improve test namespace --- Lib/sqlite3/test/userfunctions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index 032627db4fb30b..ee4033ea1c13fe 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -414,17 +414,17 @@ def setUp(self): """) self.con.create_window_function("sumint", 1, WindowSumInt) - def test_sum_int(self): + def test_win_sum_int(self): self.cur.execute(self.query % "sumint") self.assertEqual(self.cur.fetchall(), self.expected) - def test_error_on_create(self): + def test_win_error_on_create(self): self.assertRaises(sqlite.OperationalError, self.con.create_window_function, "shouldfail", -100, WindowSumInt) @with_tracebacks(['raise effect']) - def test_exception_in_method(self): + def test_win_exception_in_method(self): # Fixme: "finalize" does not raise correct exception yet for meth in ["__init__", "step", "value", "inverse"]: with self.subTest(meth=meth): @@ -438,7 +438,7 @@ def test_exception_in_method(self): self.cur.execute(self.query % name) ret = self.cur.fetchall() - def test_missing_method(self): + def test_win_missing_method(self): class MissingValue: def __init__(self): pass def step(self, x): pass @@ -473,12 +473,12 @@ def inverse(self, x): pass self.cur.execute(self.query % meth) self.cur.fetchall() - def test_clear_function(self): + def test_win_clear_function(self): self.con.create_window_function("sumint", 1, None) self.assertRaises(sqlite.OperationalError, self.cur.execute, self.query % "sumint") - def test_redefine_function(self): + def test_win_redefine_function(self): class Redefined(WindowSumInt): pass self.con.create_window_function("sumint", 1, Redefined) From 99f0c848a868eecaa1215d0ce0d33190b1fbb538 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sat, 31 Jul 2021 22:19:14 +0200 Subject: [PATCH 10/40] Adjust fixme comment --- Lib/sqlite3/test/userfunctions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index ee4033ea1c13fe..2f33766735e2f0 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -457,7 +457,7 @@ def step(self, x): pass def value(self): return 42 def inverse(self, x): pass - # Fixme: step, value and finalize does not raise correct exceptions + # Fixme: finalize does not raise correct exception dataset = ( ("step", MissingStep, "not defined"), ("value", MissingValue, "raised error"), From f4fea569922dc0f02549c0cadb330c9ed83595b3 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sat, 31 Jul 2021 22:49:06 +0200 Subject: [PATCH 11/40] Adjust docs --- Doc/includes/sqlite3/sumintwindow.py | 2 +- Doc/library/sqlite3.rst | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Doc/includes/sqlite3/sumintwindow.py b/Doc/includes/sqlite3/sumintwindow.py index f68ade87e51899..fb40501cf554c1 100644 --- a/Doc/includes/sqlite3/sumintwindow.py +++ b/Doc/includes/sqlite3/sumintwindow.py @@ -19,7 +19,7 @@ def inverse(self, value): self.count -= value def finalize(self): - """This method is invoked to return the current value of the aggregate. + """This method is invoked to return the final value of the aggregate. Any clean-up actions should be placed here. """ diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 4547561c2f9f7f..0dffde38b0368a 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -404,9 +404,7 @@ Connection Objects If *deterministic* is :const:`True`, the created function is marked as `deterministic `_, which - allows SQLite to perform additional optimizations. This flag is - supported by SQLite 3.8.3 or higher. :exc:`NotSupportedError` will - be raised if used with older versions. + allows SQLite to perform additional optimizations. If *innocuous* is :const:`True`, the created function is marked as `innocuous `_, which @@ -423,7 +421,7 @@ Connection Objects 3.30.0 or higher. :exc:`NotSupportedError` will be raised if used with older versions. - .. versionadded:: 3.10 + .. versionadded:: 3.11 Example: From 44999be9ec5d0c9fde77cab3ee6e13c060a4f201 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 8 Aug 2021 23:53:50 +0200 Subject: [PATCH 12/40] Test unable to set return value in value callback --- Lib/sqlite3/test/userfunctions.py | 9 +++++++++ Modules/_sqlite/connection.c | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index 1c9b771eb18373..5eec03b1403c94 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -572,6 +572,15 @@ class Redefined(WindowSumInt): self.cur.execute(self.query % "sumint") self.assertEqual(self.cur.fetchall(), self.expected) + def test_win_error_value_return(self): + class ErrorValueReturn: + def __init__(self): pass + def step(self, x): pass + def value(self): return 1 << 65 + self.con.create_window_function("err_val_ret", 1, ErrorValueReturn) + self.assertRaisesRegex(sqlite.DataError, "string or blob too big", + self.cur.execute, self.query % "err_val_ret") + class AggregateTests(unittest.TestCase): def setUp(self): diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index d5ac89f7664b5a..3ac6613bf760bc 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1042,7 +1042,7 @@ value_callback(sqlite3_context *context) int rc = _pysqlite_set_result(context, res); Py_DECREF(res); - if (rc != 0) { + if (rc < 0) { set_sqlite_error(context, "unable to set result from user-defined aggregate's " "'value' method"); From 25ecc849d45d3e17aaacea04fc82c186436786bc Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 13 Aug 2021 09:33:14 +0200 Subject: [PATCH 13/40] Make sure test db is committed --- Lib/sqlite3/test/userfunctions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index 5eec03b1403c94..0e7f5223910e73 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -475,6 +475,7 @@ def finalize(self): class WindowFunctionTests(unittest.TestCase): def setUp(self): self.con = sqlite.connect(":memory:") + self.cur = self.con.cursor() # Test case taken from https://www.sqlite.org/windowfunctions.html#udfwinfunc values = [ @@ -484,8 +485,9 @@ def setUp(self): ("d", 8), ("e", 1), ] - self.cur = self.con.execute("create table test(x, y)") - self.cur.executemany("insert into test values(?, ?)", values) + with self.con: + self.con.execute("create table test(x, y)") + self.con.executemany("insert into test values(?, ?)", values) self.expected = [ ("a", 9), ("b", 12), From d73adc923b762c66551495d0e69bf12e156adda4 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Tue, 24 Aug 2021 20:47:09 +0200 Subject: [PATCH 14/40] Convert to use the new callback_context struct --- Modules/_sqlite/connection.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 9c3bb62b6cd5ce..7a179637a02642 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1081,8 +1081,13 @@ pysqlite_connection_create_window_function_impl(pysqlite_Connection *self, 0, 0, 0, 0, 0, 0); } else { + callback_context *ctx = create_callback_context(self->state, + aggregate_class); + if (ctx == NULL) { + return NULL; + } rc = sqlite3_create_window_function(self->db, name, num_params, flags, - Py_NewRef(aggregate_class), + ctx, &_pysqlite_step_callback, &_pysqlite_final_callback, &value_callback, From 397e05ab7c21b7d9cbc89ccce5e6c806fe045b40 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Thu, 26 Aug 2021 14:06:43 +0200 Subject: [PATCH 15/40] Use set_sqlite_error in. step callback --- Modules/_sqlite/connection.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 3e49856765e68c..fd7c7485d7150e 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -684,10 +684,8 @@ static void _pysqlite_step_callback(sqlite3_context *context, int argc, sqlite3_ stepmethod = PyObject_GetAttrString(*aggregate_instance, "step"); if (!stepmethod) { - sqlite3_result_error(context, - "user-defined aggregate's 'step' method " - "not defined", - -1); + set_sqlite_error(context, + "user-defined aggregate's 'step' method not defined"); goto error; } From be2b54f7071ac877e8eec719e2e764e6f2c8ee63 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Thu, 26 Aug 2021 20:47:29 +0200 Subject: [PATCH 16/40] WIP --- Lib/sqlite3/test/userfunctions.py | 5 +++- Modules/_sqlite/connection.c | 40 ++++++++++++++++--------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index 0e7f5223910e73..92bf3befe20765 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -532,14 +532,17 @@ class MissingValue: def __init__(self): pass def step(self, x): pass def inverse(self, x): pass + def finalize(self): return 42 class MissingInverse: def __init__(self): pass def step(self, x): pass def value(self): return 42 + def finalize(self): return 42 class MissingStep: def __init__(self): pass def value(self): return 42 def inverse(self, x): pass + def finalize(self): return 42 class MissingFinalize: def __init__(self): pass def step(self, x): pass @@ -551,7 +554,7 @@ def inverse(self, x): pass ("step", MissingStep, "not defined"), ("value", MissingValue, "raised error"), ("inverse", MissingInverse, "not defined"), - #("finalize", MissingFinalize), + ("finalize", MissingFinalize, "raised error"), ) for meth, cls, err in dataset: with self.subTest(meth=meth, cls=cls, err=err): diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index fd7c7485d7150e..d40fdcbb4dd2d3 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -712,11 +712,7 @@ static void _pysqlite_step_callback(sqlite3_context *context, int argc, sqlite3_ static void _pysqlite_final_callback(sqlite3_context *context) { - PyObject* function_result; PyObject** aggregate_instance; - _Py_IDENTIFIER(finalize); - int ok; - PyObject *exception, *value, *tb; PyGILState_STATE threadstate; @@ -735,26 +731,32 @@ _pysqlite_final_callback(sqlite3_context *context) } /* Keep the exception (if any) of the last call to step() */ + PyObject *exception, *value, *tb; PyErr_Fetch(&exception, &value, &tb); - function_result = _PyObject_CallMethodIdNoArgs(*aggregate_instance, &PyId_finalize); - + _Py_IDENTIFIER(finalize); + PyObject *res = _PyObject_CallMethodIdNoArgs(*aggregate_instance, + &PyId_finalize); Py_DECREF(*aggregate_instance); - - ok = 0; - if (function_result) { - ok = _pysqlite_set_result(context, function_result) == 0; - Py_DECREF(function_result); - } - if (!ok) { + if (res == NULL) { set_sqlite_error(context, "user-defined aggregate's 'finalize' method raised error"); + goto error; } /* Restore the exception (if any) of the last call to step(), but clear also the current exception if finalize() failed */ PyErr_Restore(exception, value, tb); + int rc = _pysqlite_set_result(context, res); + Py_DECREF(res); + if (rc < 0) { + set_sqlite_error(context, + "unable to set result from user-defined aggregate's " + "'value' method"); + goto error; + } + error: PyGILState_Release(threadstate); } @@ -944,14 +946,13 @@ pysqlite_connection_create_function_impl(pysqlite_Connection *self, static void inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) { - PyObject *method = NULL; PyGILState_STATE gilstate = PyGILState_Ensure(); - PyObject **cls = (PyObject **)sqlite3_aggregate_context(context, - sizeof(PyObject *)); + void *agg_ctx = sqlite3_aggregate_context(context, sizeof(PyObject *)); + PyObject **cls = (PyObject **)agg_ctx; assert(cls != NULL); assert(*cls != NULL); - method = PyObject_GetAttrString(*cls, "inverse"); + PyObject *method = PyObject_GetAttrString(*cls, "inverse"); if (method == NULL) { set_sqlite_error(context, "user-defined aggregate's 'inverse' method " @@ -961,6 +962,7 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) PyObject *args = _pysqlite_build_py_params(context, argc, params); if (args == NULL) { + Py_DECREF(method); set_sqlite_error(context, "unable to build arguments for user-defined " "aggregate's 'inverse' method"); @@ -968,6 +970,7 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) } PyObject *res = PyObject_CallObject(method, args); + Py_DECREF(method); Py_DECREF(args); if (res == NULL) { set_sqlite_error(context, @@ -978,7 +981,6 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) Py_DECREF(res); exit: - Py_XDECREF(method); PyGILState_Release(gilstate); } @@ -991,7 +993,6 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) static void value_callback(sqlite3_context *context) { - _Py_IDENTIFIER(value); PyGILState_STATE gilstate = PyGILState_Ensure(); PyObject **cls = NULL; // Aggregate class instance. @@ -999,6 +1000,7 @@ value_callback(sqlite3_context *context) assert(cls != NULL); assert(*cls != NULL); + _Py_IDENTIFIER(value); PyObject *res = _PyObject_CallMethodIdNoArgs(*cls, &PyId_value); if (res == NULL) { set_sqlite_error(context, From 388e5a3486039f5aa20acdb42d4bc52aa275b366 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Mon, 30 Aug 2021 22:04:42 +0200 Subject: [PATCH 17/40] Revert "WIP" This reverts commit be2b54f7071ac877e8eec719e2e764e6f2c8ee63. --- Lib/sqlite3/test/userfunctions.py | 5 +--- Modules/_sqlite/connection.c | 40 +++++++++++++++---------------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index 92bf3befe20765..0e7f5223910e73 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -532,17 +532,14 @@ class MissingValue: def __init__(self): pass def step(self, x): pass def inverse(self, x): pass - def finalize(self): return 42 class MissingInverse: def __init__(self): pass def step(self, x): pass def value(self): return 42 - def finalize(self): return 42 class MissingStep: def __init__(self): pass def value(self): return 42 def inverse(self, x): pass - def finalize(self): return 42 class MissingFinalize: def __init__(self): pass def step(self, x): pass @@ -554,7 +551,7 @@ def inverse(self, x): pass ("step", MissingStep, "not defined"), ("value", MissingValue, "raised error"), ("inverse", MissingInverse, "not defined"), - ("finalize", MissingFinalize, "raised error"), + #("finalize", MissingFinalize), ) for meth, cls, err in dataset: with self.subTest(meth=meth, cls=cls, err=err): diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index d40fdcbb4dd2d3..fd7c7485d7150e 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -712,7 +712,11 @@ static void _pysqlite_step_callback(sqlite3_context *context, int argc, sqlite3_ static void _pysqlite_final_callback(sqlite3_context *context) { + PyObject* function_result; PyObject** aggregate_instance; + _Py_IDENTIFIER(finalize); + int ok; + PyObject *exception, *value, *tb; PyGILState_STATE threadstate; @@ -731,32 +735,26 @@ _pysqlite_final_callback(sqlite3_context *context) } /* Keep the exception (if any) of the last call to step() */ - PyObject *exception, *value, *tb; PyErr_Fetch(&exception, &value, &tb); - _Py_IDENTIFIER(finalize); - PyObject *res = _PyObject_CallMethodIdNoArgs(*aggregate_instance, - &PyId_finalize); + function_result = _PyObject_CallMethodIdNoArgs(*aggregate_instance, &PyId_finalize); + Py_DECREF(*aggregate_instance); - if (res == NULL) { + + ok = 0; + if (function_result) { + ok = _pysqlite_set_result(context, function_result) == 0; + Py_DECREF(function_result); + } + if (!ok) { set_sqlite_error(context, "user-defined aggregate's 'finalize' method raised error"); - goto error; } /* Restore the exception (if any) of the last call to step(), but clear also the current exception if finalize() failed */ PyErr_Restore(exception, value, tb); - int rc = _pysqlite_set_result(context, res); - Py_DECREF(res); - if (rc < 0) { - set_sqlite_error(context, - "unable to set result from user-defined aggregate's " - "'value' method"); - goto error; - } - error: PyGILState_Release(threadstate); } @@ -946,13 +944,14 @@ pysqlite_connection_create_function_impl(pysqlite_Connection *self, static void inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) { + PyObject *method = NULL; PyGILState_STATE gilstate = PyGILState_Ensure(); - void *agg_ctx = sqlite3_aggregate_context(context, sizeof(PyObject *)); - PyObject **cls = (PyObject **)agg_ctx; + PyObject **cls = (PyObject **)sqlite3_aggregate_context(context, + sizeof(PyObject *)); assert(cls != NULL); assert(*cls != NULL); - PyObject *method = PyObject_GetAttrString(*cls, "inverse"); + method = PyObject_GetAttrString(*cls, "inverse"); if (method == NULL) { set_sqlite_error(context, "user-defined aggregate's 'inverse' method " @@ -962,7 +961,6 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) PyObject *args = _pysqlite_build_py_params(context, argc, params); if (args == NULL) { - Py_DECREF(method); set_sqlite_error(context, "unable to build arguments for user-defined " "aggregate's 'inverse' method"); @@ -970,7 +968,6 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) } PyObject *res = PyObject_CallObject(method, args); - Py_DECREF(method); Py_DECREF(args); if (res == NULL) { set_sqlite_error(context, @@ -981,6 +978,7 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) Py_DECREF(res); exit: + Py_XDECREF(method); PyGILState_Release(gilstate); } @@ -993,6 +991,7 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) static void value_callback(sqlite3_context *context) { + _Py_IDENTIFIER(value); PyGILState_STATE gilstate = PyGILState_Ensure(); PyObject **cls = NULL; // Aggregate class instance. @@ -1000,7 +999,6 @@ value_callback(sqlite3_context *context) assert(cls != NULL); assert(*cls != NULL); - _Py_IDENTIFIER(value); PyObject *res = _PyObject_CallMethodIdNoArgs(*cls, &PyId_value); if (res == NULL) { set_sqlite_error(context, From 67649840d8e155bcec9cfabac750fa01301f1abd Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 8 Sep 2021 23:27:11 +0200 Subject: [PATCH 18/40] Fix merge --- Modules/_sqlite/connection.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 9324dd0a90cd47..aa8049b2dc0b86 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -985,14 +985,13 @@ pysqlite_connection_create_function_impl(pysqlite_Connection *self, static void inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) { - PyObject *method = NULL; PyGILState_STATE gilstate = PyGILState_Ensure(); - PyObject **cls = (PyObject **)sqlite3_aggregate_context(context, - sizeof(PyObject *)); + int size = sizeof(PyObject *); + PyObject **cls = (PyObject **)sqlite3_aggregate_context(context, size); assert(cls != NULL); assert(*cls != NULL); - method = PyObject_GetAttrString(*cls, "inverse"); + PyObject *method = PyObject_GetAttrString(*cls, "inverse"); if (method == NULL) { set_sqlite_error(context, "user-defined aggregate's 'inverse' method " @@ -1032,14 +1031,14 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) static void value_callback(sqlite3_context *context) { - _Py_IDENTIFIER(value); PyGILState_STATE gilstate = PyGILState_Ensure(); - PyObject **cls = NULL; // Aggregate class instance. - cls = (PyObject **)sqlite3_aggregate_context(context, sizeof(PyObject *)); + int size = sizeof(PyObject *); + PyObject **cls = (PyObject **)sqlite3_aggregate_context(context, size); assert(cls != NULL); assert(*cls != NULL); + _Py_IDENTIFIER(value); PyObject *res = _PyObject_CallMethodIdNoArgs(*cls, &PyId_value); if (res == NULL) { set_sqlite_error(context, @@ -1127,11 +1126,11 @@ pysqlite_connection_create_window_function_impl(pysqlite_Connection *self, } rc = sqlite3_create_window_function(self->db, name, num_params, flags, ctx, - &_pysqlite_step_callback, - &_pysqlite_final_callback, + &step_callback, + &final_callback, &value_callback, &inverse_callback, - &_destructor); + &destructor_callback); } if (rc != SQLITE_OK) { From f1331f2fb04cb27543628f5d6711e7e5887378ba Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 12 Sep 2021 18:26:31 +0200 Subject: [PATCH 19/40] Raise more accurate error messages for methods that are not defined --- Lib/sqlite3/test/userfunctions.py | 26 ++++++++++++++------------ Modules/_sqlite/connection.c | 27 +++++++++++++++++---------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index 0e7f5223910e73..447c16ff5525ab 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -529,36 +529,38 @@ def test_win_exception_in_method(self): def test_win_missing_method(self): class MissingValue: - def __init__(self): pass def step(self, x): pass def inverse(self, x): pass + def finalize(self): return 42 + class MissingInverse: - def __init__(self): pass def step(self, x): pass def value(self): return 42 + def finalize(self): return 42 + class MissingStep: - def __init__(self): pass def value(self): return 42 def inverse(self, x): pass + def finalize(self): return 42 + class MissingFinalize: - def __init__(self): pass def step(self, x): pass def value(self): return 42 def inverse(self, x): pass # Fixme: finalize does not raise correct exception dataset = ( - ("step", MissingStep, "not defined"), - ("value", MissingValue, "raised error"), - ("inverse", MissingInverse, "not defined"), + ("step", MissingStep), + ("value", MissingValue), + ("inverse", MissingInverse), #("finalize", MissingFinalize), ) - for meth, cls, err in dataset: - with self.subTest(meth=meth, cls=cls, err=err): + for meth, cls in dataset: + with self.subTest(meth=meth, cls=cls): self.con.create_window_function(meth, 1, cls) self.addCleanup(self.con.create_window_function, meth, 1, None) with self.assertRaisesRegex(sqlite.OperationalError, - f"'{meth}' method {err}"): + f"'{meth}' method not defined"): self.cur.execute(self.query % meth) self.cur.fetchall() @@ -627,10 +629,10 @@ def test_aggr_no_step(self): def test_aggr_no_finalize(self): cur = self.con.cursor() - with self.assertRaises(sqlite.OperationalError) as cm: + msg = "user-defined aggregate's 'finalize' method not defined" + with self.assertRaisesRegex(sqlite.OperationalError, msg): cur.execute("select nofinalize(t) from test") val = cur.fetchone()[0] - self.assertEqual(str(cm.exception), "user-defined aggregate's 'finalize' method raised error") @with_tracebacks(['__init__', '5/0', 'ZeroDivisionError']) def test_aggr_exception_in_init(self): diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index aa8049b2dc0b86..31a28947bdb8e9 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -765,7 +765,7 @@ final_callback(sqlite3_context *context) goto error; } - /* Keep the exception (if any) of the last call to step() */ + // Keep the exception (if any) of the last call to step, value, or inverse PyErr_Fetch(&exception, &value, &tb); function_result = _PyObject_CallMethodIdNoArgs(*aggregate_instance, &PyId_finalize); @@ -778,13 +778,17 @@ final_callback(sqlite3_context *context) Py_DECREF(function_result); } if (!ok) { - set_sqlite_error(context, - "user-defined aggregate's 'finalize' method raised error"); + const char *attr_msg = "user-defined aggregate's 'finalize' method " + "not defined"; + const char *err_msg = "user-defined aggregate's 'finalize' method " + "raised error"; + int attr_err = PyErr_ExceptionMatches(PyExc_AttributeError); + _PyErr_ChainExceptions(exception, value, tb); + set_sqlite_error(context, attr_err ? attr_msg : err_msg); + } + else { + PyErr_Restore(exception, value, tb); } - - /* Restore the exception (if any) of the last call to step(), - but clear also the current exception if finalize() failed */ - PyErr_Restore(exception, value, tb); error: PyGILState_Release(threadstate); @@ -1041,9 +1045,12 @@ value_callback(sqlite3_context *context) _Py_IDENTIFIER(value); PyObject *res = _PyObject_CallMethodIdNoArgs(*cls, &PyId_value); if (res == NULL) { - set_sqlite_error(context, - "user-defined aggregate's 'value' method " - "raised error"); + const char *attr_msg = "user-defined aggregate's 'value' method " + "not defined"; + const char *err_msg = "user-defined aggregate's 'value' method " + "raised error"; + int attr_err = PyErr_ExceptionMatches(PyExc_AttributeError); + set_sqlite_error(context, attr_err ? attr_msg : err_msg); goto exit; } From f17317863b235113820f16c0c43f2d6ccca40974 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sat, 22 Jan 2022 21:19:29 +0100 Subject: [PATCH 20/40] Fixup merge --- Lib/test/test_sqlite3/test_userfunctions.py | 7 ++- Modules/_sqlite/clinic/connection.c.h | 61 +++------------------ Modules/_sqlite/connection.c | 29 +++++----- 3 files changed, 28 insertions(+), 69 deletions(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index e3c70a814193eb..3a88357497d849 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -483,6 +483,9 @@ def inverse(self, value): def finalize(self): return self.count +class WindowBogusException(Exception): + pass + @unittest.skipIf(sqlite.sqlite_version_info < (3, 25, 0), "Requires SQLite 3.25.0 or newer") @@ -526,13 +529,13 @@ def test_win_error_on_create(self): self.con.create_window_function, "shouldfail", -100, WindowSumInt) - @with_tracebacks(['raise effect']) + @with_tracebacks(WindowBogusException) def test_win_exception_in_method(self): # Fixme: "finalize" does not raise correct exception yet for meth in ["__init__", "step", "value", "inverse"]: with self.subTest(meth=meth): with unittest.mock.patch.object(WindowSumInt, meth, - side_effect=Exception): + side_effect=WindowBogusException): name = f"exc_{meth}" self.con.create_window_function(name, 1, WindowSumInt) err_str = f"'{meth}' method raised error" diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h index 66e0d25d7788b6..701c9129ad6fc5 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -256,10 +256,11 @@ PyDoc_STRVAR(pysqlite_connection_create_window_function__doc__, " Set to None to clear the window function."); #define PYSQLITE_CONNECTION_CREATE_WINDOW_FUNCTION_METHODDEF \ - {"create_window_function", (PyCFunction)(void(*)(void))pysqlite_connection_create_window_function, METH_FASTCALL|METH_KEYWORDS, pysqlite_connection_create_window_function__doc__}, + {"create_window_function", (PyCFunction)(void(*)(void))pysqlite_connection_create_window_function, METH_METHOD|METH_FASTCALL|METH_KEYWORDS, pysqlite_connection_create_window_function__doc__}, static PyObject * pysqlite_connection_create_window_function_impl(pysqlite_Connection *self, + PyTypeObject *cls, const char *name, int num_params, PyObject *aggregate_class, @@ -268,13 +269,11 @@ pysqlite_connection_create_window_function_impl(pysqlite_Connection *self, int innocuous); static PyObject * -pysqlite_connection_create_window_function(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +pysqlite_connection_create_window_function(pysqlite_Connection *self, PyTypeObject *cls, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) { PyObject *return_value = NULL; static const char * const _keywords[] = {"", "", "", "deterministic", "directonly", "innocuous", NULL}; - static _PyArg_Parser _parser = {NULL, _keywords, "create_window_function", 0}; - PyObject *argsbuf[6]; - Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 3; + static _PyArg_Parser _parser = {"siO|$ppp:create_window_function", _keywords, 0}; const char *name; int num_params; PyObject *aggregate_class; @@ -282,55 +281,11 @@ pysqlite_connection_create_window_function(pysqlite_Connection *self, PyObject * int directonly = 0; int innocuous = 0; - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 3, 3, 0, argsbuf); - if (!args) { - goto exit; - } - if (!PyUnicode_Check(args[0])) { - _PyArg_BadArgument("create_window_function", "argument 1", "str", args[0]); - goto exit; - } - Py_ssize_t name_length; - name = PyUnicode_AsUTF8AndSize(args[0], &name_length); - if (name == NULL) { - goto exit; - } - if (strlen(name) != (size_t)name_length) { - PyErr_SetString(PyExc_ValueError, "embedded null character"); - goto exit; - } - num_params = _PyLong_AsInt(args[1]); - if (num_params == -1 && PyErr_Occurred()) { - goto exit; - } - aggregate_class = args[2]; - if (!noptargs) { - goto skip_optional_kwonly; - } - if (args[3]) { - deterministic = PyObject_IsTrue(args[3]); - if (deterministic < 0) { - goto exit; - } - if (!--noptargs) { - goto skip_optional_kwonly; - } - } - if (args[4]) { - directonly = PyObject_IsTrue(args[4]); - if (directonly < 0) { - goto exit; - } - if (!--noptargs) { - goto skip_optional_kwonly; - } - } - innocuous = PyObject_IsTrue(args[5]); - if (innocuous < 0) { + if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser, + &name, &num_params, &aggregate_class, &deterministic, &directonly, &innocuous)) { goto exit; } -skip_optional_kwonly: - return_value = pysqlite_connection_create_window_function_impl(self, name, num_params, aggregate_class, deterministic, directonly, innocuous); + return_value = pysqlite_connection_create_window_function_impl(self, cls, name, num_params, aggregate_class, deterministic, directonly, innocuous); exit: return return_value; @@ -943,4 +898,4 @@ getlimit(pysqlite_Connection *self, PyObject *arg) #ifndef PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF #define PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF #endif /* !defined(PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF) */ -/*[clinic end generated code: output=635579f747369aad input=a9049054013a1b77]*/ +/*[clinic end generated code: output=dcc8ff71a6779d53 input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index e6a506817d8961..7a812ec0256879 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -29,6 +29,8 @@ #include "prepare_protocol.h" #include "util.h" +#include + #if SQLITE_VERSION_NUMBER >= 3014000 #define HAVE_TRACE_V2 #endif @@ -1094,27 +1096,26 @@ value_callback(sqlite3_context *context) "not defined"; const char *err_msg = "user-defined aggregate's 'value' method " "raised error"; - int attr_err = PyErr_ExceptionMatches(PyExc_AttributeError); + bool attr_err = PyErr_ExceptionMatches(PyExc_AttributeError); set_sqlite_error(context, attr_err ? attr_msg : err_msg); - goto exit; } - - int rc = _pysqlite_set_result(context, res); - Py_DECREF(res); - if (rc < 0) { - set_sqlite_error(context, - "unable to set result from user-defined aggregate's " - "'value' method"); - goto exit; + else { + int rc = _pysqlite_set_result(context, res); + Py_DECREF(res); + if (rc < 0) { + set_sqlite_error(context, + "unable to set result from user-defined " + "aggregate's 'value' method"); + } } -exit: PyGILState_Release(gilstate); } /*[clinic input] _sqlite3.Connection.create_window_function as pysqlite_connection_create_window_function + cls: defining_class name: str The name of the SQL aggregate window function to be created or redefined. @@ -1135,13 +1136,14 @@ Creates or redefines an aggregate window function. Non-standard. static PyObject * pysqlite_connection_create_window_function_impl(pysqlite_Connection *self, + PyTypeObject *cls, const char *name, int num_params, PyObject *aggregate_class, int deterministic, int directonly, int innocuous) -/*[clinic end generated code: output=fbbfad556264ea1e input=877311e540865c3b]*/ +/*[clinic end generated code: output=f36d5595c0c930d5 input=34a9b57a2c411aea]*/ { if (sqlite3_libversion_number() < 3025000) { PyErr_SetString(self->NotSupportedError, @@ -1171,8 +1173,7 @@ pysqlite_connection_create_window_function_impl(pysqlite_Connection *self, 0, 0, 0, 0, 0, 0); } else { - callback_context *ctx = create_callback_context(self->state, - aggregate_class); + callback_context *ctx = create_callback_context(cls, aggregate_class); if (ctx == NULL) { return NULL; } From f1fe3325d8f140b5ef2007cf18eaea7c50d1a85c Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Thu, 3 Mar 2022 15:01:38 +0100 Subject: [PATCH 21/40] Improve docstring wording in example --- Doc/includes/sqlite3/sumintwindow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/includes/sqlite3/sumintwindow.py b/Doc/includes/sqlite3/sumintwindow.py index fb40501cf554c1..e1447799351657 100644 --- a/Doc/includes/sqlite3/sumintwindow.py +++ b/Doc/includes/sqlite3/sumintwindow.py @@ -7,19 +7,19 @@ def __init__(self): self.count = 0 def step(self, value): - """This method is invoked to add a row to the current window.""" + """This callback adds a row to the current window.""" self.count += value def value(self): - """This method is invoked to return the current value of the aggregate.""" + """This callback returns the current value of the aggregate.""" return self.count def inverse(self, value): - """This method is invoked to remove a row from the current window.""" + """This callback removes a row from the current window.""" self.count -= value def finalize(self): - """This method is invoked to return the final value of the aggregate. + """This callback returns the final value of the aggregate. Any clean-up actions should be placed here. """ From 1babe166753c3008e9ec2253001c99a41ab56164 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Thu, 3 Mar 2022 15:16:24 +0100 Subject: [PATCH 22/40] Use interned string for method lookup --- Modules/_sqlite/connection.c | 12 +++++++++--- Modules/_sqlite/module.c | 4 ++++ Modules/_sqlite/module.h | 2 ++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 87dfecf8341836..6a4d02ffb3775d 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1042,11 +1042,15 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) { PyGILState_STATE gilstate = PyGILState_Ensure(); + callback_context *ctx = (callback_context *)sqlite3_user_data(context); + assert(ctx != NULL); + int size = sizeof(PyObject *); PyObject **cls = (PyObject **)sqlite3_aggregate_context(context, size); assert(cls != NULL); assert(*cls != NULL); - PyObject *method = PyObject_GetAttrString(*cls, "inverse"); + + PyObject *method = PyObject_GetAttr(*cls, ctx->state->str_inverse); if (method == NULL) { set_sqlite_error(context, "user-defined aggregate's 'inverse' method " @@ -1088,13 +1092,15 @@ value_callback(sqlite3_context *context) { PyGILState_STATE gilstate = PyGILState_Ensure(); + callback_context *ctx = (callback_context *)sqlite3_user_data(context); + assert(ctx != NULL); + int size = sizeof(PyObject *); PyObject **cls = (PyObject **)sqlite3_aggregate_context(context, size); assert(cls != NULL); assert(*cls != NULL); - _Py_IDENTIFIER(value); - PyObject *res = _PyObject_CallMethodIdNoArgs(*cls, &PyId_value); + PyObject *res = PyObject_CallMethodNoArgs(*cls, ctx->state->str_value); if (res == NULL) { const char *attr_msg = "user-defined aggregate's 'value' method " "not defined"; diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 563105c6391002..0dbade5f28d7da 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -627,8 +627,10 @@ module_clear(PyObject *module) Py_CLEAR(state->str___conform__); Py_CLEAR(state->str_executescript); Py_CLEAR(state->str_finalize); + Py_CLEAR(state->str_inverse); Py_CLEAR(state->str_step); Py_CLEAR(state->str_upper); + Py_CLEAR(state->str_value); return 0; } @@ -714,8 +716,10 @@ module_exec(PyObject *module) ADD_INTERNED(state, __conform__); ADD_INTERNED(state, executescript); ADD_INTERNED(state, finalize); + ADD_INTERNED(state, inverse); ADD_INTERNED(state, step); ADD_INTERNED(state, upper); + ADD_INTERNED(state, value); /* Set error constants */ if (add_error_constants(module) < 0) { diff --git a/Modules/_sqlite/module.h b/Modules/_sqlite/module.h index cca52d1e04b2cb..fcea7096924ce4 100644 --- a/Modules/_sqlite/module.h +++ b/Modules/_sqlite/module.h @@ -64,8 +64,10 @@ typedef struct { PyObject *str___conform__; PyObject *str_executescript; PyObject *str_finalize; + PyObject *str_inverse; PyObject *str_step; PyObject *str_upper; + PyObject *str_value; } pysqlite_state; extern pysqlite_state pysqlite_global_state; From 0f0642812294d6a4387430a3832bf691db710a83 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Thu, 3 Mar 2022 22:11:29 +0100 Subject: [PATCH 23/40] Test adjustments --- Lib/test/test_sqlite3/test_userfunctions.py | 18 +++++------- Modules/_sqlite/clinic/connection.c.h | 30 +++++++++---------- Modules/_sqlite/connection.c | 32 +++++++++------------ 3 files changed, 33 insertions(+), 47 deletions(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index 93f6d46b576c8d..8d9680eec59fe2 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -531,7 +531,7 @@ def test_win_error_on_create(self): @with_tracebacks(WindowBogusException) def test_win_exception_in_method(self): - # Fixme: "finalize" does not raise correct exception yet + # Note: SQLite does not propagate errors from the "finalize" callback. for meth in ["__init__", "step", "value", "inverse"]: with self.subTest(meth=meth): with unittest.mock.patch.object(WindowSumInt, meth, @@ -544,7 +544,9 @@ def test_win_exception_in_method(self): self.cur.execute(self.query % name) ret = self.cur.fetchall() + @with_tracebacks(AttributeError) def test_win_missing_method(self): + # Note: SQLite does not propagate errors from the "finalize" callback. class MissingValue: def step(self, x): pass def inverse(self, x): pass @@ -560,25 +562,19 @@ def value(self): return 42 def inverse(self, x): pass def finalize(self): return 42 - class MissingFinalize: - def step(self, x): pass - def value(self): return 42 - def inverse(self, x): pass - - # Fixme: finalize does not raise correct exception dataset = ( ("step", MissingStep), ("value", MissingValue), ("inverse", MissingInverse), - #("finalize", MissingFinalize), ) for meth, cls in dataset: with self.subTest(meth=meth, cls=cls): - self.con.create_window_function(meth, 1, cls) - self.addCleanup(self.con.create_window_function, meth, 1, None) + name = f"exc_{meth}" + self.con.create_window_function(name, 1, cls) + self.addCleanup(self.con.create_window_function, name, 1, None) with self.assertRaisesRegex(sqlite.OperationalError, f"'{meth}' method not defined"): - self.cur.execute(self.query % meth) + self.cur.execute(self.query % name) self.cur.fetchall() def test_win_clear_function(self): diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h index 701c9129ad6fc5..8a055c511ec4d7 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -237,7 +237,7 @@ pysqlite_connection_create_function(pysqlite_Connection *self, PyTypeObject *cls #if defined(HAVE_WINDOW_FUNCTIONS) -PyDoc_STRVAR(pysqlite_connection_create_window_function__doc__, +PyDoc_STRVAR(create_window_function__doc__, "create_window_function($self, name, num_params, aggregate_class, /, *,\n" " deterministic=False, directonly=False,\n" " innocuous=False)\n" @@ -255,21 +255,17 @@ PyDoc_STRVAR(pysqlite_connection_create_window_function__doc__, " A class with step(), finalize(), value(), and inverse() methods.\n" " Set to None to clear the window function."); -#define PYSQLITE_CONNECTION_CREATE_WINDOW_FUNCTION_METHODDEF \ - {"create_window_function", (PyCFunction)(void(*)(void))pysqlite_connection_create_window_function, METH_METHOD|METH_FASTCALL|METH_KEYWORDS, pysqlite_connection_create_window_function__doc__}, +#define CREATE_WINDOW_FUNCTION_METHODDEF \ + {"create_window_function", (PyCFunction)(void(*)(void))create_window_function, METH_METHOD|METH_FASTCALL|METH_KEYWORDS, create_window_function__doc__}, static PyObject * -pysqlite_connection_create_window_function_impl(pysqlite_Connection *self, - PyTypeObject *cls, - const char *name, - int num_params, - PyObject *aggregate_class, - int deterministic, - int directonly, - int innocuous); +create_window_function_impl(pysqlite_Connection *self, PyTypeObject *cls, + const char *name, int num_params, + PyObject *aggregate_class, int deterministic, + int directonly, int innocuous); static PyObject * -pysqlite_connection_create_window_function(pysqlite_Connection *self, PyTypeObject *cls, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +create_window_function(pysqlite_Connection *self, PyTypeObject *cls, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) { PyObject *return_value = NULL; static const char * const _keywords[] = {"", "", "", "deterministic", "directonly", "innocuous", NULL}; @@ -285,7 +281,7 @@ pysqlite_connection_create_window_function(pysqlite_Connection *self, PyTypeObje &name, &num_params, &aggregate_class, &deterministic, &directonly, &innocuous)) { goto exit; } - return_value = pysqlite_connection_create_window_function_impl(self, cls, name, num_params, aggregate_class, deterministic, directonly, innocuous); + return_value = create_window_function_impl(self, cls, name, num_params, aggregate_class, deterministic, directonly, innocuous); exit: return return_value; @@ -887,9 +883,9 @@ getlimit(pysqlite_Connection *self, PyObject *arg) return return_value; } -#ifndef PYSQLITE_CONNECTION_CREATE_WINDOW_FUNCTION_METHODDEF - #define PYSQLITE_CONNECTION_CREATE_WINDOW_FUNCTION_METHODDEF -#endif /* !defined(PYSQLITE_CONNECTION_CREATE_WINDOW_FUNCTION_METHODDEF) */ +#ifndef CREATE_WINDOW_FUNCTION_METHODDEF + #define CREATE_WINDOW_FUNCTION_METHODDEF +#endif /* !defined(CREATE_WINDOW_FUNCTION_METHODDEF) */ #ifndef PYSQLITE_CONNECTION_ENABLE_LOAD_EXTENSION_METHODDEF #define PYSQLITE_CONNECTION_ENABLE_LOAD_EXTENSION_METHODDEF @@ -898,4 +894,4 @@ getlimit(pysqlite_Connection *self, PyObject *arg) #ifndef PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF #define PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF #endif /* !defined(PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF) */ -/*[clinic end generated code: output=dcc8ff71a6779d53 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=cee6f0d6fa9b61dd input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 6a4d02ffb3775d..7a60d759110714 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1053,16 +1053,15 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) PyObject *method = PyObject_GetAttr(*cls, ctx->state->str_inverse); if (method == NULL) { set_sqlite_error(context, - "user-defined aggregate's 'inverse' method " - "not defined"); + "user-defined aggregate's 'inverse' method not defined"); goto exit; } PyObject *args = _pysqlite_build_py_params(context, argc, params); if (args == NULL) { set_sqlite_error(context, - "unable to build arguments for user-defined " - "aggregate's 'inverse' method"); + "unable to build arguments for user-defined aggregate's " + "'inverse' method"); goto exit; } @@ -1070,8 +1069,7 @@ inverse_callback(sqlite3_context *context, int argc, sqlite3_value **params) Py_DECREF(args); if (res == NULL) { set_sqlite_error(context, - "user-defined aggregate's 'inverse' method " - "raised error"); + "user-defined aggregate's 'inverse' method raised error"); goto exit; } Py_DECREF(res); @@ -1114,8 +1112,8 @@ value_callback(sqlite3_context *context) Py_DECREF(res); if (rc < 0) { set_sqlite_error(context, - "unable to set result from user-defined " - "aggregate's 'value' method"); + "unable to set result from user-defined aggregate's " + "'value' method"); } } @@ -1123,7 +1121,7 @@ value_callback(sqlite3_context *context) } /*[clinic input] -_sqlite3.Connection.create_window_function as pysqlite_connection_create_window_function +_sqlite3.Connection.create_window_function as create_window_function cls: defining_class name: str @@ -1145,15 +1143,11 @@ Creates or redefines an aggregate window function. Non-standard. [clinic start generated code]*/ static PyObject * -pysqlite_connection_create_window_function_impl(pysqlite_Connection *self, - PyTypeObject *cls, - const char *name, - int num_params, - PyObject *aggregate_class, - int deterministic, - int directonly, - int innocuous) -/*[clinic end generated code: output=f36d5595c0c930d5 input=34a9b57a2c411aea]*/ +create_window_function_impl(pysqlite_Connection *self, PyTypeObject *cls, + const char *name, int num_params, + PyObject *aggregate_class, int deterministic, + int directonly, int innocuous) +/*[clinic end generated code: output=32287d1dac62b1d3 input=66afbc429306d593]*/ { if (sqlite3_libversion_number() < 3025000) { PyErr_SetString(self->NotSupportedError, @@ -2196,7 +2190,6 @@ static PyMethodDef connection_methods[] = { PYSQLITE_CONNECTION_CREATE_AGGREGATE_METHODDEF PYSQLITE_CONNECTION_CREATE_COLLATION_METHODDEF PYSQLITE_CONNECTION_CREATE_FUNCTION_METHODDEF - PYSQLITE_CONNECTION_CREATE_WINDOW_FUNCTION_METHODDEF PYSQLITE_CONNECTION_CURSOR_METHODDEF PYSQLITE_CONNECTION_ENABLE_LOAD_EXTENSION_METHODDEF PYSQLITE_CONNECTION_ENTER_METHODDEF @@ -2213,6 +2206,7 @@ static PyMethodDef connection_methods[] = { PYSQLITE_CONNECTION_SET_TRACE_CALLBACK_METHODDEF SETLIMIT_METHODDEF GETLIMIT_METHODDEF + CREATE_WINDOW_FUNCTION_METHODDEF {NULL, NULL} }; From 61cdd90c192bea82551243803acc9ab9dda0616a Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Thu, 3 Mar 2022 22:34:25 +0100 Subject: [PATCH 24/40] Add tests for finalize errors, and missing finalize methods --- Lib/test/test_sqlite3/test_userfunctions.py | 50 +++++++++++++++------ Modules/_sqlite/connection.c | 4 ++ 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index 8d9680eec59fe2..5d0c56a6a9d9ff 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -27,9 +27,9 @@ import re import sys import unittest -import unittest.mock import sqlite3 as sqlite +from unittest.mock import Mock, patch from test.support import bigmemtest, catch_unraisable_exception, gc_collect from test.test_sqlite3.test_dbapi import cx_limit @@ -384,7 +384,7 @@ def append_result(arg): # indices, which allows testing based on syntax, iso. the query optimizer. @unittest.skipIf(sqlite.sqlite_version_info < (3, 8, 3), "Requires SQLite 3.8.3 or higher") def test_func_non_deterministic(self): - mock = unittest.mock.Mock(return_value=None) + mock = Mock(return_value=None) self.con.create_function("nondeterministic", 0, mock, deterministic=False) if sqlite.sqlite_version_info < (3, 15, 0): self.con.execute("select nondeterministic() = nondeterministic()") @@ -395,7 +395,7 @@ def test_func_non_deterministic(self): @unittest.skipIf(sqlite.sqlite_version_info < (3, 8, 3), "Requires SQLite 3.8.3 or higher") def test_func_deterministic(self): - mock = unittest.mock.Mock(return_value=None) + mock = Mock(return_value=None) self.con.create_function("deterministic", 0, mock, deterministic=True) if sqlite.sqlite_version_info < (3, 15, 0): self.con.execute("select deterministic() = deterministic()") @@ -483,7 +483,7 @@ def inverse(self, value): def finalize(self): return self.count -class WindowBogusException(Exception): +class BadWindow(Exception): pass @@ -529,24 +529,31 @@ def test_win_error_on_create(self): self.con.create_window_function, "shouldfail", -100, WindowSumInt) - @with_tracebacks(WindowBogusException) + @with_tracebacks(BadWindow) def test_win_exception_in_method(self): - # Note: SQLite does not propagate errors from the "finalize" callback. for meth in ["__init__", "step", "value", "inverse"]: with self.subTest(meth=meth): - with unittest.mock.patch.object(WindowSumInt, meth, - side_effect=WindowBogusException): + with patch.object(WindowSumInt, meth, side_effect=BadWindow): name = f"exc_{meth}" self.con.create_window_function(name, 1, WindowSumInt) - err_str = f"'{meth}' method raised error" - with self.assertRaisesRegex(sqlite.OperationalError, - err_str): + msg = f"'{meth}' method raised error" + with self.assertRaisesRegex(sqlite.OperationalError, msg): self.cur.execute(self.query % name) - ret = self.cur.fetchall() + self.cur.fetchall() + + @with_tracebacks(BadWindow) + def test_win_exception_in_finalize(self): + # Note: SQLite does not (as of version 3.38.0) propagate finalize + # callback errors to sqlite3_step(); this implies that OperationalError + # is _not_ raised. + with patch.object(WindowSumInt, "finalize", side_effect=BadWindow): + name = f"exception_in_finalize" + self.con.create_window_function(name, 1, WindowSumInt) + self.cur.execute(self.query % name) + self.cur.fetchall() @with_tracebacks(AttributeError) def test_win_missing_method(self): - # Note: SQLite does not propagate errors from the "finalize" callback. class MissingValue: def step(self, x): pass def inverse(self, x): pass @@ -577,6 +584,23 @@ def finalize(self): return 42 self.cur.execute(self.query % name) self.cur.fetchall() + @with_tracebacks(AttributeError) + def test_win_missing_finalize(self): + # Note: SQLite does not (as of version 3.38.0) propagate finalize + # callback errors to sqlite3_step(); this implies that OperationalError + # is _not_ raised. + name = "missing_finalize" + + class MissingFinalize: + def step(self, x): pass + def value(self): return 42 + def inverse(self, x): pass + + self.con.create_window_function(name, 1, MissingFinalize) + self.addCleanup(self.con.create_window_function, name, 1, None) + self.cur.execute(self.query % name) + self.cur.fetchall() + def test_win_clear_function(self): self.con.create_window_function("sumint", 1, None) self.assertRaises(sqlite.OperationalError, self.cur.execute, diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 7a60d759110714..df1254de6e00f1 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -823,6 +823,10 @@ final_callback(sqlite3_context *context) "raised error"; int attr_err = PyErr_ExceptionMatches(PyExc_AttributeError); _PyErr_ChainExceptions(exception, value, tb); + + /* Note: contrary to the step, value, and inverse callbacks, SQLite + * does not, as of SQLite 3.38.0, propagate errors to sqlite3_step() + * from the finalize callback. */ set_sqlite_error(context, attr_err ? attr_msg : err_msg); } else { From 6606760a93e965548211e9f61abfcf007bc98d55 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Thu, 3 Mar 2022 23:53:36 +0100 Subject: [PATCH 25/40] Don't use stdbool --- Modules/_sqlite/connection.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index df1254de6e00f1..68ec57a8ad0769 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -29,8 +29,6 @@ #include "prepare_protocol.h" #include "util.h" -#include - #if SQLITE_VERSION_NUMBER >= 3014000 #define HAVE_TRACE_V2 #endif @@ -1108,7 +1106,7 @@ value_callback(sqlite3_context *context) "not defined"; const char *err_msg = "user-defined aggregate's 'value' method " "raised error"; - bool attr_err = PyErr_ExceptionMatches(PyExc_AttributeError); + int attr_err = PyErr_ExceptionMatches(PyExc_AttributeError); set_sqlite_error(context, attr_err ? attr_msg : err_msg); } else { From bbf0babff6ab0c517984b40dc582c720b822c7c1 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Thu, 3 Mar 2022 23:54:07 +0100 Subject: [PATCH 26/40] Add keyword test --- Lib/test/test_sqlite3/test_dbapi.py | 2 ++ Lib/test/test_sqlite3/test_userfunctions.py | 24 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 4eb4e180bf117e..50c2304d509b64 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1029,6 +1029,8 @@ def test_check_connection_thread(self): lambda: self.con.setlimit(sqlite.SQLITE_LIMIT_LENGTH, -1), lambda: self.con.getlimit(sqlite.SQLITE_LIMIT_LENGTH), ] + if sqlite.sqlite_version_info >= (3, 25, 0): + fns.append(lambda: self.con.create_window_function("foo", 0, None)) for fn in fns: with self.subTest(fn=fn): self._run_test(fn) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index 5d0c56a6a9d9ff..c341042095e9b4 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -24,6 +24,7 @@ import contextlib import functools import io +import itertools import re import sys import unittest @@ -622,6 +623,29 @@ def value(self): return 1 << 65 self.assertRaisesRegex(sqlite.DataError, "string or blob too big", self.cur.execute, self.query % "err_val_ret") + def test_win_flags(self): + flags = ("deterministic", "directonly", "innocuous") + dataset = itertools.product([False, True], repeat=3) + for i, d in enumerate(dataset): + kwargs = {k: v for k, v in zip(flags, d)} + with self.subTest(kwargs=kwargs): + name = f"flags{i}" + + if sqlite.sqlite_version_info < (3, 30, 0): + notsupported = "directonly" + elif sqlite.sqlite_version_info < (3, 31, 0): + notsupported = "innocuous" + else: + notsupported = None + + if notsupported: + with self.assertRaises(sqlite.NotSupportedError, notsupported): + self.con.create_window_function(name, 1, WindowSumInt, + **kwargs) + else: + self.con.create_window_function(name, 1, WindowSumInt, **kwargs) + self.addCleanup(self.con.create_window_function, name, 1, None) + class AggregateTests(unittest.TestCase): def setUp(self): From a4e0eb60563ebe5a9a76bb9ef5bcbdededd7296f Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 00:00:22 +0100 Subject: [PATCH 27/40] Use static inline iso. macro --- Modules/_sqlite/connection.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 68ec57a8ad0769..21961cd1aebcda 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -926,13 +926,14 @@ do { \ } \ } while (0) -#define SET_FLAG(fl) \ -do { \ - assert(flags != NULL); \ - if (is_set) { \ - *flags |= fl; \ - } \ -} while (0) +static inline void +set_flag(int flag, int *dst, int is_set) +{ + assert(dst != NULL); + if (is_set) { + *dst |= flag; + } +} static int add_deterministic_flag_if_supported(pysqlite_Connection *self, int *flags, @@ -944,7 +945,7 @@ add_deterministic_flag_if_supported(pysqlite_Connection *self, int *flags, if (sqlite3_libversion_number() < 3008003) { NOT_SUPPORTED(self, "deterministic", "3.8.3"); } - SET_FLAG(SQLITE_DETERMINISTIC); + set_flag(SQLITE_DETERMINISTIC, flags, is_set); #endif return 0; } @@ -959,7 +960,7 @@ add_innocuous_flag_if_supported(pysqlite_Connection *self, int *flags, if (sqlite3_libversion_number() < 3031000) { NOT_SUPPORTED(self, "innocuous", "3.31.0"); } - SET_FLAG(SQLITE_INNOCUOUS); + set_flag(SQLITE_INNOCUOUS, flags, is_set); #endif return 0; } @@ -974,12 +975,11 @@ add_directonly_flag_if_supported(pysqlite_Connection *self, int *flags, if (sqlite3_libversion_number() < 3030000) { NOT_SUPPORTED(self, "directonly", "3.30.0"); } - SET_FLAG(SQLITE_DIRECTONLY); + set_flag(SQLITE_DIRECTONLY, flags, is_set); #endif return 0; } -#undef SET_FLAG #undef NOT_SUPPORTED /*[clinic input] From d31f51f6508e1b2628bfc7951357143feb474a37 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 00:18:35 +0100 Subject: [PATCH 28/40] No need to check if SQLITE_DETERMINISTIC is supported --- Lib/test/test_sqlite3/test_userfunctions.py | 6 ++-- Modules/_sqlite/connection.c | 32 +++++++++------------ 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index c341042095e9b4..69b2886a12826a 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -631,15 +631,15 @@ def test_win_flags(self): with self.subTest(kwargs=kwargs): name = f"flags{i}" - if sqlite.sqlite_version_info < (3, 30, 0): + if kwargs["directonly"] and sqlite.sqlite_version_info < (3, 30, 0): notsupported = "directonly" - elif sqlite.sqlite_version_info < (3, 31, 0): + elif kwargs["innocuous"] and sqlite.sqlite_version_info < (3, 31, 0): notsupported = "innocuous" else: notsupported = None if notsupported: - with self.assertRaises(sqlite.NotSupportedError, notsupported): + with self.assertRaisesRegex(sqlite.NotSupportedError, notsupported): self.con.create_window_function(name, 1, WindowSumInt, **kwargs) else: diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 21961cd1aebcda..87e81d77bda72c 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -935,21 +935,6 @@ set_flag(int flag, int *dst, int is_set) } } -static int -add_deterministic_flag_if_supported(pysqlite_Connection *self, int *flags, - int is_set) -{ -#if SQLITE_VERSION_NUMBER < 3008003 - NOT_SUPPORTED(self, "deterministic", "3.8.3"); -#else - if (sqlite3_libversion_number() < 3008003) { - NOT_SUPPORTED(self, "deterministic", "3.8.3"); - } - set_flag(SQLITE_DETERMINISTIC, flags, is_set); -#endif - return 0; -} - static int add_innocuous_flag_if_supported(pysqlite_Connection *self, int *flags, int is_set) @@ -1010,8 +995,19 @@ pysqlite_connection_create_function_impl(pysqlite_Connection *self, return NULL; } - if (add_deterministic_flag_if_supported(self, &flags, deterministic) < 0) { + if (deterministic) { +#if SQLITE_VERSION_NUMBER < 3008003 + PyErr_SetString(self->NotSupportedError, + "deterministic=True requires SQLite 3.8.3 or higher"); return NULL; +#else + if (sqlite3_libversion_number() < 3008003) { + PyErr_SetString(self->NotSupportedError, + "deterministic=True requires SQLite 3.8.3 or higher"); + return NULL; + } + flags |= SQLITE_DETERMINISTIC; +#endif } callback_context *ctx = create_callback_context(cls, func); if (ctx == NULL) { @@ -1163,8 +1159,8 @@ create_window_function_impl(pysqlite_Connection *self, PyTypeObject *cls, } int flags = SQLITE_UTF8; - if (add_deterministic_flag_if_supported(self, &flags, deterministic) < 0) { - return NULL; + if (deterministic) { + flags |= SQLITE_DETERMINISTIC; } if (add_directonly_flag_if_supported(self, &flags, directonly) < 0) { return NULL; From 2bb1ec2a1bf38503a6e815e404f502cbcb6488d4 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 13:07:11 +0100 Subject: [PATCH 29/40] Test that flags are keyword only --- Lib/test/test_sqlite3/test_userfunctions.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index 69b2886a12826a..6af79287811e79 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -623,6 +623,10 @@ def value(self): return 1 << 65 self.assertRaisesRegex(sqlite.DataError, "string or blob too big", self.cur.execute, self.query % "err_val_ret") + def test_win_flags_keyword_only(self): + with self.assertRaisesRegex(TypeError, "positional arguments"): + self.con.create_window_function("foo", 1, WindowSumInt, True) + def test_win_flags(self): flags = ("deterministic", "directonly", "innocuous") dataset = itertools.product([False, True], repeat=3) @@ -640,8 +644,7 @@ def test_win_flags(self): if notsupported: with self.assertRaisesRegex(sqlite.NotSupportedError, notsupported): - self.con.create_window_function(name, 1, WindowSumInt, - **kwargs) + self.con.create_window_function(name, 1, WindowSumInt, **kwargs) else: self.con.create_window_function(name, 1, WindowSumInt, **kwargs) self.addCleanup(self.con.create_window_function, name, 1, None) From 99b752fae270e9b533772e9b2524f469a10b6c68 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 14:59:56 +0100 Subject: [PATCH 30/40] Reduce PR: simplify API by excluding flags for now --- Lib/test/test_sqlite3/test_userfunctions.py | 29 +-------- Modules/_sqlite/clinic/connection.c.h | 20 +++--- Modules/_sqlite/connection.c | 69 +-------------------- 3 files changed, 10 insertions(+), 108 deletions(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index 6af79287811e79..8d26c6649a589b 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -590,13 +590,12 @@ def test_win_missing_finalize(self): # Note: SQLite does not (as of version 3.38.0) propagate finalize # callback errors to sqlite3_step(); this implies that OperationalError # is _not_ raised. - name = "missing_finalize" - class MissingFinalize: def step(self, x): pass def value(self): return 42 def inverse(self, x): pass + name = "missing_finalize" self.con.create_window_function(name, 1, MissingFinalize) self.addCleanup(self.con.create_window_function, name, 1, None) self.cur.execute(self.query % name) @@ -623,32 +622,6 @@ def value(self): return 1 << 65 self.assertRaisesRegex(sqlite.DataError, "string or blob too big", self.cur.execute, self.query % "err_val_ret") - def test_win_flags_keyword_only(self): - with self.assertRaisesRegex(TypeError, "positional arguments"): - self.con.create_window_function("foo", 1, WindowSumInt, True) - - def test_win_flags(self): - flags = ("deterministic", "directonly", "innocuous") - dataset = itertools.product([False, True], repeat=3) - for i, d in enumerate(dataset): - kwargs = {k: v for k, v in zip(flags, d)} - with self.subTest(kwargs=kwargs): - name = f"flags{i}" - - if kwargs["directonly"] and sqlite.sqlite_version_info < (3, 30, 0): - notsupported = "directonly" - elif kwargs["innocuous"] and sqlite.sqlite_version_info < (3, 31, 0): - notsupported = "innocuous" - else: - notsupported = None - - if notsupported: - with self.assertRaisesRegex(sqlite.NotSupportedError, notsupported): - self.con.create_window_function(name, 1, WindowSumInt, **kwargs) - else: - self.con.create_window_function(name, 1, WindowSumInt, **kwargs) - self.addCleanup(self.con.create_window_function, name, 1, None) - class AggregateTests(unittest.TestCase): def setUp(self): diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h index 8a055c511ec4d7..06d9484806403c 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -238,9 +238,7 @@ pysqlite_connection_create_function(pysqlite_Connection *self, PyTypeObject *cls #if defined(HAVE_WINDOW_FUNCTIONS) PyDoc_STRVAR(create_window_function__doc__, -"create_window_function($self, name, num_params, aggregate_class, /, *,\n" -" deterministic=False, directonly=False,\n" -" innocuous=False)\n" +"create_window_function($self, name, num_params, aggregate_class, /)\n" "--\n" "\n" "Creates or redefines an aggregate window function. Non-standard.\n" @@ -261,27 +259,23 @@ PyDoc_STRVAR(create_window_function__doc__, static PyObject * create_window_function_impl(pysqlite_Connection *self, PyTypeObject *cls, const char *name, int num_params, - PyObject *aggregate_class, int deterministic, - int directonly, int innocuous); + PyObject *aggregate_class); static PyObject * create_window_function(pysqlite_Connection *self, PyTypeObject *cls, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) { PyObject *return_value = NULL; - static const char * const _keywords[] = {"", "", "", "deterministic", "directonly", "innocuous", NULL}; - static _PyArg_Parser _parser = {"siO|$ppp:create_window_function", _keywords, 0}; + static const char * const _keywords[] = {"", "", "", NULL}; + static _PyArg_Parser _parser = {"siO:create_window_function", _keywords, 0}; const char *name; int num_params; PyObject *aggregate_class; - int deterministic = 0; - int directonly = 0; - int innocuous = 0; if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser, - &name, &num_params, &aggregate_class, &deterministic, &directonly, &innocuous)) { + &name, &num_params, &aggregate_class)) { goto exit; } - return_value = create_window_function_impl(self, cls, name, num_params, aggregate_class, deterministic, directonly, innocuous); + return_value = create_window_function_impl(self, cls, name, num_params, aggregate_class); exit: return return_value; @@ -894,4 +888,4 @@ getlimit(pysqlite_Connection *self, PyObject *arg) #ifndef PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF #define PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF #endif /* !defined(PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF) */ -/*[clinic end generated code: output=cee6f0d6fa9b61dd input=a9049054013a1b77]*/ +/*[clinic end generated code: output=a8039b35c44796d9 input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 87e81d77bda72c..7b3e4d2a777e58 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -917,56 +917,6 @@ destructor_callback(void *ctx) } } -#define NOT_SUPPORTED(con, name, ver) \ -do { \ - if (is_set) { \ - PyErr_SetString(con->NotSupportedError, \ - name "=True requires SQLite " ver "or higher"); \ - return -1; \ - } \ -} while (0) - -static inline void -set_flag(int flag, int *dst, int is_set) -{ - assert(dst != NULL); - if (is_set) { - *dst |= flag; - } -} - -static int -add_innocuous_flag_if_supported(pysqlite_Connection *self, int *flags, - int is_set) -{ -#if SQLITE_VERSION_NUMBER < 3031000 - NOT_SUPPORTED(self, "innocuous", "3.31.0"); -#else - if (sqlite3_libversion_number() < 3031000) { - NOT_SUPPORTED(self, "innocuous", "3.31.0"); - } - set_flag(SQLITE_INNOCUOUS, flags, is_set); -#endif - return 0; -} - -static int -add_directonly_flag_if_supported(pysqlite_Connection *self, int *flags, - int is_set) -{ -#if SQLITE_VERSION_NUMBER < 3030000 - NOT_SUPPORTED(self, "directonly", "3.30.0"); -#else - if (sqlite3_libversion_number() < 3030000) { - NOT_SUPPORTED(self, "directonly", "3.30.0"); - } - set_flag(SQLITE_DIRECTONLY, flags, is_set); -#endif - return 0; -} - -#undef NOT_SUPPORTED - /*[clinic input] _sqlite3.Connection.create_function as pysqlite_connection_create_function @@ -1132,10 +1082,6 @@ _sqlite3.Connection.create_window_function as create_window_function A class with step(), finalize(), value(), and inverse() methods. Set to None to clear the window function. / - * - deterministic: bool = False - directonly: bool = False - innocuous: bool = False Creates or redefines an aggregate window function. Non-standard. [clinic start generated code]*/ @@ -1143,9 +1089,8 @@ Creates or redefines an aggregate window function. Non-standard. static PyObject * create_window_function_impl(pysqlite_Connection *self, PyTypeObject *cls, const char *name, int num_params, - PyObject *aggregate_class, int deterministic, - int directonly, int innocuous) -/*[clinic end generated code: output=32287d1dac62b1d3 input=66afbc429306d593]*/ + PyObject *aggregate_class) +/*[clinic end generated code: output=5332cd9464522235 input=258eac1970a0947e]*/ { if (sqlite3_libversion_number() < 3025000) { PyErr_SetString(self->NotSupportedError, @@ -1159,16 +1104,6 @@ create_window_function_impl(pysqlite_Connection *self, PyTypeObject *cls, } int flags = SQLITE_UTF8; - if (deterministic) { - flags |= SQLITE_DETERMINISTIC; - } - if (add_directonly_flag_if_supported(self, &flags, directonly) < 0) { - return NULL; - } - if (add_innocuous_flag_if_supported(self, &flags, innocuous) < 0) { - return NULL; - } - int rc; if (Py_IsNone(aggregate_class)) { rc = sqlite3_create_window_function(self->db, name, num_params, flags, From 81287f21ffe701e7b437718e85dfb5ed119a967a Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 15:06:57 +0100 Subject: [PATCH 31/40] Remove keywords from docs --- Doc/library/sqlite3.rst | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 83c6365c32dd46..2f9f717dff973a 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -473,9 +473,7 @@ Connection Objects .. literalinclude:: ../includes/sqlite3/mysumaggr.py - .. method:: create_window_function(name, num_params, aggregate_class, /, *, - deterministic=False, innocuous=False, - directonly=False) + .. method:: create_window_function(name, num_params, aggregate_class, /) Creates a user-defined aggregate window function. Aggregate window functions are supported by SQLite 3.25.0 and higher. @@ -492,31 +490,13 @@ Connection Objects supported by SQLite: :class:`bytes`, :class:`str`, :class:`int`, :class:`float` and :const:`None`. - If *deterministic* is :const:`True`, the created function is marked as - `deterministic `_, which - allows SQLite to perform additional optimizations. - - If *innocuous* is :const:`True`, the created function is marked as - `innocuous `_, which - indicates to SQLite that it is unlikely to cause problems, even if - misused. This flag is supported by SQLite 3.31.0 or higher. - :exc:`NotSupportedError` will be raised if used with older - versions. - - If *directonly* is :const:`True`, the created function is marked as - `directonly `_, which - means that it may only be invoked from top-level SQL. This flag - is an SQLite security feature that is recommended for all - user-defined SQL functions. This flag is supported by SQLite - 3.30.0 or higher. :exc:`NotSupportedError` will be raised if used - with older versions. - .. versionadded:: 3.11 Example: .. literalinclude:: ../includes/sqlite3/sumintwindow.py + .. method:: create_collation(name, callable) Creates a collation with the specified *name* and *callable*. The callable will From c66c9923ce1d19e444b4994af8cb6a88ae8875dc Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 15:08:23 +0100 Subject: [PATCH 32/40] Remove useless include --- Lib/test/test_sqlite3/test_userfunctions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index 8d26c6649a589b..ed064de387080a 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -24,7 +24,6 @@ import contextlib import functools import io -import itertools import re import sys import unittest From f442f77a0f446f5152bbd4974e8caf6db4f6cafa Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 15:37:05 +0100 Subject: [PATCH 33/40] Raise ProgrammingError if unable to create fn --- Lib/test/test_sqlite3/test_userfunctions.py | 2 +- Modules/_sqlite/connection.c | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index ed064de387080a..bf65986123341c 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -525,7 +525,7 @@ def test_win_sum_int(self): self.assertEqual(self.cur.fetchall(), self.expected) def test_win_error_on_create(self): - self.assertRaises(sqlite.OperationalError, + self.assertRaises(sqlite.ProgrammingError, self.con.create_window_function, "shouldfail", -100, WindowSumInt) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 7b3e4d2a777e58..6f47c307754c66 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1124,8 +1124,9 @@ create_window_function_impl(pysqlite_Connection *self, PyTypeObject *cls, } if (rc != SQLITE_OK) { - PyErr_SetString(self->OperationalError, - "Error creating window function"); + // Errors are not set on the database connection, so we cannot + // use _pysqlite_seterror(). + PyErr_SetString(self->ProgrammingError, sqlite3_errstr(rc)); return NULL; } Py_RETURN_NONE; From cfc4d6fe7cc913fd4762bfb5fa0fddf3bb167b68 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 19:52:05 +0100 Subject: [PATCH 34/40] Reword What's New and NEWS --- Doc/whatsnew/3.11.rst | 4 ++-- .../next/Library/2020-05-24-23-52-03.bpo-40617.lycF9q.rst | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index d2451834ff1eab..8602a4cae9905d 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -321,8 +321,8 @@ sqlite3 Instead we leave it to the SQLite library to handle these cases. (Contributed by Erlend E. Aasland in :issue:`44092`.) -* Added :meth:`~sqlite3.Connection.create_window_function` to create aggregate - window functions. +* Add :meth:`~sqlite3.Connection.create_window_function` to + :class:`sqlite3.Connection` for creating aggregate window functions. (Contributed by Erlend E. Aasland in :issue:`34916`.) diff --git a/Misc/NEWS.d/next/Library/2020-05-24-23-52-03.bpo-40617.lycF9q.rst b/Misc/NEWS.d/next/Library/2020-05-24-23-52-03.bpo-40617.lycF9q.rst index 6fdcaf4ef73604..123b49ddb5ac69 100644 --- a/Misc/NEWS.d/next/Library/2020-05-24-23-52-03.bpo-40617.lycF9q.rst +++ b/Misc/NEWS.d/next/Library/2020-05-24-23-52-03.bpo-40617.lycF9q.rst @@ -1 +1,3 @@ -Add support for aggregate window functions in :mod:`sqlite3`. Patch by Erlend E. Aasland. +Add :meth:`~sqlite3.Connection.create_window_function` to +:class:`sqlite3.Connection` for creating aggregate window functions. +Patch by Erlend E. Aasland. From 9355614603eaf87484b5554d418e00dda16e36d8 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 20:26:02 +0100 Subject: [PATCH 35/40] Reword docs --- Doc/includes/sqlite3/sumintwindow.py | 8 +++---- Doc/library/sqlite3.rst | 33 ++++++++++++++++------------ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/Doc/includes/sqlite3/sumintwindow.py b/Doc/includes/sqlite3/sumintwindow.py index e1447799351657..0e915d6cc6ae68 100644 --- a/Doc/includes/sqlite3/sumintwindow.py +++ b/Doc/includes/sqlite3/sumintwindow.py @@ -7,19 +7,19 @@ def __init__(self): self.count = 0 def step(self, value): - """This callback adds a row to the current window.""" + """Adds a row to the current window.""" self.count += value def value(self): - """This callback returns the current value of the aggregate.""" + """Returns the current value of the aggregate.""" return self.count def inverse(self, value): - """This callback removes a row from the current window.""" + """Removes a row from the current window.""" self.count -= value def finalize(self): - """This callback returns the final value of the aggregate. + """Returns the final value of the aggregate. Any clean-up actions should be placed here. """ diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 2f9f717dff973a..eeebacf7f4c16b 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -475,20 +475,25 @@ Connection Objects .. method:: create_window_function(name, num_params, aggregate_class, /) - Creates a user-defined aggregate window function. Aggregate window - functions are supported by SQLite 3.25.0 and higher. - :exc:`NotSupportedError` will be raised if used with older - versions. - - The aggregate class must implement ``step`` and ``inverse`` - methods, which accept the number of parameters *num_params* (if - *num_params* is -1, the function may take any number of arguments), - and ``finalize`` and ``value`` methods which return the final and - the current result of the aggregate. - - The ``finalize`` and ``value`` methods can return any of the types - supported by SQLite: :class:`bytes`, :class:`str`, :class:`int`, - :class:`float` and :const:`None`. + Creates user-defined aggregate window function *name*. + + *aggregate_class* must implement the following methods: + + * ``step``: adds a row to the current window + * ``value``: returns the current value of the aggregate + * ``inverse``: removes a row from the current window + * ``finalize``: returns the final value of the aggregate + + ``step`` and ``value`` accept *num_params* number of parameters, + unless *num_params* is ``-1``, in which case they may take any number of + arguments. ``finalize`` and ``value`` can return any of the types + supported by SQLite: + :class:`bytes`, :class:`str`, :class:`int`, :class:`float`, and + :const:`None`. Call :meth:`create_window_function` with + *aggregate_class* set to :const:`None` to clear window function *name*. + + Aggregate window functions are supported by SQLite 3.25.0 and higher. + :exc:`NotSupportedError` will be raised if used with older versions. .. versionadded:: 3.11 From d5b816d2a47b62c62aeb6a14f91117f6ea583138 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 20:27:38 +0100 Subject: [PATCH 36/40] Reword docstring --- Modules/_sqlite/clinic/connection.c.h | 5 ++--- Modules/_sqlite/connection.c | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h index 06d9484806403c..3eb30eea89ee40 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -247,8 +247,7 @@ PyDoc_STRVAR(create_window_function__doc__, " The name of the SQL aggregate window function to be created or\n" " redefined.\n" " num_params\n" -" The number of arguments that the SQL aggregate window function\n" -" takes.\n" +" The number of arguments the step and inverse methods takes.\n" " aggregate_class\n" " A class with step(), finalize(), value(), and inverse() methods.\n" " Set to None to clear the window function."); @@ -888,4 +887,4 @@ getlimit(pysqlite_Connection *self, PyObject *arg) #ifndef PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF #define PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF #endif /* !defined(PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF) */ -/*[clinic end generated code: output=a8039b35c44796d9 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=17b01069e28ba5bd input=a9049054013a1b77]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 6f47c307754c66..db7a38e6ee749a 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1076,8 +1076,7 @@ _sqlite3.Connection.create_window_function as create_window_function The name of the SQL aggregate window function to be created or redefined. num_params: int - The number of arguments that the SQL aggregate window function - takes. + The number of arguments the step and inverse methods takes. aggregate_class: object A class with step(), finalize(), value(), and inverse() methods. Set to None to clear the window function. @@ -1090,7 +1089,7 @@ static PyObject * create_window_function_impl(pysqlite_Connection *self, PyTypeObject *cls, const char *name, int num_params, PyObject *aggregate_class) -/*[clinic end generated code: output=5332cd9464522235 input=258eac1970a0947e]*/ +/*[clinic end generated code: output=5332cd9464522235 input=46d57a54225b5228]*/ { if (sqlite3_libversion_number() < 3025000) { PyErr_SetString(self->NotSupportedError, From 22fdb3d29aaadc49804437052d7f18f11dbce5e5 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 20:39:48 +0100 Subject: [PATCH 37/40] Squeeze error handlers --- Modules/_sqlite/connection.c | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index db7a38e6ee749a..38564db50a2598 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -815,17 +815,15 @@ final_callback(sqlite3_context *context) Py_DECREF(function_result); } if (!ok) { - const char *attr_msg = "user-defined aggregate's 'finalize' method " - "not defined"; - const char *err_msg = "user-defined aggregate's 'finalize' method " - "raised error"; int attr_err = PyErr_ExceptionMatches(PyExc_AttributeError); _PyErr_ChainExceptions(exception, value, tb); /* Note: contrary to the step, value, and inverse callbacks, SQLite * does not, as of SQLite 3.38.0, propagate errors to sqlite3_step() * from the finalize callback. */ - set_sqlite_error(context, attr_err ? attr_msg : err_msg); + set_sqlite_error(context, attr_err + ? "user-defined aggregate's 'finalize' method not defined" + : "user-defined aggregate's 'finalize' method raised error"); } else { PyErr_Restore(exception, value, tb); @@ -1048,12 +1046,10 @@ value_callback(sqlite3_context *context) PyObject *res = PyObject_CallMethodNoArgs(*cls, ctx->state->str_value); if (res == NULL) { - const char *attr_msg = "user-defined aggregate's 'value' method " - "not defined"; - const char *err_msg = "user-defined aggregate's 'value' method " - "raised error"; int attr_err = PyErr_ExceptionMatches(PyExc_AttributeError); - set_sqlite_error(context, attr_err ? attr_msg : err_msg); + set_sqlite_error(context, attr_err + ? "user-defined aggregate's 'value' method not defined" + : "user-defined aggregate's 'value' method raised error"); } else { int rc = _pysqlite_set_result(context, res); From 217da20969a36c5cf09ec1452f6c703c0c4ec021 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 20:40:15 +0100 Subject: [PATCH 38/40] Expand explanatory comment --- Modules/_sqlite/connection.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 38564db50a2598..eab709344ea027 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -819,8 +819,9 @@ final_callback(sqlite3_context *context) _PyErr_ChainExceptions(exception, value, tb); /* Note: contrary to the step, value, and inverse callbacks, SQLite - * does not, as of SQLite 3.38.0, propagate errors to sqlite3_step() - * from the finalize callback. */ + * does _not_, as of SQLite 3.38.0, propagate errors to sqlite3_step() + * from the finalize callback. This implies that execute*() will not + * raise OperationalError, as it normally would. */ set_sqlite_error(context, attr_err ? "user-defined aggregate's 'finalize' method not defined" : "user-defined aggregate's 'finalize' method raised error"); From 6a789ac1fb15b19cdb44f34c68cf99ffba0472d2 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 4 Mar 2022 20:47:27 +0100 Subject: [PATCH 39/40] Clean up tests --- Lib/test/test_sqlite3/test_userfunctions.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index bf65986123341c..855086e662c803 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -512,12 +512,12 @@ def setUp(self): ("d", 12), ("e", 9), ] - self.query = (""" + self.query = """ select x, %s(y) over ( order by x rows between 1 preceding and 1 following ) as sum_y from test order by x - """) + """ self.con.create_window_function("sumint", 1, WindowSumInt) def test_win_sum_int(self): @@ -531,7 +531,7 @@ def test_win_error_on_create(self): @with_tracebacks(BadWindow) def test_win_exception_in_method(self): - for meth in ["__init__", "step", "value", "inverse"]: + for meth in "__init__", "step", "value", "inverse": with self.subTest(meth=meth): with patch.object(WindowSumInt, meth, side_effect=BadWindow): name = f"exc_{meth}" @@ -578,7 +578,6 @@ def finalize(self): return 42 with self.subTest(meth=meth, cls=cls): name = f"exc_{meth}" self.con.create_window_function(name, 1, cls) - self.addCleanup(self.con.create_window_function, name, 1, None) with self.assertRaisesRegex(sqlite.OperationalError, f"'{meth}' method not defined"): self.cur.execute(self.query % name) @@ -596,7 +595,6 @@ def inverse(self, x): pass name = "missing_finalize" self.con.create_window_function(name, 1, MissingFinalize) - self.addCleanup(self.con.create_window_function, name, 1, None) self.cur.execute(self.query % name) self.cur.fetchall() @@ -617,6 +615,7 @@ class ErrorValueReturn: def __init__(self): pass def step(self, x): pass def value(self): return 1 << 65 + self.con.create_window_function("err_val_ret", 1, ErrorValueReturn) self.assertRaisesRegex(sqlite.DataError, "string or blob too big", self.cur.execute, self.query % "err_val_ret") From 431ace8af2fc93bab6895f6c6128b25f206ab725 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Mon, 11 Apr 2022 22:14:40 +0200 Subject: [PATCH 40/40] Address review --- Lib/test/test_sqlite3/test_userfunctions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index c96220544c8086..0970b0378ad615 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -619,11 +619,15 @@ def test_win_clear_function(self): self.query % "sumint") def test_win_redefine_function(self): + # Redefine WindowSumInt; adjust the expected results accordingly. class Redefined(WindowSumInt): - pass + def step(self, value): self.count += value * 2 + def inverse(self, value): self.count -= value * 2 + expected = [(v[0], v[1]*2) for v in self.expected] + self.con.create_window_function("sumint", 1, Redefined) self.cur.execute(self.query % "sumint") - self.assertEqual(self.cur.fetchall(), self.expected) + self.assertEqual(self.cur.fetchall(), expected) def test_win_error_value_return(self): class ErrorValueReturn: