@@ -117,6 +117,9 @@ CommitHook = Callable[[], bool]
117117"""Commit hook is called with no arguments and should return True to abort the commit and False
118118to let it continue"""
119119
120+ PreupdateHook = Callable [[PreUpdate ], None ]
121+ """The hook is called with information about the update, and has no return value"""
122+
120123TokenizerResult = Iterable [str | tuple [str , ...] | tuple [int , int , * tuple [str , ...]]]
121124"""The return from a tokenizer is based on the include_offsets and
122125include_colocated parameters you provided, both defaulting to
@@ -367,7 +370,8 @@ memoryused = memory_used ## OLD-NAME
367370no_change : object
368371"""A sentinel value used to indicate no change in a value when
369372used with :meth:`VTCursor.ColumnNoChange`,
370- :meth:`VTTable.UpdateChangeRow`, and :attr:`TableChange.new`."""
373+ :meth:`VTTable.UpdateChangeRow`, :attr:`TableChange.new`,
374+ and :class:`PreUpdate.update`."""
371375
372376def pyobject (object : Any ):
373377 """Indicates a Python object is being provided as a
@@ -1689,6 +1693,35 @@ class Connection:
16891693 * :ref:`Example <example_pragma>`"""
16901694 ...
16911695
1696+ def preupdate_hook (self , callback : Optional [PreupdateHook ], * , id : Optional [Any ] = None ) -> None :
1697+ """A callback just after a database row is updated. You can have multiple hooks at once
1698+ (managed by APSW) by specifying different ``id`` for each. Using :class:`None` for
1699+ ``callback`` will remove it.
1700+
1701+ SQLite provides no way to report errors from the callback. The SQLite level update
1702+ will always succeed, with Python exceptions reported when control returns to Python
1703+ code.
1704+
1705+ .. important::
1706+
1707+ The :doc:`session` extension uses the preupdate hook, and will **CRASH
1708+ THE PROCESS** if you register a hook via this method, and then create
1709+ a :class:`Session`.
1710+
1711+ SQLlite must be compiled with ``SQLITE_ENABLE_PREUPDATE_HOOK`` and this must be known
1712+ to APSW at compile time. If not, this API and :class:`PreUpdate` will not be present.
1713+
1714+ You do not get calls undoing changes when a transaction is
1715+ aborted/rolled back. Consequently you can't use this hook to track
1716+ the current state of the database. The approach taken by the
1717+ :doc:`session` is to note the rowid (or primary keys for without rowid
1718+ tables), and initial values the first time that a row is seen. When a
1719+ changeset is requested, it compares the contents of the row now to the row
1720+ then, and generates the appropriate changeset entry.
1721+
1722+ Calls: `sqlite3_preupdate_hook <https://sqlite.org/c3ref/preupdate_blobwrite.html>`__"""
1723+ ...
1724+
16921725 def read (self , schema : str , which : int , offset : int , amount : int ) -> tuple [bool , bytes ]:
16931726 """Invokes the underlying VFS method to read data from the database. It
16941727 is strongly recommended to read aligned complete pages, since that is
@@ -1831,13 +1864,16 @@ class Connection:
18311864
18321865 setbusytimeout = set_busy_timeout ## OLD-NAME
18331866
1834- def set_commit_hook (self , callable : Optional [CommitHook ]) -> None :
1867+ def set_commit_hook (self , callable : Optional [CommitHook ], * , id : Optional [ Any ] = None ) -> None :
18351868 """*callable* will be called just before a commit. It should return
18361869 False for the commit to go ahead and True for it to be turned
18371870 into a rollback. In the case of an exception in your callable, a
18381871 True (rollback) value is returned. Pass None to unregister
18391872 the existing hook.
18401873
1874+ You can have multiple hooks at once (managed by APSW) by specifying
1875+ different ``id`` for each one.
1876+
18411877 .. seealso::
18421878
18431879 * :ref:`Example <example_commit_hook>`
@@ -1895,12 +1931,15 @@ class Connection:
18951931
18961932 setprogresshandler = set_progress_handler ## OLD-NAME
18971933
1898- def set_rollback_hook (self , callable : Optional [Callable [[], None ]]) -> None :
1934+ def set_rollback_hook (self , callable : Optional [Callable [[], None ]], * , id : Optional [ Any ] = None ) -> None :
18991935 """Sets a callable which is invoked during a rollback. If *callable*
19001936 is *None* then any existing rollback hook is unregistered.
19011937
19021938 The *callable* is called with no parameters and the return value is ignored.
19031939
1940+ You can have multiple hooks at once (managed by APSW) by specifying
1941+ different ``id`` for each one.
1942+
19041943 Calls: `sqlite3_rollback_hook <https://sqlite.org/c3ref/commit_hook.html>`__"""
19051944 ...
19061945
@@ -2718,6 +2757,81 @@ class IndexInfo:
27182757 """Sets *omit* for *aConstraintUsage[which]*"""
27192758 ...
27202759
2760+ @final
2761+ class PreUpdate :
2762+ """Provides the details of one update to the
2763+ :meth:`Connection.preupdate_hook` callback.
2764+
2765+ .. note::
2766+
2767+ The object is only valid inside a the callback.
2768+ Using it outside the hook gives :exc:`InvalidContextError`.
2769+ You should copy all desired information in the callback."""
2770+
2771+ blob_write : int
2772+ """Writes to blobs show up as `DELETE`, with this having the
2773+ column number being rewritten. The value is negative if
2774+ no blob is being written.
2775+
2776+ Only the old value is available. To get the new value you have
2777+ to query the database.
2778+
2779+ Calls: `sqlite3_preupdate_blobwrite <https://sqlite.org/c3ref/preupdate_blobwrite.html>`__"""
2780+
2781+ connection : Connection
2782+ """The :class:`Connection` the preupdate is called on."""
2783+
2784+ database_name : str
2785+ """``main``, ``temp``, the name of an attached database."""
2786+
2787+ depth : int
2788+ """0 for direct SQL, 1 for triggers, 2 and so on for triggers
2789+ firing by a higher level trigger.
2790+
2791+ Calls: `sqlite3_preupdate_depth <https://sqlite.org/c3ref/preupdate_blobwrite.html>`__"""
2792+
2793+ new : tuple [SQLiteValue , ...] | None
2794+ """Row values for an INSERT, or after an UPDATE. :class:`None` for
2795+ DELETE. See also :attr:`old` and :attr:`update`.
2796+
2797+ Calls: `sqlite3_preupdate_new <https://sqlite.org/c3ref/preupdate_blobwrite.html>`__"""
2798+
2799+ old : tuple [SQLiteValue , ...] | None
2800+ """Row values for a DELETE, or before an UPDATE. :class:`None` for
2801+ INSERT. See also :attr:`new` and :attr:`update`.
2802+
2803+ Calls: `sqlite3_preupdate_old <https://sqlite.org/c3ref/preupdate_blobwrite.html>`__"""
2804+
2805+ op : str
2806+ """The operation code as a string ``INSERT``,
2807+ ``DELETE``, or ``UPDATE``. See :attr:`opcode`
2808+ for this as a number."""
2809+
2810+ opcode : int
2811+ """The operation code - ``apsw.SQLITE_INSERT``,
2812+ ``apsw.SQLITE_DELETE``, or ``apsw.SQLITE_UPDATE``.
2813+ See :attr:`op` for this as a string."""
2814+
2815+ rowid : int
2816+ """The affected rowid."""
2817+
2818+ rowid_new : int
2819+ """New rowid if changed via rowid UPDATE."""
2820+
2821+ table_name : str
2822+ """Table name."""
2823+
2824+ update : tuple [SQLiteValue | Literal [no_change ], ...] | None
2825+ """For UPDATE compares old and new values, providing the changed value,
2826+ or :attr:`apsw.no_change` if that column was not changed.
2827+
2828+ :class:`None` for INSERT and DELETE. See also :attr:`old` and
2829+ :attr:`new`.
2830+
2831+ Calls:
2832+ * `sqlite3_preupdate_old <https://sqlite.org/c3ref/preupdate_blobwrite.html>`__
2833+ * `sqlite3_preupdate_new <https://sqlite.org/c3ref/preupdate_blobwrite.html>`__"""
2834+
27212835@final
27222836class Rebaser :
27232837 """This object wraps a `sqlite3_rebaser
0 commit comments