Skip to content

Commit

Permalink
- Exceptions in callbacks lead to the query being aborted now instead of
Browse files Browse the repository at this point in the history
  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.
  • Loading branch information
gh committed Jun 5, 2006
1 parent 137a45d commit 1e8bd36
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 71 deletions.
29 changes: 18 additions & 11 deletions doc/usage-guide.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

Expand Down Expand Up @@ -225,15 +224,18 @@ 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
SQLite's supported types. The callable accepts as single parameter the Python
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)
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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:

Expand All @@ -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:

Expand Down Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions pysqlite2/test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
30 changes: 12 additions & 18 deletions pysqlite2/test/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand All @@ -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')
Expand Down
61 changes: 43 additions & 18 deletions pysqlite2/test/userfunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class AggrNoStep:
def __init__(self):
pass

def finalize(self):
return 1

class AggrNoFinalize:
def __init__(self):
pass
Expand Down Expand Up @@ -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()")

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
62 changes: 51 additions & 11 deletions src/connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
}
}
Expand All @@ -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:
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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}
};

Expand Down
Loading

0 comments on commit 1e8bd36

Please sign in to comment.