Skip to content

Commit

Permalink
Allow multiple trace/profile callbacks
Browse files Browse the repository at this point in the history
Fixes #502

This also has some changes that won't be noticed:

* The callbacks are always called even if there are pending
  exceptions (they get chained) so no more unraisable.

* There was a SEGV if using SQLITE_TRACE_CLOSE and there were
  no references to the Connection.
  • Loading branch information
rogerbinns committed Oct 7, 2024
1 parent 016e0a3 commit cbb7e6c
Show file tree
Hide file tree
Showing 6 changed files with 335 additions and 146 deletions.
15 changes: 12 additions & 3 deletions apsw/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1777,9 +1777,14 @@ class Connection:

totalchanges = total_changes ## OLD-NAME

def trace_v2(self, mask: int, callback: Optional[Callable[[dict], None]] = None) -> None:
"""Registers a trace callback. The callback is called with a dict of relevant values based
on the code.
def trace_v2(self, mask: int, callback: Optional[Callable[[dict], None]] = None, *, id: Optional[Any] = None) -> None:
"""Registers a trace callback. Multiple traces can be active at once
(implemented by APSW). A callback of :class:`None` unregisters a
trace. Registered callbacks are distinguished by their ``id`` - an
equality test is done to match ids.
The callback is called with a dict of relevant values based on the
code.
.. list-table::
:header-rows: 1
Expand Down Expand Up @@ -1808,6 +1813,10 @@ class Connection:
The counters are reset each time a statement
starts execution.
Note that SQLite ignores any errors from the trace callbacks, so
whatever was being traced will still proceed. Exceptions will be
delivered when your Python code resumes.
.. seealso::
* :ref:`Example <example_trace_v2>`
Expand Down
56 changes: 54 additions & 2 deletions apsw/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3255,6 +3255,8 @@ def profile(*args):
wasrun = [False]

def profile(*args):
# should still be run despite there being a pending exception
# from the update hook
wasrun[0] = True

def uh(*args):
Expand All @@ -3263,7 +3265,7 @@ def uh(*args):
self.db.set_profile(profile)
self.db.set_update_hook(uh)
self.assertRaises(ZeroDivisionError, c.execute, "insert into foo values(3)")
self.assertEqual(wasrun[0], False)
self.assertEqual(wasrun[0], True)
self.db.set_profile(None)
self.db.set_update_hook(None)

Expand Down Expand Up @@ -6285,9 +6287,59 @@ def tracehook(x):
1 / 0

self.db.trace_v2(apsw.SQLITE_TRACE_STMT, tracehook)
self.assertRaisesUnraisable(ZeroDivisionError, self.db.execute, query)
self.assertRaises(ZeroDivisionError, self.db.execute, query)
self.assertEqual(0, len(results))

# added with id parameter
counter = [0]

def meth():
while True:
self.db.trace_v2(apsw.SQLITE_TRACE_STMT, meth, id=f"hello{counter[0]}")
counter[0] += 1

with contextlib.suppress(MemoryError):
meth()

self.assertGreater(counter[0], 1000)

# ensure all unregistered
for i in range(0, counter[0]+1):
self.db.trace_v2(0, None, id = f"hello{i}")
self.db.trace_v2(0, None) # tracehook zero div above

# should be fine
self.db.execute("select 3").get

# have exceptions - ensure all called
counter = [0]
for i in range(10):
def meth(*args):
counter[0] += 1
1/0
self.db.trace_v2(apsw.SQLITE_TRACE_ROW, meth, id = meth)

with contextlib.suppress(ZeroDivisionError):
self.db.execute("select 4").get

self.assertEqual(counter[0], 10)

# bad equals for id checking
class bad_equals:
def __eq__(self, other):
1 / 0

self.assertRaises(ZeroDivisionError, self.db.trace_v2, apsw.SQLITE_TRACE_ROW, bad_equals, id=bad_equals())

def harmless(*args):
counter[0] = 99
1/0

self.db.trace_v2(apsw.SQLITE_TRACE_CLOSE, harmless, id="jkhkjh")

with contextlib.suppress(ZeroDivisionError):
self.db.close()

def testURIFilenames(self):
assertRaises = self.assertRaises
assertEqual = self.assertEqual
Expand Down
3 changes: 3 additions & 0 deletions doc/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Added :func:`recursive triggers
<apsw.bestpractice.connection_recursive_triggers>` and :func:`optimize
<apsw.bestpractice.connection_optimize>` to :mod:`apsw.bestpractice`.

Multiple callbacks can be present for :meth:`Connection.trace_v2`
(:issue:`502`)

3.46.1.0
========

Expand Down
21 changes: 16 additions & 5 deletions src/apsw.docstrings
Original file line number Diff line number Diff line change
Expand Up @@ -2193,9 +2193,14 @@
#define Connection_total_changes_USAGE "Connection.total_changes() -> int"
#define Connection_total_changes_OLDDOC Connection_total_changes_USAGE "\n(Old less clear name totalchanges)"

#define Connection_trace_v2_DOC "trace_v2($self,mask,callback=None)\n--\n\nConnection.trace_v2(mask: int, callback: Optional[Callable[[dict], None]] = None) -> None\n\n" \
"Registers a trace callback. The callback is called with a dict of relevant values based\n" \
"on the code.\n" \
#define Connection_trace_v2_DOC "trace_v2($self,mask,callback=None,*,id=None)\n--\n\nConnection.trace_v2(mask: int, callback: Optional[Callable[[dict], None]] = None, *, id: Optional[Any] = None) -> None\n\n" \
"Registers a trace callback. Multiple traces can be active at once\n" \
"(implemented by APSW). A callback of :class:`None` unregisters a\n" \
"trace. Registered callbacks are distinguished by their ``id`` - an\n" \
"equality test is done to match ids.\n" \
"\n" \
"The callback is called with a dict of relevant values based on the\n" \
"code.\n" \
"\n" \
".. list-table::\n" \
" :header-rows: 1\n" \
Expand Down Expand Up @@ -2224,6 +2229,10 @@
" The counters are reset each time a statement\n" \
" starts execution.\n" \
"\n" \
"Note that SQLite ignores any errors from the trace callbacks, so\n" \
"whatever was being traced will still proceed. Exceptions will be\n" \
"delivered when your Python code resumes.\n" \
"\n" \
".. seealso::\n" \
"\n" \
" * :ref:`Example <example_trace_v2>`\n" \
Expand All @@ -2232,13 +2241,15 @@
" * `sqlite3_trace_v2 <https://sqlite.org/c3ref/trace_v2.html>`__\n" \
" * `sqlite3_stmt_status <https://sqlite.org/c3ref/stmt_status.html>`__\n"

#define Connection_trace_v2_KWNAMES "mask", "callback"
#define Connection_trace_v2_USAGE "Connection.trace_v2(mask: int, callback: Optional[Callable[[dict], None]] = None) -> None"
#define Connection_trace_v2_KWNAMES "mask", "callback", "id"
#define Connection_trace_v2_USAGE "Connection.trace_v2(mask: int, callback: Optional[Callable[[dict], None]] = None, *, id: Optional[Any] = None) -> None"

#define Connection_trace_v2_CHECK do { \
assert(__builtin_types_compatible_p(typeof(mask), int)); \
assert(__builtin_types_compatible_p(typeof(callback), PyObject *)); \
assert(callback == NULL); \
assert(__builtin_types_compatible_p(typeof(id), PyObject *)); \
assert(id == NULL); \
} while(0)


Expand Down
Loading

0 comments on commit cbb7e6c

Please sign in to comment.