Extend plsample example to include a trigger handler.
authorTom Lane <tgl@sss.pgh.pa.us>
Thu, 7 Apr 2022 22:26:13 +0000 (18:26 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Thu, 7 Apr 2022 22:26:20 +0000 (18:26 -0400)
Mark Wong and Konstantina Skovola, reviewed by Chapman Flack

Discussion: https://postgr.es/m/Yd8Cz22eHi80XS30@workstation-mark-wong

src/test/modules/plsample/expected/plsample.out
src/test/modules/plsample/plsample.c
src/test/modules/plsample/sql/plsample.sql

index a0c318b6df55ffd7fd7f72651ae14db2c9264f1f..8ad5f7af14c9615a13be33d21ecf0525a2d687d4 100644 (file)
@@ -34,3 +34,84 @@ NOTICE:  argument: 0; name: a1; value: {foo,bar,hoge}
  
 (1 row)
 
+CREATE FUNCTION my_trigger_func() RETURNS trigger AS $$
+if TD_event == "INSERT"
+    return TD_NEW
+elseif TD_event == "UPDATE"
+    return TD_NEW
+else
+    return "OK"
+end
+$$ language plsample;
+CREATE TABLE my_table (num integer, description text);
+CREATE TRIGGER my_trigger_func BEFORE INSERT OR UPDATE ON my_table
+       FOR EACH ROW EXECUTE FUNCTION my_trigger_func();
+CREATE TRIGGER my_trigger_func2 AFTER INSERT OR UPDATE ON my_table
+       FOR EACH ROW EXECUTE FUNCTION my_trigger_func(8);
+INSERT INTO my_table (num, description)
+VALUES (1, 'first');
+NOTICE:  source text of function "my_trigger_func": 
+if TD_event == "INSERT"
+    return TD_NEW
+elseif TD_event == "UPDATE"
+    return TD_NEW
+else
+    return "OK"
+end
+
+NOTICE:  trigger name: my_trigger_func
+NOTICE:  trigger relation: my_table
+NOTICE:  trigger relation schema: public
+NOTICE:  triggered by INSERT
+NOTICE:  triggered BEFORE
+NOTICE:  triggered per row
+NOTICE:  source text of function "my_trigger_func": 
+if TD_event == "INSERT"
+    return TD_NEW
+elseif TD_event == "UPDATE"
+    return TD_NEW
+else
+    return "OK"
+end
+
+NOTICE:  trigger name: my_trigger_func2
+NOTICE:  trigger relation: my_table
+NOTICE:  trigger relation schema: public
+NOTICE:  triggered by INSERT
+NOTICE:  triggered AFTER
+NOTICE:  triggered per row
+NOTICE:  trigger arg[0]: 8
+UPDATE my_table
+SET description = 'first, modified once'
+WHERE num = 1;
+NOTICE:  source text of function "my_trigger_func": 
+if TD_event == "INSERT"
+    return TD_NEW
+elseif TD_event == "UPDATE"
+    return TD_NEW
+else
+    return "OK"
+end
+
+NOTICE:  trigger name: my_trigger_func
+NOTICE:  trigger relation: my_table
+NOTICE:  trigger relation schema: public
+NOTICE:  triggered by UPDATE
+NOTICE:  triggered BEFORE
+NOTICE:  triggered per row
+NOTICE:  source text of function "my_trigger_func": 
+if TD_event == "INSERT"
+    return TD_NEW
+elseif TD_event == "UPDATE"
+    return TD_NEW
+else
+    return "OK"
+end
+
+NOTICE:  trigger name: my_trigger_func2
+NOTICE:  trigger relation: my_table
+NOTICE:  trigger relation schema: public
+NOTICE:  triggered by UPDATE
+NOTICE:  triggered AFTER
+NOTICE:  triggered per row
+NOTICE:  trigger arg[0]: 8
index 72693f6ee5b35d1b533ca41ff132d7e4671394f4..780db7292a4c1876a050e35ae4fd7f1fce5d456a 100644 (file)
@@ -19,6 +19,7 @@
 #include "catalog/pg_type.h"
 #include "commands/event_trigger.h"
 #include "commands/trigger.h"
+#include "executor/spi.h"
 #include "funcapi.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
@@ -29,6 +30,7 @@ PG_MODULE_MAGIC;
 PG_FUNCTION_INFO_V1(plsample_call_handler);
 
 static Datum plsample_func_handler(PG_FUNCTION_ARGS);
+static HeapTuple plsample_trigger_handler(PG_FUNCTION_ARGS);
 
 /*
  * Handle function, procedure, and trigger calls.
@@ -38,6 +40,11 @@ plsample_call_handler(PG_FUNCTION_ARGS)
 {
    Datum       retval = (Datum) 0;
 
+   /*
+    * Many languages will require cleanup that happens even in the event of
+    * an error.  That can happen in the PG_FINALLY block.  If none is needed,
+    * this PG_TRY construct can be omitted.
+    */
    PG_TRY();
    {
        /*
@@ -51,6 +58,7 @@ plsample_call_handler(PG_FUNCTION_ARGS)
             * (TriggerData *) fcinfo->context includes the information of the
             * context.
             */
+           retval = PointerGetDatum(plsample_trigger_handler(fcinfo));
        }
        else if (CALLED_AS_EVENT_TRIGGER(fcinfo))
        {
@@ -58,6 +66,8 @@ plsample_call_handler(PG_FUNCTION_ARGS)
             * This function is called as an event trigger function, where
             * (EventTriggerData *) fcinfo->context includes the information
             * of the context.
+            *
+            * TODO: provide an example handler.
             */
        }
        else
@@ -101,9 +111,9 @@ plsample_func_handler(PG_FUNCTION_ARGS)
    FmgrInfo    result_in_func;
    int         numargs;
 
-   /* Fetch the source text of the function. */
-   pl_tuple = SearchSysCache(PROCOID,
-                             ObjectIdGetDatum(fcinfo->flinfo->fn_oid), 0, 0, 0);
+   /* Fetch the function's pg_proc entry. */
+   pl_tuple = SearchSysCache1(PROCOID,
+                              ObjectIdGetDatum(fcinfo->flinfo->fn_oid));
    if (!HeapTupleIsValid(pl_tuple))
        elog(ERROR, "cache lookup failed for function %u",
             fcinfo->flinfo->fn_oid);
@@ -185,3 +195,160 @@ plsample_func_handler(PG_FUNCTION_ARGS)
    ret = InputFunctionCall(&result_in_func, source, result_typioparam, -1);
    PG_RETURN_DATUM(ret);
 }
+
+/*
+ * plsample_trigger_handler
+ *
+ * Function called by the call handler for trigger execution.
+ */
+static HeapTuple
+plsample_trigger_handler(PG_FUNCTION_ARGS)
+{
+   TriggerData *trigdata = (TriggerData *) fcinfo->context;
+   char       *string;
+   volatile HeapTuple rettup;
+   HeapTuple   pl_tuple;
+   Datum       ret;
+   char       *source;
+   bool        isnull;
+   Form_pg_proc pl_struct;
+   char       *proname;
+   int         rc PG_USED_FOR_ASSERTS_ONLY;
+
+   /* Make sure this is being called from a trigger. */
+   if (!CALLED_AS_TRIGGER(fcinfo))
+       elog(ERROR, "not called by trigger manager");
+
+   /* Connect to the SPI manager */
+   if (SPI_connect() != SPI_OK_CONNECT)
+       elog(ERROR, "could not connect to SPI manager");
+
+   rc = SPI_register_trigger_data(trigdata);
+   Assert(rc >= 0);
+
+   /* Fetch the function's pg_proc entry. */
+   pl_tuple = SearchSysCache1(PROCOID,
+                              ObjectIdGetDatum(fcinfo->flinfo->fn_oid));
+   if (!HeapTupleIsValid(pl_tuple))
+       elog(ERROR, "cache lookup failed for function %u",
+            fcinfo->flinfo->fn_oid);
+
+   /*
+    * Code Retrieval
+    *
+    * Extract and print the source text of the function.  This can be used as
+    * a base for the function validation and execution.
+    */
+   pl_struct = (Form_pg_proc) GETSTRUCT(pl_tuple);
+   proname = pstrdup(NameStr(pl_struct->proname));
+   ret = SysCacheGetAttr(PROCOID, pl_tuple, Anum_pg_proc_prosrc, &isnull);
+   if (isnull)
+       elog(ERROR, "could not find source text of function \"%s\"",
+            proname);
+   source = DatumGetCString(DirectFunctionCall1(textout, ret));
+   ereport(NOTICE,
+           (errmsg("source text of function \"%s\": %s",
+                   proname, source)));
+
+   /*
+    * We're done with the pg_proc tuple, so release it.  (Note that the
+    * "proname" and "source" strings are now standalone copies.)
+    */
+   ReleaseSysCache(pl_tuple);
+
+   /*
+    * Code Augmentation
+    *
+    * The source text may be augmented here, such as by wrapping it as the
+    * body of a function in the target language, prefixing a parameter list
+    * with names like TD_name, TD_relid, TD_table_name, TD_table_schema,
+    * TD_event, TD_when, TD_level, TD_NEW, TD_OLD, and args, using whatever
+    * types in the target language are convenient. The augmented text can be
+    * cached in a longer-lived memory context, or, if the target language
+    * uses a compilation step, that can be done here, caching the result of
+    * the compilation.
+    */
+
+   /*
+    * Code Execution
+    *
+    * Here the function (the possibly-augmented source text, or the result of
+    * compilation if the target language uses such a step) should be
+    * executed, after binding values from the TriggerData struct to the
+    * appropriate parameters.
+    *
+    * In this example we just print a lot of info via ereport.
+    */
+
+   PG_TRY();
+   {
+       ereport(NOTICE,
+               (errmsg("trigger name: %s", trigdata->tg_trigger->tgname)));
+       string = SPI_getrelname(trigdata->tg_relation);
+       ereport(NOTICE, (errmsg("trigger relation: %s", string)));
+
+       string = SPI_getnspname(trigdata->tg_relation);
+       ereport(NOTICE, (errmsg("trigger relation schema: %s", string)));
+
+       /* Example handling of different trigger aspects. */
+
+       if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
+       {
+           ereport(NOTICE, (errmsg("triggered by INSERT")));
+           rettup = trigdata->tg_trigtuple;
+       }
+       else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
+       {
+           ereport(NOTICE, (errmsg("triggered by DELETE")));
+           rettup = trigdata->tg_trigtuple;
+       }
+       else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
+       {
+           ereport(NOTICE, (errmsg("triggered by UPDATE")));
+           rettup = trigdata->tg_trigtuple;
+       }
+       else if (TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event))
+       {
+           ereport(NOTICE, (errmsg("triggered by TRUNCATE")));
+           rettup = trigdata->tg_trigtuple;
+       }
+       else
+           elog(ERROR, "unrecognized event: %u", trigdata->tg_event);
+
+       if (TRIGGER_FIRED_BEFORE(trigdata->tg_event))
+           ereport(NOTICE, (errmsg("triggered BEFORE")));
+       else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
+           ereport(NOTICE, (errmsg("triggered AFTER")));
+       else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event))
+           ereport(NOTICE, (errmsg("triggered INSTEAD OF")));
+       else
+           elog(ERROR, "unrecognized when: %u", trigdata->tg_event);
+
+       if (TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
+           ereport(NOTICE, (errmsg("triggered per row")));
+       else if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
+           ereport(NOTICE, (errmsg("triggered per statement")));
+       else
+           elog(ERROR, "unrecognized level: %u", trigdata->tg_event);
+
+       /*
+        * Iterate through all of the trigger arguments, printing each input
+        * value.
+        */
+       for (int i = 0; i < trigdata->tg_trigger->tgnargs; i++)
+           ereport(NOTICE,
+                   (errmsg("trigger arg[%i]: %s", i,
+                           trigdata->tg_trigger->tgargs[i])));
+   }
+   PG_CATCH();
+   {
+       /* Error cleanup code would go here */
+       PG_RE_THROW();
+   }
+   PG_END_TRY();
+
+   if (SPI_finish() != SPI_OK_FINISH)
+       elog(ERROR, "SPI_finish() failed");
+
+   return rettup;
+}
index bf0fddac7fc8ee5f9377a5692fe9992caec47525..cf652ad56fe16ec7afaf24ca2a5fe757c9aeec9e 100644 (file)
@@ -13,3 +13,26 @@ AS $$
   Example of source with void result.
 $$ LANGUAGE plsample;
 SELECT plsample_result_void('{foo, bar, hoge}');
+
+CREATE FUNCTION my_trigger_func() RETURNS trigger AS $$
+if TD_event == "INSERT"
+    return TD_NEW
+elseif TD_event == "UPDATE"
+    return TD_NEW
+else
+    return "OK"
+end
+$$ language plsample;
+
+CREATE TABLE my_table (num integer, description text);
+CREATE TRIGGER my_trigger_func BEFORE INSERT OR UPDATE ON my_table
+       FOR EACH ROW EXECUTE FUNCTION my_trigger_func();
+CREATE TRIGGER my_trigger_func2 AFTER INSERT OR UPDATE ON my_table
+       FOR EACH ROW EXECUTE FUNCTION my_trigger_func(8);
+
+INSERT INTO my_table (num, description)
+VALUES (1, 'first');
+
+UPDATE my_table
+SET description = 'first, modified once'
+WHERE num = 1;