Skip to content
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

Merged
merged 14 commits into from
Jul 8, 2018

Conversation

sir-sigurd
Copy link
Contributor

@sir-sigurd sir-sigurd commented Jul 4, 2018

@@ -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
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

@sir-sigurd sir-sigurd Jul 4, 2018

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.

if (!PyArg_ParseTupleAndKeywords(args, kwargs, "siO", kwlist,
&name, &narg, &func))
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "siO|p", kwlist,
&name, &narg, &func, &deterministic))
Copy link
Member

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?

@bedevere-bot
Copy link

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. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

{
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) {
Copy link
Member

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 :-)

Copy link
Contributor Author

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.

@@ -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)
Copy link
Member

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.

Copy link
Member

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.

@sir-sigurd
Copy link
Contributor Author

I have made the requested changes; please review again

@bedevere-bot
Copy link

Thanks for making the requested changes!

@vstinner: please review the changes made to this pull request.


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.
Copy link
Member

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().
Copy link
Member

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`

Copy link
Member

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

Copy link
Member

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.".

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.
Copy link
Member

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?


PyObject* func;
char* name;
int narg;
int rc;
int deterministic = 0;
int eTextRep = SQLITE_UTF8;
Copy link
Member

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)
Copy link
Member

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)

Copy link
Member

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

"should be built with SQLite 3.8.3 or higher");
return NULL;
#else
if (sqlite3_libversion_number() < 3008003) {
Copy link
Member

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)

Copy link
Member

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.

@bedevere-bot
Copy link

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. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@sir-sigurd
Copy link
Contributor Author

I have made the requested changes; please review again

@bedevere-bot
Copy link

Thanks for making the requested changes!

@berkerpeksag, @vstinner: please review the changes made to this pull request.

Copy link
Member

@vstinner vstinner left a 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)
Copy link
Member

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

#else
if (sqlite3_libversion_number() < 3008003) {
PyErr_SetString(pysqlite_NotSupportedError,
"requires SQLite 3.8.3 or higher");
Copy link
Member

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"

Copy link
Contributor Author

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"?

Copy link
Member

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).

if (deterministic) {
#if SQLITE_VERSION_NUMBER < 3008003
PyErr_SetString(pysqlite_NotSupportedError,
"should be built with SQLite 3.8.3 or higher");
Copy link
Member

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"

"requires SQLite 3.8.3 or higher");
return NULL;
}
else {
Copy link
Member

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.

"should be built with SQLite 3.8.3 or higher");
return NULL;
#else
if (sqlite3_libversion_number() < 3008003) {
Copy link
Member

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.

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@vstinner
Copy link
Member

vstinner commented Jul 6, 2018

@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
Copy link
Member

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

Copy link
Member

@berkerpeksag berkerpeksag left a 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!

Copy link
Member

@serhiy-storchaka serhiy-storchaka left a 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.

@@ -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()
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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)?

Copy link
Member

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)

Copy link
Contributor Author

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.

Copy link
Member

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)
Copy link
Member

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?

Copy link
Contributor Author

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.

@@ -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()
Copy link
Member

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.

@@ -275,6 +276,28 @@ def CheckAnyArguments(self):
val = cur.fetchone()[0]
self.assertEqual(val, 2)

def CheckFuncDeterministic(self):
Copy link
Member

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:

  1. deterministic=False
  2. deterministic=True if SQLite is older than 3.8.3
  3. deterministic=True if SQLite supports the feature

self.con.execute(query)
self.assertEqual(mock.call_count, 1)

def CheckFuncDeterministicKwOnly(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Kw -> Keyword

@berkerpeksag berkerpeksag merged commit 0830858 into python:master Jul 8, 2018
@berkerpeksag
Copy link
Member

Thanks!

@sir-sigurd sir-sigurd deleted the sqlite-deterministic-function branch July 8, 2018 07:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants