PL/Python: Add event trigger support
authorPeter Eisentraut <peter@eisentraut.org>
Thu, 21 Aug 2025 07:15:55 +0000 (09:15 +0200)
committerPeter Eisentraut <peter@eisentraut.org>
Thu, 21 Aug 2025 07:21:11 +0000 (09:21 +0200)
Allow event triggers to be written in PL/Python.  It provides a TD
dictionary with some information about the event trigger.

Author: Euler Taveira <euler@eulerto.com>
Co-authored-by: Dimitri Fontaine <dimitri@2ndQuadrant.fr>
Reviewed-by: Pavel Stehule <pavel.stehule@gmail.com>
Discussion: https://www.postgresql.org/message-id/flat/03f03515-2068-4f5b-b357-8fb540883c38%40app.fastmail.com

doc/src/sgml/plpython.sgml
src/pl/plpython/expected/plpython_trigger.out
src/pl/plpython/plpy_exec.c
src/pl/plpython/plpy_exec.h
src/pl/plpython/plpy_main.c
src/pl/plpython/plpy_procedure.h
src/pl/plpython/sql/plpython_trigger.sql

index cb065bf5f88dbc4b904d4c7d407d5b7521e88d54..27c4467ba7da3f2857c5b820f35ec0ed9b88a0a7 100644 (file)
@@ -662,6 +662,14 @@ $$ LANGUAGE plpython3u;
    <secondary>in PL/Python</secondary>
   </indexterm>
 
+  <para>
+   <application>PL/Python</application> can be used to define trigger
+   functions.
+   <productname>PostgreSQL</productname> requires that a function that is to
+   be called as a trigger must be declared as a function with no arguments and
+   a return type of <literal>trigger</literal>.
+  </para>
+
   <para>
    When a function is used as a trigger, the dictionary
    <literal>TD</literal> contains trigger-related values:
@@ -769,6 +777,74 @@ $$ LANGUAGE plpython3u;
   </para>
  </sect1>
 
+ <sect1 id="plpython-event-trigger">
+  <title>Event Trigger Functions</title>
+
+  <indexterm zone="plpython-event-trigger">
+   <primary>event trigger</primary>
+   <secondary>in PL/Python</secondary>
+  </indexterm>
+
+  <para>
+   <application>PL/Python</application> can be used to define event triggers
+   (see also <xref linkend="event-triggers"/>).
+   <productname>PostgreSQL</productname> requires that a function that is to
+   be called as an event trigger must be declared as a function with no
+   arguments and a return type of <literal>event_trigger</literal>.
+  </para>
+
+  <para>
+   When a function is used as an event trigger, the dictionary
+   <literal>TD</literal> contains trigger-related values:
+
+   <variablelist>
+    <varlistentry>
+     <term><varname>TD["event"]</varname></term>
+     <listitem>
+      <para>
+       The event the trigger was fired for, as a string, for example
+       <literal>ddl_command_start</literal>.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><varname>TD["tag"]</varname></term>
+     <listitem>
+      <para>
+       The command tag for which the trigger was fired, as a string, for
+       example <literal>DROP TABLE</literal>.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+  </para>
+
+  <para>
+   <xref linkend="plpython-event-trigger-example"/> shows an example of an
+   event trigger function in <application>PL/Python</application>.
+  </para>
+
+  <example id="plpython-event-trigger-example">
+   <title>A <application>PL/Python</application> Event Trigger Function</title>
+
+   <para>
+    This example trigger simply raises a <literal>NOTICE</literal> message
+    each time a supported command is executed.
+   </para>
+
+<programlisting>
+CREATE OR REPLACE FUNCTION pysnitch() RETURNS event_trigger
+LANGUAGE plpython3u
+AS $$
+  plpy.notice("TD[event] => " + TD["event"] + " ; TD[tag] => " + TD["tag"]);
+$$;
+
+CREATE EVENT TRIGGER pysnitch ON ddl_command_start EXECUTE FUNCTION pysnitch();
+</programlisting>
+  </example>
+ </sect1>
+
  <sect1 id="plpython-database">
   <title>Database Access</title>
 
index 64eab2fa3f4b5266361195fb030fe5ecaff619e5..bd35b220c5eda38b3f915a420a327f047f10b930 100644 (file)
@@ -646,3 +646,30 @@ SELECT * FROM recursive_trigger_test;
   1 | 2
 (2 rows)
 
+-- event triggers
+CREATE OR REPLACE FUNCTION pysnitch() RETURNS event_trigger
+LANGUAGE plpython3u
+AS $$
+  plpy.notice("TD[event] => " + TD["event"] + " ; TD[tag] => " + TD["tag"]);
+$$;
+CREATE EVENT TRIGGER python_a_snitch ON ddl_command_start
+   EXECUTE PROCEDURE pysnitch();
+CREATE EVENT TRIGGER python_b_snitch ON ddl_command_end
+   EXECUTE PROCEDURE pysnitch();
+CREATE OR REPLACE FUNCTION foobar() RETURNS int LANGUAGE sql AS $$SELECT 1;$$;
+NOTICE:  TD[event] => ddl_command_start ; TD[tag] => CREATE FUNCTION
+NOTICE:  TD[event] => ddl_command_end ; TD[tag] => CREATE FUNCTION
+ALTER FUNCTION foobar() COST 77;
+NOTICE:  TD[event] => ddl_command_start ; TD[tag] => ALTER FUNCTION
+NOTICE:  TD[event] => ddl_command_end ; TD[tag] => ALTER FUNCTION
+DROP FUNCTION foobar();
+NOTICE:  TD[event] => ddl_command_start ; TD[tag] => DROP FUNCTION
+NOTICE:  TD[event] => ddl_command_end ; TD[tag] => DROP FUNCTION
+CREATE TABLE foo();
+NOTICE:  TD[event] => ddl_command_start ; TD[tag] => CREATE TABLE
+NOTICE:  TD[event] => ddl_command_end ; TD[tag] => CREATE TABLE
+DROP TABLE foo;
+NOTICE:  TD[event] => ddl_command_start ; TD[tag] => DROP TABLE
+NOTICE:  TD[event] => ddl_command_end ; TD[tag] => DROP TABLE
+DROP EVENT TRIGGER python_a_snitch;
+DROP EVENT TRIGGER python_b_snitch;
index 22835174b698778526ee9e48f62a94982de43cd3..fd06b9e0e4e98598bf2206a395d5a8b0b8ecad20 100644 (file)
@@ -9,6 +9,7 @@
 #include "access/htup_details.h"
 #include "access/xact.h"
 #include "catalog/pg_type.h"
+#include "commands/event_trigger.h"
 #include "commands/trigger.h"
 #include "executor/spi.h"
 #include "funcapi.h"
@@ -427,6 +428,47 @@ PLy_exec_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc)
    return rv;
 }
 
+/*
+ * event trigger subhandler
+ */
+void
+PLy_exec_event_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc)
+{
+   EventTriggerData *tdata;
+   PyObject   *volatile pltdata = NULL;
+
+   Assert(CALLED_AS_EVENT_TRIGGER(fcinfo));
+   tdata = (EventTriggerData *) fcinfo->context;
+
+   PG_TRY();
+   {
+       PyObject   *pltevent,
+                  *plttag;
+
+       pltdata = PyDict_New();
+       if (!pltdata)
+           PLy_elog(ERROR, NULL);
+
+       pltevent = PLyUnicode_FromString(tdata->event);
+       PyDict_SetItemString(pltdata, "event", pltevent);
+       Py_DECREF(pltevent);
+
+       plttag = PLyUnicode_FromString(GetCommandTagName(tdata->tag));
+       PyDict_SetItemString(pltdata, "tag", plttag);
+       Py_DECREF(plttag);
+
+       PLy_procedure_call(proc, "TD", pltdata);
+
+       if (SPI_finish() != SPI_OK_FINISH)
+           elog(ERROR, "SPI_finish() failed");
+   }
+   PG_FINALLY();
+   {
+       Py_XDECREF(pltdata);
+   }
+   PG_END_TRY();
+}
+
 /* helper functions for Python code execution */
 
 static PyObject *
index 68da1ffcb2ef12c33fb3f7476a476a1dea2ee9f8..f35eabbd8ee8e5b6799a08d9ee739ae40bd800ec 100644 (file)
@@ -9,5 +9,6 @@
 
 extern Datum PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc);
 extern HeapTuple PLy_exec_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc);
+extern void PLy_exec_event_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc);
 
 #endif                         /* PLPY_EXEC_H */
index 900c65e8da066d3581da30f8f455efbf259aaf4f..70fc2c9257a89862eea0ca051edfd1e3d2ab3853 100644 (file)
@@ -9,6 +9,7 @@
 #include "access/htup_details.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
+#include "commands/event_trigger.h"
 #include "commands/trigger.h"
 #include "executor/spi.h"
 #include "miscadmin.h"
@@ -240,6 +241,13 @@ plpython3_call_handler(PG_FUNCTION_ARGS)
            trv = PLy_exec_trigger(fcinfo, proc);
            retval = PointerGetDatum(trv);
        }
+       else if (CALLED_AS_EVENT_TRIGGER(fcinfo))
+       {
+           proc = PLy_procedure_get(funcoid, InvalidOid, PLPY_EVENT_TRIGGER);
+           exec_ctx->curr_proc = proc;
+           PLy_exec_event_trigger(fcinfo, proc);
+           retval = (Datum) 0;
+       }
        else
        {
            proc = PLy_procedure_get(funcoid, InvalidOid, PLPY_NOT_TRIGGER);
@@ -346,6 +354,9 @@ PLy_procedure_is_trigger(Form_pg_proc procStruct)
        case TRIGGEROID:
            ret = PLPY_TRIGGER;
            break;
+       case EVENT_TRIGGEROID:
+           ret = PLPY_EVENT_TRIGGER;
+           break;
        default:
            ret = PLPY_NOT_TRIGGER;
            break;
index 601b91d5d94e960a101d0798c5e6e6ae812782c6..3ef22844a9b7186550da62462565622eef686e82 100644 (file)
@@ -17,6 +17,7 @@ extern void init_procedure_caches(void);
 typedef enum PLyTrigType
 {
    PLPY_TRIGGER,
+   PLPY_EVENT_TRIGGER,
    PLPY_NOT_TRIGGER,
 } PLyTrigType;
 
index 440549c0785da91f78569a78591b4f3cfa3dfee4..e1a552e079fe854af5a767992337b4a21a256273 100644 (file)
@@ -492,3 +492,27 @@ CREATE TRIGGER recursive_trigger_trig
 INSERT INTO recursive_trigger_test VALUES (0, 0);
 UPDATE recursive_trigger_test SET a = 11 WHERE b = 0;
 SELECT * FROM recursive_trigger_test;
+
+
+-- event triggers
+
+CREATE OR REPLACE FUNCTION pysnitch() RETURNS event_trigger
+LANGUAGE plpython3u
+AS $$
+  plpy.notice("TD[event] => " + TD["event"] + " ; TD[tag] => " + TD["tag"]);
+$$;
+
+CREATE EVENT TRIGGER python_a_snitch ON ddl_command_start
+   EXECUTE PROCEDURE pysnitch();
+CREATE EVENT TRIGGER python_b_snitch ON ddl_command_end
+   EXECUTE PROCEDURE pysnitch();
+
+CREATE OR REPLACE FUNCTION foobar() RETURNS int LANGUAGE sql AS $$SELECT 1;$$;
+ALTER FUNCTION foobar() COST 77;
+DROP FUNCTION foobar();
+
+CREATE TABLE foo();
+DROP TABLE foo;
+
+DROP EVENT TRIGGER python_a_snitch;
+DROP EVENT TRIGGER python_b_snitch;