Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1n7gZ7-0003HP-Ph for pgsql-hackers@arkaria.postgresql.org; Wed, 12 Jan 2022 16:33:38 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.92) (envelope-from ) id 1n7gZ6-00043e-Bq for pgsql-hackers@arkaria.postgresql.org; Wed, 12 Jan 2022 16:33:36 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1n7gZ5-00043V-Q8 for pgsql-hackers@lists.postgresql.org; Wed, 12 Jan 2022 16:33:36 +0000 Received: from mail-ed1-x529.google.com ([2a00:1450:4864:20::529]) by makus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.92) (envelope-from ) id 1n7gZ3-0002wd-12 for pgsql-hackers@lists.postgresql.org; Wed, 12 Jan 2022 16:33:34 +0000 Received: by mail-ed1-x529.google.com with SMTP id u25so12274582edf.1 for ; Wed, 12 Jan 2022 08:33:32 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=date:from:to:cc:subject:message-id:mime-version:content-disposition; bh=S1QsWrTkbzS6yFzsPlc7Gzd0N9hgaF750sW34ByoUPY=; b=ic1Z+YV6yR5w5z9WWOAAhJRPXfBrZ6xYJkXrB9ukKae9uFnsqdZjgP8jBNqxi+J9DB B7jcwb0WWvn0z737jvDIHgfm2MuS22sPf4YFqSh/i2W/7O4gzpmZgX8QImFjSpLREyHv bFd0n8NiCwLubgq1M2J/3Ggc0F1JuFR3wqYphr/35MQxf2qVdTlSo339Uz3H+uuGJI0e Lp3/qwVkEwqDqNF9jBMsP37VFzauR59xju0/tKTSd2S7PYTSJewq0O8iopuT3fVF14zB 5BNBaJIpot5ptTEzwXg1SvBvGxK245m6ZFAd7f3Kw+PZ3kSP8JGnh+3VukEs1JDJvQ7N vxvA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:date:from:to:cc:subject:message-id:mime-version :content-disposition; bh=S1QsWrTkbzS6yFzsPlc7Gzd0N9hgaF750sW34ByoUPY=; b=C35hMy5QAnmSmT2gKS0mI8poCWSlpbSOMfMT474owcKqEH7qCTiptL4ifox6gU0rAG FiAcQlA24RkBkAwSyrhN5icO7mdbxY5V42Hh2aVRU9ecuDeCLFWGcM3QcmeTf7obrxm0 2oSLVT35AOTom5gIC80OShd/2gkbMmOhSyzRBNBWNlX83Lr0qKFfJOwWX22RSBYlkbWQ 1Rp184UGU2/2GM3yQ21zEPtXKjhLdNMDx0htlxQgcoX2Lax7IaGHykFyRk39+qfgN1mr aUFTKuyvM6EzGB+Fe1AF0fNlVPdmXOBwcx2MP0nBu49hBavu+Lmhf0q/nfWunILUTunr bqaA== X-Gm-Message-State: AOAM530X9WFVbJGGiml4ZcAqDjTFbHlvA9ELYi31I+Kedf2riPXvD6wc n2ZscvUkt5gHMIky0FAKZSIxHBe9ChksXg== X-Google-Smtp-Source: ABdhPJwg8g1KCAUO9jcnmY3JCpl2oQ3IAhtt9UTL19sKFot5oxYpicJ2sMyy/V1C5iCjVU5KruaSEg== X-Received: by 2002:a17:907:c0a:: with SMTP id ga10mr402596ejc.411.1642005210318; Wed, 12 Jan 2022 08:33:30 -0800 (PST) Received: from workstation-mark-wong (71-36-115-78.ptld.qwest.net. [71.36.115.78]) by smtp.gmail.com with ESMTPSA id hq35sm65349ejc.100.2022.01.12.08.33.28 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 12 Jan 2022 08:33:29 -0800 (PST) Date: Wed, 12 Jan 2022 08:33:19 -0800 From: Mark Wong To: pgsql-hackers@lists.postgresql.org Cc: Konstantina Skovola Subject: trigger example for plsample Message-ID: MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="7wuC2KwZkaclehjT" Content-Disposition: inline List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --7wuC2KwZkaclehjT Content-Type: text/plain; charset=us-ascii Content-Disposition: inline Hi everyone, I've adapted the work that Konstantina did for pl/julia as part of her GSOC project to add an example of handling triggers to plsample. Which was based from pl/tcl and pl/perl. One aspect that I'm not sure about is whether the example should be duplicating code (as it is now) for keeping an example contained within a single function. The only reason I can come up with is to try to read through an example with minimal jumping around. Hoping this is a good start. Regards, Mark --7wuC2KwZkaclehjT Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="plsample-trigger-v1.patch" diff --git a/src/test/modules/plsample/expected/plsample.out b/src/test/modules/plsample/expected/plsample.out index a0c318b6df..832db79b5c 100644 --- a/src/test/modules/plsample/expected/plsample.out +++ b/src/test/modules/plsample/expected/plsample.out @@ -34,3 +34,280 @@ 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'), (2, 'second'), (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: compile trigger function: my_trigger_func(TD_name, TD_relid, TD_table_name, TD_table_schema, TD_event, TD_when, TD_level, TD_NEW, TD_OLD, args) +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: out +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: compile trigger function: my_trigger_func(TD_name, TD_relid, TD_table_name, TD_table_schema, TD_event, TD_when, TD_level, TD_NEW, TD_OLD, args) +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: out +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: compile trigger function: my_trigger_func(TD_name, TD_relid, TD_table_name, TD_table_schema, TD_event, TD_when, TD_level, TD_NEW, TD_OLD, args) +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: out +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: compile trigger function: my_trigger_func(TD_name, TD_relid, TD_table_name, TD_table_schema, TD_event, TD_when, TD_level, TD_NEW, TD_OLD, args) +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 +NOTICE: out +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: compile trigger function: my_trigger_func(TD_name, TD_relid, TD_table_name, TD_table_schema, TD_event, TD_when, TD_level, TD_NEW, TD_OLD, args) +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 +NOTICE: out +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: compile trigger function: my_trigger_func(TD_name, TD_relid, TD_table_name, TD_table_schema, TD_event, TD_when, TD_level, TD_NEW, TD_OLD, args) +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 +NOTICE: out +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: compile trigger function: my_trigger_func(TD_name, TD_relid, TD_table_name, TD_table_schema, TD_event, TD_when, TD_level, TD_NEW, TD_OLD, args) +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: out +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: compile trigger function: my_trigger_func(TD_name, TD_relid, TD_table_name, TD_table_schema, TD_event, TD_when, TD_level, TD_NEW, TD_OLD, args) +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: out +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: compile trigger function: my_trigger_func(TD_name, TD_relid, TD_table_name, TD_table_schema, TD_event, TD_when, TD_level, TD_NEW, TD_OLD, args) +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 +NOTICE: out +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: compile trigger function: my_trigger_func(TD_name, TD_relid, TD_table_name, TD_table_schema, TD_event, TD_when, TD_level, TD_NEW, TD_OLD, args) +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 +NOTICE: out +DROP TRIGGER my_trigger_func ON my_table; +DROP TRIGGER my_trigger_func2 ON my_table; +DROP FUNCTION my_trigger_func; diff --git a/src/test/modules/plsample/plsample.c b/src/test/modules/plsample/plsample.c index 6fc33c728c..a2c89d9ab2 100644 --- a/src/test/modules/plsample/plsample.c +++ b/src/test/modules/plsample/plsample.c @@ -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. @@ -51,6 +53,8 @@ plsample_call_handler(PG_FUNCTION_ARGS) * (TriggerData *) fcinfo->context includes the information of the * context. */ + retval = PointerGetDatum(plsample_trigger_handler(fcinfo)); + ereport(NOTICE, (errmsg("out"))); } else if (CALLED_AS_EVENT_TRIGGER(fcinfo)) { @@ -185,3 +189,177 @@ 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; + MemoryContext proc_cxt = NULL; + MemoryContext old_cxt = NULL; + char *trigger_code = NULL; + + /* 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 source text of the function. */ + pl_tuple = SearchSysCache(PROCOID, + ObjectIdGetDatum(fcinfo->flinfo->fn_oid), 0, 0, 0); + if (!HeapTupleIsValid(pl_tuple)) + elog(ERROR, "cache lookup failed for function %u", + fcinfo->flinfo->fn_oid); + + /* + * 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))); + + PG_TRY(); + { + char *internal_args = "TD_name, TD_relid, TD_table_name, TD_table_schema, TD_event, TD_when, TD_level, TD_NEW, TD_OLD, args"; + + /* + * The code generated for this trigger is: proname(internal_args) + * source + */ + int length = + strlen(proname) + strlen(source) + strlen(internal_args) + 4; + + /* + * Allocate a context that will hold all the Postgres data for the + * procedure. + */ + proc_cxt = AllocSetContextCreate(TopMemoryContext, + "PL/Sample function", ALLOCSET_SMALL_SIZES); + + trigger_code = (char *) palloc0(length * sizeof(char)); + trigger_code[0] = '\0'; + strcpy(trigger_code, proname); + strcat(trigger_code, "("); + strcat(trigger_code, internal_args); + strcat(trigger_code, ") "); + strcat(trigger_code, source); + + ereport(NOTICE, + (errmsg("compile trigger function: %s", trigger_code))); + + old_cxt = MemoryContextSwitchTo(proc_cxt); + + MemoryContextSwitchTo(old_cxt); + } + PG_CATCH(); + { + PG_RE_THROW(); + } + PG_END_TRY(); + + pfree(source); + + ReleaseSysCache(pl_tuple); + + PG_TRY(); + { + int i; + + ereport(NOTICE, + (errmsg("trigger name: %s", trigdata->tg_trigger->tgname))); + string = SPI_getrelname(trigdata->tg_relation); + ereport(NOTICE, (errmsg("trigger relation: %s", string))); + pfree(string); + + string = SPI_getnspname(trigdata->tg_relation); + ereport(NOTICE, (errmsg("trigger relation schema: %s", string))); + pfree(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 (i = 0; i < trigdata->tg_trigger->tgnargs; i++) + ereport(NOTICE, + (errmsg("trigger arg[%i]: %s", i, + trigdata->tg_trigger->tgargs[i]))); + } + PG_CATCH(); + { + PG_RE_THROW(); + } + PG_END_TRY(); + + if (SPI_finish() != SPI_OK_FINISH) + elog(ERROR, "SPI_finish() failed"); + + return rettup; +} diff --git a/src/test/modules/plsample/sql/plsample.sql b/src/test/modules/plsample/sql/plsample.sql index bf0fddac7f..e3ffc4ecf9 100644 --- a/src/test/modules/plsample/sql/plsample.sql +++ b/src/test/modules/plsample/sql/plsample.sql @@ -13,3 +13,31 @@ 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'), (2, 'second'), (1, 'first'); + +UPDATE my_table +SET description = 'first, modified once' +WHERE num = 1; + +DROP TRIGGER my_trigger_func ON my_table; +DROP TRIGGER my_trigger_func2 ON my_table; +DROP FUNCTION my_trigger_func; + --7wuC2KwZkaclehjT--