diff options
Diffstat (limited to 'src/test/modules/plsample')
-rw-r--r-- | src/test/modules/plsample/.gitignore | 3 | ||||
-rw-r--r-- | src/test/modules/plsample/Makefile | 20 | ||||
-rw-r--r-- | src/test/modules/plsample/README | 6 | ||||
-rw-r--r-- | src/test/modules/plsample/expected/plsample.out | 117 | ||||
-rw-r--r-- | src/test/modules/plsample/plsample--1.0.sql | 14 | ||||
-rw-r--r-- | src/test/modules/plsample/plsample.c | 354 | ||||
-rw-r--r-- | src/test/modules/plsample/plsample.control | 8 | ||||
-rw-r--r-- | src/test/modules/plsample/sql/plsample.sql | 38 |
8 files changed, 560 insertions, 0 deletions
diff --git a/src/test/modules/plsample/.gitignore b/src/test/modules/plsample/.gitignore new file mode 100644 index 0000000..44d119c --- /dev/null +++ b/src/test/modules/plsample/.gitignore @@ -0,0 +1,3 @@ +# Generated subdirectories +/log/ +/results/ diff --git a/src/test/modules/plsample/Makefile b/src/test/modules/plsample/Makefile new file mode 100644 index 0000000..f1bc334 --- /dev/null +++ b/src/test/modules/plsample/Makefile @@ -0,0 +1,20 @@ +# src/test/modules/plsample/Makefile + +MODULES = plsample + +EXTENSION = plsample +DATA = plsample--1.0.sql +PGFILEDESC = "PL/Sample - template for procedural language" + +REGRESS = plsample + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/plsample +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/plsample/README b/src/test/modules/plsample/README new file mode 100644 index 0000000..0ed3193 --- /dev/null +++ b/src/test/modules/plsample/README @@ -0,0 +1,6 @@ +PL/Sample +========= + +PL/Sample is an example template of procedural-language handler. It is +a simple implementation, yet demonstrates some of the things that can be done +to build a fully functional procedural-language handler. diff --git a/src/test/modules/plsample/expected/plsample.out b/src/test/modules/plsample/expected/plsample.out new file mode 100644 index 0000000..8ad5f7a --- /dev/null +++ b/src/test/modules/plsample/expected/plsample.out @@ -0,0 +1,117 @@ +CREATE EXTENSION plsample; +-- Create and test some dummy functions +CREATE FUNCTION plsample_result_text(a1 numeric, a2 text, a3 integer[]) +RETURNS TEXT +AS $$ + Example of source with text result. +$$ LANGUAGE plsample; +SELECT plsample_result_text(1.23, 'abc', '{4, 5, 6}'); +NOTICE: source text of function "plsample_result_text": + Example of source with text result. + +NOTICE: argument: 0; name: a1; value: 1.23 +NOTICE: argument: 1; name: a2; value: abc +NOTICE: argument: 2; name: a3; value: {4,5,6} + plsample_result_text +--------------------------------------- + + + Example of source with text result.+ + +(1 row) + +CREATE FUNCTION plsample_result_void(a1 text[]) +RETURNS VOID +AS $$ + Example of source with void result. +$$ LANGUAGE plsample; +SELECT plsample_result_void('{foo, bar, hoge}'); +NOTICE: source text of function "plsample_result_void": + Example of source with void result. + +NOTICE: argument: 0; name: a1; value: {foo,bar,hoge} + plsample_result_void +---------------------- + +(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 diff --git a/src/test/modules/plsample/plsample--1.0.sql b/src/test/modules/plsample/plsample--1.0.sql new file mode 100644 index 0000000..fc5b280 --- /dev/null +++ b/src/test/modules/plsample/plsample--1.0.sql @@ -0,0 +1,14 @@ +/* src/test/modules/plsample/plsample--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION plsample" to load this file. \quit + +CREATE FUNCTION plsample_call_handler() RETURNS language_handler + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE TRUSTED LANGUAGE plsample + HANDLER plsample_call_handler; + +ALTER LANGUAGE plsample OWNER TO @extowner@; + +COMMENT ON LANGUAGE plsample IS 'PL/Sample procedural language'; diff --git a/src/test/modules/plsample/plsample.c b/src/test/modules/plsample/plsample.c new file mode 100644 index 0000000..780db72 --- /dev/null +++ b/src/test/modules/plsample/plsample.c @@ -0,0 +1,354 @@ +/*------------------------------------------------------------------------- + * + * plsample.c + * Handler for the PL/Sample procedural language + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/test/modules/plsample/plsample.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.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 "funcapi.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +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. + */ +Datum +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(); + { + /* + * Determine if called as function or trigger and call appropriate + * subhandler. + */ + if (CALLED_AS_TRIGGER(fcinfo)) + { + /* + * This function has been called as a trigger function, where + * (TriggerData *) fcinfo->context includes the information of the + * context. + */ + retval = PointerGetDatum(plsample_trigger_handler(fcinfo)); + } + else if (CALLED_AS_EVENT_TRIGGER(fcinfo)) + { + /* + * 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 + { + /* Regular function handler */ + retval = plsample_func_handler(fcinfo); + } + } + PG_FINALLY(); + { + } + PG_END_TRY(); + + return retval; +} + +/* + * plsample_func_handler + * + * Function called by the call handler for function execution. + */ +static Datum +plsample_func_handler(PG_FUNCTION_ARGS) +{ + HeapTuple pl_tuple; + Datum ret; + char *source; + bool isnull; + FmgrInfo *arg_out_func; + Form_pg_type type_struct; + HeapTuple type_tuple; + Form_pg_proc pl_struct; + volatile MemoryContext proc_cxt = NULL; + Oid *argtypes; + char **argnames; + char *argmodes; + char *proname; + Form_pg_type pg_type_entry; + Oid result_typioparam; + Oid prorettype; + FmgrInfo result_in_func; + int numargs; + + /* 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); + + /* + * 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))); + + /* + * Allocate a context that will hold all the Postgres data for the + * procedure. + */ + proc_cxt = AllocSetContextCreate(TopMemoryContext, + "PL/Sample function", + ALLOCSET_SMALL_SIZES); + + arg_out_func = (FmgrInfo *) palloc0(fcinfo->nargs * sizeof(FmgrInfo)); + numargs = get_func_arg_info(pl_tuple, &argtypes, &argnames, &argmodes); + + /* + * Iterate through all of the function arguments, printing each input + * value. + */ + for (int i = 0; i < numargs; i++) + { + Oid argtype = pl_struct->proargtypes.values[i]; + char *value; + + type_tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(argtype)); + if (!HeapTupleIsValid(type_tuple)) + elog(ERROR, "cache lookup failed for type %u", argtype); + + type_struct = (Form_pg_type) GETSTRUCT(type_tuple); + fmgr_info_cxt(type_struct->typoutput, &(arg_out_func[i]), proc_cxt); + ReleaseSysCache(type_tuple); + + value = OutputFunctionCall(&arg_out_func[i], fcinfo->args[i].value); + ereport(NOTICE, + (errmsg("argument: %d; name: %s; value: %s", + i, argnames[i], value))); + } + + /* Type of the result */ + prorettype = pl_struct->prorettype; + ReleaseSysCache(pl_tuple); + + /* + * Get the required information for input conversion of the return value. + * + * If the function uses VOID as result, it is better to return NULL. + * Anyway, let's be honest. This is just a template, so there is not much + * we can do here. This returns NULL except if the result type is text, + * where the result is the source text of the function. + */ + if (prorettype != TEXTOID) + PG_RETURN_NULL(); + + type_tuple = SearchSysCache1(TYPEOID, + ObjectIdGetDatum(prorettype)); + if (!HeapTupleIsValid(type_tuple)) + elog(ERROR, "cache lookup failed for type %u", prorettype); + pg_type_entry = (Form_pg_type) GETSTRUCT(type_tuple); + result_typioparam = getTypeIOParam(type_tuple); + + fmgr_info_cxt(pg_type_entry->typinput, &result_in_func, proc_cxt); + ReleaseSysCache(type_tuple); + + 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; +} diff --git a/src/test/modules/plsample/plsample.control b/src/test/modules/plsample/plsample.control new file mode 100644 index 0000000..1e67251 --- /dev/null +++ b/src/test/modules/plsample/plsample.control @@ -0,0 +1,8 @@ +# plsample extension +comment = 'PL/Sample' +default_version = '1.0' +module_pathname = '$libdir/plsample' +relocatable = false +schema = pg_catalog +superuser = false +trusted = true diff --git a/src/test/modules/plsample/sql/plsample.sql b/src/test/modules/plsample/sql/plsample.sql new file mode 100644 index 0000000..cf652ad --- /dev/null +++ b/src/test/modules/plsample/sql/plsample.sql @@ -0,0 +1,38 @@ +CREATE EXTENSION plsample; +-- Create and test some dummy functions +CREATE FUNCTION plsample_result_text(a1 numeric, a2 text, a3 integer[]) +RETURNS TEXT +AS $$ + Example of source with text result. +$$ LANGUAGE plsample; +SELECT plsample_result_text(1.23, 'abc', '{4, 5, 6}'); + +CREATE FUNCTION plsample_result_void(a1 text[]) +RETURNS VOID +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; |