From 1e8bd36be93b7d7425910642b72e4152c77b0dfd Mon Sep 17 00:00:00 2001 From: gh Date: Mon, 5 Jun 2006 01:04:33 +0000 Subject: [PATCH] - Exceptions in callbacks lead to the query being aborted now instead of silently leading to generating values. - Exceptions in callbacks can be echoed to stderr if you call the module level function enable_callback_tracebacks: enable_callback_tracebacks(1). - A new method "interrupt" of the connection object can be used to abort ongoing queries. Obviously, this only makes sense when called from a different thread. - A bug was fixed that would lead to crashes when multiple errors were happening during constructing values for one result row. - Converter names are no longer case-sensitive. --- doc/usage-guide.txt | 29 +++++++++------ pysqlite2/test/__init__.py | 1 + pysqlite2/test/types.py | 30 +++++++--------- pysqlite2/test/userfunctions.py | 61 ++++++++++++++++++++++---------- src/connection.c | 62 +++++++++++++++++++++++++++------ src/cursor.c | 42 ++++++++++++++++------ src/module.c | 38 ++++++++++++++++++-- src/module.h | 2 ++ 8 files changed, 194 insertions(+), 71 deletions(-) diff --git a/doc/usage-guide.txt b/doc/usage-guide.txt index c3ebfa9..f44679f 100644 --- a/doc/usage-guide.txt +++ b/doc/usage-guide.txt @@ -173,7 +173,6 @@ Python DB API. declared type, i. e. for "integer primary key", it will parse out "integer". Then for that column, it will look into pysqlite's converters dictionary and use the converter function registered for that type there. - Converter names are case-sensitive! * **sqlite.PARSE_COLNAMES** - This makes pysqlite parse the column name for each column it returns. It will look for a string formed @@ -188,7 +187,7 @@ Python DB API. The following example uses the column name *timestamp*, which is already registered by default in the converters dictionary with an appropriate - converter! Note that converter names are case-sensitive! + converter! Example: @@ -225,8 +224,7 @@ Python DB API. registers a callable to convert a bytestring from the database into a custom Python type. The converter will be invoked for all database values that are of the type ``typename``. Confer the parameter **detect_types** of the - **connect** method for how the type detection works. Note that the case - ``typename`` and the name of the type in your query must match! + **connect** method for how the type detection works. * **register_adapter** function - ``register_adapter(type, callable)`` registers a callable to convert the custom Python **type** into one of @@ -234,6 +232,10 @@ Python DB API. value, and must return a value of the following types: int, long, float, str (UTF-8 encoded), unicode or buffer. +* **enable_callback_tracebacks** function - ``enable_callback_tracebacks(flag)`` + Can be used to enable displaying tracebacks of exceptions in user-defined functions, aggregates and other callbacks being printed to stderr. + methods should never raise any exception. This feature is off by default. + * **Connection** class * **isolation_level** attribute (read-write) @@ -376,7 +378,7 @@ Python DB API. execute more than one statement with it, it will raise a Warning. Use *executescript* if want to execute multiple SQL statements with one call. -* **executescript** method + * **executescript** method .. code-block:: Python @@ -394,6 +396,12 @@ Python DB API. :language: Python :source-file: code/executescript.py + * **interrupt** method + + This method has no arguments. You can call it from a different thread to + abort any queries that are currently executing on the connection. This can + be used to let the user abort runaway queries, for example. + * **rowcount** attribute Although pysqlite's Cursors implement this attribute, the database @@ -573,8 +581,8 @@ functions with the connection's **create_function** method: the Python function The function can return any of pysqlite's supported SQLite types: unicode, - str, int, long, float, buffer and None. The function should never raise an - exception. + str, int, long, float, buffer and None. Any exception in the user-defined + function leads to the SQL statement executed being aborted. Example: @@ -597,8 +605,9 @@ create new aggregate functions with the connection's *create_aggregate* method. method which will return the final result of the aggregate. The *finalize* method can return any of pysqlite's supported SQLite types: - unicode, str, int, long, float, buffer and None. The aggregate class's - methods should never raise any exception. + unicode, str, int, long, float, buffer and None. Any exception in the + aggregate's *__init__*, *step* or *finalize* methods lead to the SQL + statement executed being aborted. Example: @@ -776,8 +785,6 @@ Let's first define a converter function that accepts the string as a parameter a !!! Note that converter functions *always* get called with a string, no matter under which data type you sent the value to SQLite !!! -!!! Also note that converter names are looked up in a case-sensitive manner !!! - .. code-block:: Python def convert_point(s): diff --git a/pysqlite2/test/__init__.py b/pysqlite2/test/__init__.py index f07ad15..bef87e5 100644 --- a/pysqlite2/test/__init__.py +++ b/pysqlite2/test/__init__.py @@ -24,6 +24,7 @@ import unittest from pysqlite2.test import dbapi, types, userfunctions, factory, transactions,\ hooks, regression +from pysqlite2 import dbapi2 as sqlite def suite(): return unittest.TestSuite( diff --git a/pysqlite2/test/types.py b/pysqlite2/test/types.py index 977044b..f142280 100644 --- a/pysqlite2/test/types.py +++ b/pysqlite2/test/types.py @@ -101,16 +101,16 @@ def setUp(self): self.cur.execute("create table test(i int, s str, f float, b bool, u unicode, foo foo, bin blob)") # override float, make them always return the same number - sqlite.converters["float"] = lambda x: 47.2 + sqlite.converters["FLOAT"] = lambda x: 47.2 # and implement two custom ones - sqlite.converters["bool"] = lambda x: bool(int(x)) - sqlite.converters["foo"] = DeclTypesTests.Foo + sqlite.converters["BOOL"] = lambda x: bool(int(x)) + sqlite.converters["FOO"] = DeclTypesTests.Foo def tearDown(self): - del sqlite.converters["float"] - del sqlite.converters["bool"] - del sqlite.converters["foo"] + del sqlite.converters["FLOAT"] + del sqlite.converters["BOOL"] + del sqlite.converters["FOO"] self.cur.close() self.con.close() @@ -208,14 +208,14 @@ def setUp(self): self.cur = self.con.cursor() self.cur.execute("create table test(x foo)") - sqlite.converters["foo"] = lambda x: "[%s]" % x - sqlite.converters["bar"] = lambda x: "<%s>" % x - sqlite.converters["exc"] = lambda x: 5/0 + sqlite.converters["FOO"] = lambda x: "[%s]" % x + sqlite.converters["BAR"] = lambda x: "<%s>" % x + sqlite.converters["EXC"] = lambda x: 5/0 def tearDown(self): - del sqlite.converters["foo"] - del sqlite.converters["bar"] - del sqlite.converters["exc"] + del sqlite.converters["FOO"] + del sqlite.converters["BAR"] + del sqlite.converters["EXC"] self.cur.close() self.con.close() @@ -231,12 +231,6 @@ def CheckNone(self): val = self.cur.fetchone()[0] self.failUnlessEqual(val, None) - def CheckExc(self): - # Exceptions in type converters result in returned Nones - self.cur.execute('select 5 as "x [exc]"') - val = self.cur.fetchone()[0] - self.failUnlessEqual(val, None) - def CheckColName(self): self.cur.execute("insert into test(x) values (?)", ("xxx",)) self.cur.execute('select x as "x [bar]" from test') diff --git a/pysqlite2/test/userfunctions.py b/pysqlite2/test/userfunctions.py index 7982147..f966122 100644 --- a/pysqlite2/test/userfunctions.py +++ b/pysqlite2/test/userfunctions.py @@ -55,6 +55,9 @@ class AggrNoStep: def __init__(self): pass + def finalize(self): + return 1 + class AggrNoFinalize: def __init__(self): pass @@ -144,9 +147,12 @@ def CheckFuncErrorOnCreate(self): def CheckFuncRefCount(self): def getfunc(): def f(): - return val + return 1 return f - self.con.create_function("reftest", 0, getfunc()) + f = getfunc() + globals()["foo"] = f + # self.con.create_function("reftest", 0, getfunc()) + self.con.create_function("reftest", 0, f) cur = self.con.cursor() cur.execute("select reftest()") @@ -195,9 +201,12 @@ def CheckFuncReturnBlob(self): def CheckFuncException(self): cur = self.con.cursor() - cur.execute("select raiseexception()") - val = cur.fetchone()[0] - self.failUnlessEqual(val, None) + try: + cur.execute("select raiseexception()") + cur.fetchone() + self.fail("should have raised OperationalError") + except sqlite.OperationalError, e: + self.failUnlessEqual(e.args[0], 'user-defined function raised exception') def CheckParamString(self): cur = self.con.cursor() @@ -267,31 +276,47 @@ def CheckAggrErrorOnCreate(self): def CheckAggrNoStep(self): cur = self.con.cursor() - cur.execute("select nostep(t) from test") + try: + cur.execute("select nostep(t) from test") + self.fail("should have raised an AttributeError") + except AttributeError, e: + self.failUnlessEqual(e.args[0], "AggrNoStep instance has no attribute 'step'") def CheckAggrNoFinalize(self): cur = self.con.cursor() - cur.execute("select nofinalize(t) from test") - val = cur.fetchone()[0] - self.failUnlessEqual(val, None) + try: + cur.execute("select nofinalize(t) from test") + val = cur.fetchone()[0] + self.fail("should have raised an OperationalError") + except sqlite.OperationalError, e: + self.failUnlessEqual(e.args[0], "user-defined aggregate's 'finalize' method raised error") def CheckAggrExceptionInInit(self): cur = self.con.cursor() - cur.execute("select excInit(t) from test") - val = cur.fetchone()[0] - self.failUnlessEqual(val, None) + try: + cur.execute("select excInit(t) from test") + val = cur.fetchone()[0] + self.fail("should have raised an OperationalError") + except sqlite.OperationalError, e: + self.failUnlessEqual(e.args[0], "user-defined aggregate's '__init__' method raised error") def CheckAggrExceptionInStep(self): cur = self.con.cursor() - cur.execute("select excStep(t) from test") - val = cur.fetchone()[0] - self.failUnlessEqual(val, 42) + try: + cur.execute("select excStep(t) from test") + val = cur.fetchone()[0] + self.fail("should have raised an OperationalError") + except sqlite.OperationalError, e: + self.failUnlessEqual(e.args[0], "user-defined aggregate's 'step' method raised error") def CheckAggrExceptionInFinalize(self): cur = self.con.cursor() - cur.execute("select excFinalize(t) from test") - val = cur.fetchone()[0] - self.failUnlessEqual(val, None) + try: + cur.execute("select excFinalize(t) from test") + val = cur.fetchone()[0] + self.fail("should have raised an OperationalError") + except sqlite.OperationalError, e: + self.failUnlessEqual(e.args[0], "user-defined aggregate's 'finalize' method raised error") def CheckAggrCheckParamStr(self): cur = self.con.cursor() diff --git a/src/connection.c b/src/connection.c index 64e43eb..e8ffee2 100644 --- a/src/connection.c +++ b/src/connection.c @@ -405,8 +405,6 @@ void _set_result(sqlite3_context* context, PyObject* py_val) PyObject* stringval; if ((!py_val) || PyErr_Occurred()) { - /* Errors in callbacks are ignored, and we return NULL */ - PyErr_Clear(); sqlite3_result_null(context); } else if (py_val == Py_None) { sqlite3_result_null(context); @@ -519,8 +517,17 @@ void _func_callback(sqlite3_context* context, int argc, sqlite3_value** argv) Py_DECREF(args); } - _set_result(context, py_retval); - Py_XDECREF(py_retval); + if (py_retval) { + _set_result(context, py_retval); + Py_DECREF(py_retval); + } else { + if (_enable_callback_tracebacks) { + PyErr_Print(); + } else { + PyErr_Clear(); + } + sqlite3_result_error(context, "user-defined function raised exception", -1); + } PyGILState_Release(threadstate); } @@ -545,8 +552,13 @@ static void _step_callback(sqlite3_context *context, int argc, sqlite3_value** p *aggregate_instance = PyObject_CallFunction(aggregate_class, ""); if (PyErr_Occurred()) { - PyErr_Clear(); *aggregate_instance = 0; + if (_enable_callback_tracebacks) { + PyErr_Print(); + } else { + PyErr_Clear(); + } + sqlite3_result_error(context, "user-defined aggregate's '__init__' method raised error", -1); goto error; } } @@ -565,7 +577,12 @@ static void _step_callback(sqlite3_context *context, int argc, sqlite3_value** p Py_DECREF(args); if (!function_result) { - PyErr_Clear(); + if (_enable_callback_tracebacks) { + PyErr_Print(); + } else { + PyErr_Clear(); + } + sqlite3_result_error(context, "user-defined aggregate's 'step' method raised error", -1); } error: @@ -597,13 +614,16 @@ void _final_callback(sqlite3_context* context) function_result = PyObject_CallMethod(*aggregate_instance, "finalize", ""); if (!function_result) { - PyErr_Clear(); - Py_INCREF(Py_None); - function_result = Py_None; + if (_enable_callback_tracebacks) { + PyErr_Print(); + } else { + PyErr_Clear(); + } + sqlite3_result_error(context, "user-defined aggregate's 'finalize' method raised error", -1); + } else { + _set_result(context, function_result); } - _set_result(context, function_result); - error: Py_XDECREF(*aggregate_instance); Py_XDECREF(function_result); @@ -974,6 +994,24 @@ collation_callback( return result; } +static PyObject * +connection_interrupt(Connection* self, PyObject* args) +{ + PyObject* retval = NULL; + + if (!check_connection(self)) { + goto finally; + } + + sqlite3_interrupt(self->db); + + Py_INCREF(Py_None); + retval = Py_None; + +finally: + return retval; +} + static PyObject * connection_create_collation(Connection* self, PyObject* args) { @@ -1075,6 +1113,8 @@ static PyMethodDef connection_methods[] = { PyDoc_STR("Executes a multiple SQL statements at once. Non-standard.")}, {"create_collation", (PyCFunction)connection_create_collation, METH_VARARGS, PyDoc_STR("Creates a collation function. Non-standard.")}, + {"interrupt", (PyCFunction)connection_interrupt, METH_NOARGS, + PyDoc_STR("Abort any pending database operation. Non-standard.")}, {NULL, NULL} }; diff --git a/src/cursor.c b/src/cursor.c index e686baf..8bae9d7 100644 --- a/src/cursor.c +++ b/src/cursor.c @@ -133,6 +133,18 @@ void cursor_dealloc(Cursor* self) self->ob_type->tp_free((PyObject*)self); } +PyObject* _get_converter(PyObject* key) +{ + PyObject* upcase_key; + + upcase_key = PyObject_CallMethod(key, "upper", ""); + if (!upcase_key) { + return NULL; + } + + return PyDict_GetItem(converters, upcase_key); +} + int build_row_cast_map(Cursor* self) { int i; @@ -170,7 +182,7 @@ int build_row_cast_map(Cursor* self) break; } - converter = PyDict_GetItem(converters, key); + converter = _get_converter(key); Py_DECREF(key); break; } @@ -191,7 +203,7 @@ int build_row_cast_map(Cursor* self) } } - converter = PyDict_GetItem(converters, py_decltype); + converter = _get_converter(py_decltype); Py_DECREF(py_decltype); } } @@ -311,13 +323,10 @@ PyObject* _fetch_one_row(Cursor* self) return NULL; } converted = PyObject_CallFunction(converter, "O", item); + Py_DECREF(item); if (!converted) { - /* TODO: have a way to log these errors */ - Py_INCREF(Py_None); - converted = Py_None; - PyErr_Clear(); + break; } - Py_DECREF(item); } } else { Py_BEGIN_ALLOW_THREADS @@ -345,10 +354,10 @@ PyObject* _fetch_one_row(Cursor* self) if (!converted) { colname = sqlite3_column_name(self->statement->st, i); - if (colname) { + if (!colname) { colname = ""; } - PyOS_snprintf(buf, sizeof(buf) - 1, "Could not decode to UTF-8 column %s with text %s", + PyOS_snprintf(buf, sizeof(buf) - 1, "Could not decode to UTF-8 column '%s' with text '%s'", colname , val_str); PyErr_SetString(OperationalError, buf); } @@ -372,7 +381,12 @@ PyObject* _fetch_one_row(Cursor* self) } } - PyTuple_SetItem(row, i, converted); + if (converted) { + PyTuple_SetItem(row, i, converted); + } else { + Py_INCREF(Py_None); + PyTuple_SetItem(row, i, Py_None); + } } if (PyErr_Occurred()) { @@ -597,6 +611,14 @@ PyObject* _query_execute(Cursor* self, int multiple, PyObject* args) goto error; } } else { + if (PyErr_Occurred()) { + /* there was an error that occured in a user-defined callback */ + if (_enable_callback_tracebacks) { + PyErr_Print(); + } else { + PyErr_Clear(); + } + } _seterror(self->connection->db); goto error; } diff --git a/src/module.c b/src/module.c index 93956b7..45db0eb 100644 --- a/src/module.c +++ b/src/module.c @@ -40,6 +40,7 @@ PyObject* Error, *Warning, *InterfaceError, *DatabaseError, *InternalError, *NotSupportedError, *OptimizedUnicode; PyObject* converters; +int _enable_callback_tracebacks; static PyObject* module_connect(PyObject* self, PyObject* args, PyObject* kwargs) @@ -140,14 +141,42 @@ static PyObject* module_register_adapter(PyObject* self, PyObject* args, PyObjec static PyObject* module_register_converter(PyObject* self, PyObject* args, PyObject* kwargs) { - PyObject* name; + char* orig_name; + char* name = NULL; + char* c; PyObject* callable; + PyObject* retval = NULL; - if (!PyArg_ParseTuple(args, "OO", &name, &callable)) { + if (!PyArg_ParseTuple(args, "sO", &orig_name, &callable)) { return NULL; } - if (PyDict_SetItem(converters, name, callable) != 0) { + /* convert the name to lowercase */ + name = PyMem_Malloc(strlen(orig_name) + 2); + if (!name) { + goto error; + } + strcpy(name, orig_name); + for (c = name; *c != (char)0; c++) { + *c = (*c) & 0xDF; + } + + if (PyDict_SetItemString(converters, name, callable) != 0) { + goto error; + } + + Py_INCREF(Py_None); + retval = Py_None; +error: + if (name) { + PyMem_Free(name); + } + return retval; +} + +static PyObject* enable_callback_tracebacks(PyObject* self, PyObject* args, PyObject* kwargs) +{ + if (!PyArg_ParseTuple(args, "i", &_enable_callback_tracebacks)) { return NULL; } @@ -174,6 +203,7 @@ static PyMethodDef module_methods[] = { {"register_adapter", (PyCFunction)module_register_adapter, METH_VARARGS, PyDoc_STR("Registers an adapter with pysqlite's adapter registry. Non-standard.")}, {"register_converter", (PyCFunction)module_register_converter, METH_VARARGS, PyDoc_STR("Registers a converter with pysqlite. Non-standard.")}, {"adapt", (PyCFunction)psyco_microprotocols_adapt, METH_VARARGS, psyco_microprotocols_adapt_doc}, + {"enable_callback_tracebacks", (PyCFunction)enable_callback_tracebacks, METH_VARARGS, PyDoc_STR("Enable or disable callback functions throwing errors to stderr.")}, {NULL, NULL} }; @@ -302,6 +332,8 @@ PyMODINIT_FUNC init_sqlite(void) /* initialize the default converters */ converters_init(dict); + _enable_callback_tracebacks = 0; + /* Original comment form _bsddb.c in the Python core. This is also still * needed nowadays for Python 2.3/2.4. * diff --git a/src/module.h b/src/module.h index f3e2aa1..45f9c73 100644 --- a/src/module.h +++ b/src/module.h @@ -50,6 +50,8 @@ extern PyObject* time_sleep; */ extern PyObject* converters; +extern int _enable_callback_tracebacks; + #define PARSE_DECLTYPES 1 #define PARSE_COLNAMES 2 #endif