From 0830858aeedecc9ece60349f8c31c2690d1a99f8 Mon Sep 17 00:00:00 2001 From: Sergey Fedoseev Date: Sun, 8 Jul 2018 12:09:20 +0500 Subject: [PATCH] bpo-34041: Allow creating deterministic functions in Connection.create_function() (GH-8086) --- Doc/library/sqlite3.rst | 11 +++++-- Lib/sqlite3/test/userfunctions.py | 23 +++++++++++++ .../2018-07-06-15-06-32.bpo-34041.0zrKLh.rst | 2 ++ Modules/_sqlite/connection.c | 32 ++++++++++++++++--- 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-07-06-15-06-32.bpo-34041.0zrKLh.rst diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index efc74a6bab8..d30e4d41579 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -337,17 +337,24 @@ Connection Objects :meth:`~Cursor.executescript` method with the given *sql_script*, and returns the cursor. - .. method:: create_function(name, num_params, func) + .. method:: create_function(name, num_params, func, *, deterministic=False) Creates a user-defined function that you can later use from within SQL statements under the function name *name*. *num_params* is the number of parameters the function accepts (if *num_params* is -1, the function may take any number of arguments), and *func* is a Python callable that is - called as the SQL function. + called as the SQL function. If *deterministic* is 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, ``sqlite3.NotSupportedError`` will be raised if used + with older versions. The function can return any of the types supported by SQLite: bytes, str, int, float and ``None``. + .. versionchanged:: 3.8 + The *deterministic* parameter was added. + Example: .. literalinclude:: ../includes/sqlite3/md5func.py diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index 4075045b727..9501f535c49 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -23,6 +23,7 @@ # 3. This notice may not be removed or altered from any source distribution. import unittest +import unittest.mock import sqlite3 as sqlite def func_returntext(): @@ -275,6 +276,28 @@ class FunctionTests(unittest.TestCase): val = cur.fetchone()[0] self.assertEqual(val, 2) + def CheckFuncNonDeterministic(self): + mock = unittest.mock.Mock(return_value=None) + self.con.create_function("deterministic", 0, mock, deterministic=False) + self.con.execute("select deterministic() = deterministic()") + self.assertEqual(mock.call_count, 2) + + @unittest.skipIf(sqlite.sqlite_version_info < (3, 8, 3), "deterministic parameter not supported") + def CheckFuncDeterministic(self): + mock = unittest.mock.Mock(return_value=None) + self.con.create_function("deterministic", 0, mock, deterministic=True) + self.con.execute("select deterministic() = deterministic()") + self.assertEqual(mock.call_count, 1) + + @unittest.skipIf(sqlite.sqlite_version_info >= (3, 8, 3), "SQLite < 3.8.3 needed") + def CheckFuncDeterministicNotSupported(self): + with self.assertRaises(sqlite.NotSupportedError): + self.con.create_function("deterministic", 0, int, deterministic=True) + + def CheckFuncDeterministicKeywordOnly(self): + with self.assertRaises(TypeError): + self.con.create_function("deterministic", 0, int, True) + class AggregateTests(unittest.TestCase): def setUp(self): diff --git a/Misc/NEWS.d/next/Library/2018-07-06-15-06-32.bpo-34041.0zrKLh.rst b/Misc/NEWS.d/next/Library/2018-07-06-15-06-32.bpo-34041.0zrKLh.rst new file mode 100644 index 00000000000..c41876bc60c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-07-06-15-06-32.bpo-34041.0zrKLh.rst @@ -0,0 +1,2 @@ +Add the parameter *deterministic* to the +:meth:`sqlite3.Connection.create_function` method. Patch by Sergey Fedoseev. diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index ef2daeb0c25..b8470df7fb8 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -810,24 +810,48 @@ static void _pysqlite_drop_unused_cursor_references(pysqlite_Connection* self) PyObject* pysqlite_connection_create_function(pysqlite_Connection* self, PyObject* args, PyObject* kwargs) { - static char *kwlist[] = {"name", "narg", "func", NULL, NULL}; + static char *kwlist[] = {"name", "narg", "func", "deterministic", NULL}; PyObject* func; char* name; int narg; int rc; + int deterministic = 0; + int flags = SQLITE_UTF8; if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) { return NULL; } - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "siO", kwlist, - &name, &narg, &func)) + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "siO|$p", kwlist, + &name, &narg, &func, &deterministic)) { return NULL; } - rc = sqlite3_create_function(self->db, name, narg, SQLITE_UTF8, (void*)func, _pysqlite_func_callback, NULL, NULL); + if (deterministic) { +#if SQLITE_VERSION_NUMBER < 3008003 + PyErr_SetString(pysqlite_NotSupportedError, + "deterministic=True requires SQLite 3.8.3 or higher"); + 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(self->db, + name, + narg, + flags, + (void*)func, + _pysqlite_func_callback, + NULL, + NULL); if (rc != SQLITE_OK) { /* Workaround for SQLite bug: no error code or string is available here */