-
-
Notifications
You must be signed in to change notification settings - Fork 31.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
bpo-34041: Added *deterministic* parameter to sqlite3.Connection.create_function(). #8086
bpo-34041: Added *deterministic* parameter to sqlite3.Connection.create_function(). #8086
Conversation
Modules/_sqlite/connection.c
Outdated
@@ -808,26 +808,43 @@ static void _pysqlite_drop_unused_cursor_references(pysqlite_Connection* self) | |||
Py_SETREF(self->cursors, new_list); | |||
} | |||
|
|||
#ifndef SQLITE_DETERMINISTIC | |||
#define SQLITE_DETERMINISTIC 0x800 | |||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you have to define the constant in Python? Does it mean that SQLite doesn't support the flag? I would prefer to raise an exception, rather than using an hardcoded constant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think an exception should raised if Python is compiled with SQLite version that doesn't support this feature but is runned with SQLite version that supports it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if you pass 0x800 to a SQLite which doesn't know/support this flag? I expect an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked it with SQLite 3.8.2 (the most ancient dockerized version I found). Function is created, but it's not marked as deterministic, so optimizations are not used, it's reflected in the added test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO it doesn't make sense to raise exception here because if deterministic
is requested, but created function is not marked as deterministic this only affects performance, but indeed, flag should not be passed if it's unsupported as it was working properly only by accident.
Modules/_sqlite/connection.c
Outdated
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "siO", kwlist, | ||
&name, &narg, &func)) | ||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "siO|p", kwlist, | ||
&name, &narg, &func, &deterministic)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it would be safer to use a keyword only parameter, to prevent bugs?
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
Modules/_sqlite/connection.c
Outdated
{ | ||
return NULL; | ||
} | ||
|
||
rc = sqlite3_create_function(self->db, name, narg, SQLITE_UTF8, (void*)func, _pysqlite_func_callback, NULL, NULL); | ||
if (deterministic && sqlite3_libversion_number() >= 3008003) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum, if the flag only has an impact on performance, IHMO it's ok to ignore it if SQLite doesn't support it. But I would prefer to use "#ifndef SQLITE_DETERMINISTIC" for this test. Something like:
#ifndef SQLITE_DETERMINISTIC
if (deterministic) eTextRep |= SQLITE_DETERMINISTIC;
#endif
If Python is built on old SQLite: no luck, you will never get deterministic functions. If you build Python on new SQLite but run old SQLite: SQLITE_DETERMINISTIC is ignored, it's fine. If you have new versions in both cases: you're lucky, and you get the optimization :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I carefully reread SQLite docs and it appears that non-deterministic functions can't be used in indexes, so it seems that exception should be raised after all.
Doc/library/sqlite3.rst
Outdated
@@ -337,17 +337,23 @@ 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would make the new boolean parameter a keyword-only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I made the same proposal :-) At the C level, it means using "$" instead of "|" before the parameter. Parameters after "$" are keyword-only parameters.
I have made the requested changes; please review again |
Thanks for making the requested changes! @vstinner: please review the changes made to this pull request. |
Doc/library/sqlite3.rst
Outdated
|
||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Indentation is off here:
.. versionchanged:: 3.8
The *deterministic* parameter was added.
@@ -0,0 +1,2 @@ | |||
Add the parameter *deterministic* to the | |||
sqlite3.Connection.create_function(). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better if we can link to the create_function()
documentation:
.. untested
:meth:`sqlite3.Connection.create_function`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we are missing "method" here:
the sqlite3.Connection.create_function() -> the sqlite3.Connection.create_function() method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And please add "Patch by Sergey Fedoseev.".
Doc/library/sqlite3.rst
Outdated
called as the SQL function. If *deterministic* is true, the created function | ||
is marked as `deterministic <https://sqlite.org/deterministic.html>`_, which | ||
allows SQLite to perform additional optimizations. This flag is supported by | ||
SQLite 3.8.3 or higher and has no effect on older versions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please mention that NotSupportedError
will be raised as well?
Modules/_sqlite/connection.c
Outdated
|
||
PyObject* func; | ||
char* name; | ||
int narg; | ||
int rc; | ||
int deterministic = 0; | ||
int eTextRep = SQLITE_UTF8; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style nit: We usually don't use camelCase convention in Python codebase.
@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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also add a simple test to make sure that deterministic
is a keyword-only argument?
self.con.create_function("deterministic", 0, int, True)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in the following test
Modules/_sqlite/connection.c
Outdated
"should be built with SQLite 3.8.3 or higher"); | ||
return NULL; | ||
#else | ||
if (sqlite3_libversion_number() < 3008003) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually don't check for runtime library version when doing feature detection, but I think this is better. (Note that the only other check was added only in 2016: 7bea234)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for the runtime check
Checking the runtime version is fine. SQLITE_VERSION_NUMBER is the build version. Using Linux packages, it's common that the machine building packages and the users installing packages use different versions.
It also seems risky to use the flag on old versions according to the discussion in this PR.
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
I have made the requested changes; please review again |
Thanks for making the requested changes! @berkerpeksag, @vstinner: please review the changes made to this pull request. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost good! I just have a minor request on the error message.
@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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in the following test
Modules/_sqlite/connection.c
Outdated
#else | ||
if (sqlite3_libversion_number() < 3008003) { | ||
PyErr_SetString(pysqlite_NotSupportedError, | ||
"requires SQLite 3.8.3 or higher"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would you mind to add "deterministic"? "deterministic requires SQLite 3.8.3 or higher"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe even "deterministic=True requires SQLite 3.8.3 or higher"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, =True is maybe even better (more explicit).
Modules/_sqlite/connection.c
Outdated
if (deterministic) { | ||
#if SQLITE_VERSION_NUMBER < 3008003 | ||
PyErr_SetString(pysqlite_NotSupportedError, | ||
"should be built with SQLite 3.8.3 or higher"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to reuse the same error message: "deterministic requires SQLite 3.8.3 or higher"
Modules/_sqlite/connection.c
Outdated
"requires SQLite 3.8.3 or higher"); | ||
return NULL; | ||
} | ||
else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"else {" can be removed, since the if block returns.
Modules/_sqlite/connection.c
Outdated
"should be built with SQLite 3.8.3 or higher"); | ||
return NULL; | ||
#else | ||
if (sqlite3_libversion_number() < 3008003) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for the runtime check
Checking the runtime version is fine. SQLITE_VERSION_NUMBER is the build version. Using Linux packages, it's common that the machine building packages and the users installing packages use different versions.
It also seems risky to use the flag on old versions according to the discussion in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
@serhiy-storchaka, @berkerpeksag: The change LGTM. Would you mind to also review the latest version? |
called as the SQL function. If *deterministic* is true, the created function | ||
is marked as `deterministic <https://sqlite.org/deterministic.html>`_, 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to suggest add a link to the NotSupportedError
documentation, then I noticed it wasn't documented. I've opened https://bugs.python.org/issue34061
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM too. I will wait for a day or two to let Serhiy have a chance to review the latest revision, and merge it.
Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM if LGT @berkerpeksag.
I just do not fully understand the test.
Lib/sqlite3/test/userfunctions.py
Outdated
@@ -275,6 +276,24 @@ def CheckAnyArguments(self): | |||
val = cur.fetchone()[0] | |||
self.assertEqual(val, 2) | |||
|
|||
@unittest.skipIf(sqlite.sqlite_version_info < (3, 8, 3), "deterministic parameter not supported") | |||
def CheckFuncDeterministic(self): | |||
mock = unittest.mock.Mock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why a mock is used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used to test how much times function is called. If function is non-deterministic, query can't be optimized, so function have to be called 2 times (and actually there is no need to call it at all for this query :-).
This test relies on SQLite's behavior in some degree, but I did't find out a better way to test it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for clarification.
Is it worth to add a test for deterministic=False
(or for not specified)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we can do something like the following instead of using mock:
def foo():
foo.calls.append(True)
foo.calls = []
Then we can check the call count as:
self.assertEqual(len(foo.calls), 1)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@serhiy-storchaka added test for it.
@berkerpeksag not sure why this could be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@berkerpeksag This is how I would write this test. But I just don't use mocks much. I think using a mock here is good.
@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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.con.create_function("deterministic", 0, int, deterministic=False)
should work even on SQLite < 3.8.3, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, just pushed the fix.
Lib/sqlite3/test/userfunctions.py
Outdated
@@ -275,6 +276,24 @@ def CheckAnyArguments(self): | |||
val = cur.fetchone()[0] | |||
self.assertEqual(val, 2) | |||
|
|||
@unittest.skipIf(sqlite.sqlite_version_info < (3, 8, 3), "deterministic parameter not supported") | |||
def CheckFuncDeterministic(self): | |||
mock = unittest.mock.Mock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@berkerpeksag This is how I would write this test. But I just don't use mocks much. I think using a mock here is good.
Lib/sqlite3/test/userfunctions.py
Outdated
@@ -275,6 +276,28 @@ def CheckAnyArguments(self): | |||
val = cur.fetchone()[0] | |||
self.assertEqual(val, 2) | |||
|
|||
def CheckFuncDeterministic(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this test can be split into three tests:
deterministic=False
deterministic=True
if SQLite is older than 3.8.3deterministic=True
if SQLite supports the feature
Lib/sqlite3/test/userfunctions.py
Outdated
self.con.execute(query) | ||
self.assertEqual(mock.call_count, 1) | ||
|
||
def CheckFuncDeterministicKwOnly(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Kw -> Keyword
Thanks! |
https://bugs.python.org/issue34041