cpython icon indicating copy to clipboard operation
cpython copied to clipboard

sqlite3 - preserve error information from a user-defined function exception

Open pchemguy opened this issue 3 years ago • 5 comments

In accordance with sqlite3.enable_callback_tracebacks documentation, "By default you will not get any tracebacks in user-defined functions, aggregates, converters, authorizer callbacks etc." While it is possible to retrieve debugging information via sqlite3.enable_callback_tracebacks & sys.unraisablehook, often it would be sufficient to append the exception message to the canned response, avoiding the need for extra efforts. For example, the problem in the following code is clearly indicated by the actual exception message, whereas the canned message is not nearly as helpful:

import sqlite3

con = sqlite3.connect(":memory:")
con.create_function("dummy", 0, lambda __: 1)
sqlite3.enable_callback_tracebacks(True)  # Provides error details to stderr
try:
    con.execute("SELECT dummy();")
except sqlite3.OperationalError as err:
    print(err)  # Error details are not available, only a canned message.

As pointed out in this SO answer, it should be possible to appended the actual exception message to the canned response in the code block that processes the sqlite3.enable_callback_tracebacks flag.

pchemguy avatar Oct 31 '22 08:10 pchemguy

The original exception message is unfortunately not directly available for us. We'd have to extract the current in-flight exception using PyErr_Fetch(), and then use some other tricks to extract the exception message as a C string. Is this easily feasable using the C API, @iritkatriel?

erlend-aasland avatar Nov 08 '22 10:11 erlend-aasland

The only generic way to get the exception message is to call str() on the exception. You don't know how each exception type implements that or what fields it has.

iritkatriel avatar Nov 08 '22 10:11 iritkatriel

Yeah, that could work. Thanks.

erlend-aasland avatar Nov 08 '22 12:11 erlend-aasland

Proof-of-concept branch: https://github.com/erlend-aasland/cpython/pull/new/sqlite-callback-exceptions

erlend-aasland avatar Nov 08 '22 13:11 erlend-aasland

If we choose to implement this, it would make sense to alter some of the exception messages so they play better with the potentially added information. This is technically a change in the API, so we'll have to document it in the What's New document for 3.12. Also, note that we cannot backport enhancements.

erlend-aasland avatar Nov 08 '22 13:11 erlend-aasland

I think this is worth implementing; better error messages FTW. I'll create a PR and we'll get it into one of the next 3.13 alphas.

erlend-aasland avatar Jan 06 '24 00:01 erlend-aasland

I've resurrected my old branch and created a draft PR; I'll have a closer look at it in a few days.

erlend-aasland avatar Jan 06 '24 00:01 erlend-aasland

As the PR currently stands, we now get exceptions like this:

OperationalError: user-defined function raised exception: ZeroDivisionError: division by zero
OperationalError: user-defined function raised exception: Exception: 
OperationalError: user-defined aggregate's 'step' method raised error: Exception: 
OperationalError: user-defined aggregate's 'step' method raised error: ZeroDivisionError: division by zero
OperationalError: user-defined aggregate's 'step' method raised error: AttributeError: 'Empty' object has no attribute 'step'

Another option could be to add the underlying exception (for example ZeroDivisionError) as a member to the OperationalError, similar to how we deal with SQLite error codes.

erlend-aasland avatar Jan 18 '24 12:01 erlend-aasland

With this feature, we now get duplicate information if we sqlite3.enable_callback_tracebacks(True). Also, I believe the information you get by setting enable_callback_tracebacks to True is better than what you get by concat'ing one exception on top of another. With that in mind, I'm inclined to reject this feature.

OTOH, what I would like to do, is to enable callback tracebacks by default; they are already outputted as unraisable exceptions, which makes for a better debug experience:

  • https://docs.python.org/3/library/sys.html#sys.unraisablehook

erlend-aasland avatar Jan 18 '24 12:01 erlend-aasland

I'm going to reject this feature and instead work on enabling unraisable exceptions by default. That is:

  • if an exception happens in a callback, raise an unraisable exception at once
  • make sqlite3.enable_callback_tracebacks a no-op

I'll create a topic on Discourse to get more input on this.

erlend-aasland avatar Jan 21 '24 20:01 erlend-aasland

Topic created:

  • https://discuss.python.org/t/enable-unraisable-exceptions-by-default-for-sqlite3-callbacks/43808

erlend-aasland avatar Jan 21 '24 21:01 erlend-aasland