doc examples for pghandler

Started by Mark Wongover 5 years ago11 messages
#1Mark Wong
mark@2ndquadrant.com
1 attachment(s)

Hi everyone,

Would some additional procedure language handler code examples in the
documentation be good to add? I've put some together in the attached
patch, and can log it to a future commitfest if people think it would
be a good addition.

Regards,
Mark
--
Mark Wong
2ndQuadrant - PostgreSQL Solutions for the Enterprise
https://www.2ndQuadrant.com/

Attachments:

doc-pghandler-v1.patchtext/x-diff; charset=us-asciiDownload
diff --git a/doc/src/sgml/plhandler.sgml b/doc/src/sgml/plhandler.sgml
index e1b0af7a60..0287d424cb 100644
--- a/doc/src/sgml/plhandler.sgml
+++ b/doc/src/sgml/plhandler.sgml
@@ -241,4 +241,560 @@ CREATE LANGUAGE plsample
     reference page also has some useful details.
    </para>
 
+   <para>
+    The following subsections contain additional examples to help build a
+    complete procedural language handler.
+   </para>
+
+   <sect1 id="plhandler-minimal">
+    <title>Minimal Example</title>
+
+    <para>
+     Here is a complete minimal example that builds a procedural language
+     handler <application>PL/Sample</application> as an extension.  Functions
+     can be created and called for <application>PL/Sample</application> but
+     they will not be able to perform any usefule actions.
+    </para>
+
+    <para>
+     The <filename>plsample--0.1.sql</filename> file:
+<programlisting>
+CREATE FUNCTION plsample_call_handler()
+RETURNS language_handler
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
+
+CREATE LANGUAGE plsample
+HANDLER plsample_call_handler;
+
+COMMENT ON LANGUAGE plsample IS 'PL/Sample procedural language';
+</programlisting>
+    </para>
+
+    <para>
+     The control file <filename>plsample.control</filename> looks like this:
+<programlisting>
+comment = 'PL/Sample'
+default_version = '0.1'
+module_pathname = '$libdir/plsample'
+relocatable = false
+schema = pg_catalog
+superuser = false
+</programlisting>
+     See <xref linkend="extend-extensions"/> for more information about writing
+     control files.
+    </para>
+
+    <para>
+     The following <filename>Makefile</filename> relies on
+     <acronym>PGXS</acronym>.
+<programlisting>
+PGFILEDESC = "PL/Sample - procedural language"
+
+EXTENSION = plsample
+EXTVERSION = 0.1
+
+MODULE_big = plsample
+
+OBJS = plsample.o
+
+DATA = plsample.control plsample--0.1.sql
+
+plsample.o: plsample.c
+
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+</programlisting>
+     See <xref linkend="extend-pgxs"/> for more information on makefiles with
+     <acronym>PGXS</acronym>.
+    </para>
+
+    <para>
+     Here is the minimal C code in <filename>plsample.c</filename> that will
+     handle calls to this sample procedural language:
+<programlisting>
+#include &lt;postgres.h&gt;
+#include &lt;fmgr.h&gt;
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(plsample_call_handler);
+
+/*
+ * Handle function, procedure, and trigger calls.
+ */
+Datum
+plsample_call_handler(PG_FUNCTION_ARGS)
+{
+        return 0;
+}
+</programlisting>
+    </para>
+
+    <para>
+     The following sections will continue building upon this example to
+     describe how to add additional functionality to a call handler for a
+     procedural language.
+    </para>
+   </sect1>
+
+   <sect1 id="plhandler-source">
+    <title>Fetching the source of a function</title>
+
+    <para>
+     Additional code is added to <filename>plsample.c</filename> from <xref
+     linkend="plhandler-minimal"/> to include additional headers for the
+     additional code that fetches the source text of the function.  The
+     resulting file now looks like:
+<programlisting>
+#include &lt;postgres.h&gt;
+#include &lt;fmgr.h&gt;
+#include &lt;funcapi.h&gt;
+#include &lt;access/htup_details.h&gt;
+#include &lt;catalog/pg_proc.h&gt;
+#include &lt;catalog/pg_type.h&gt;
+#include &lt;utils/memutils.h&gt;
+#include &lt;utils/builtins.h&gt;
+#include &lt;utils/syscache.h&gt;
+
+MemoryContext TopMemoryContext = NULL;
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(plsample_call_handler);
+
+/*
+ * Handle function, procedure, and trigger calls.
+ */
+
+Datum
+plsample_call_handler(PG_FUNCTION_ARGS)
+{
+        HeapTuple pl_tuple;
+        Datum pl_datum;
+        const char *source;
+        bool isnull;
+
+        /* Fetch the source 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);
+
+        pl_datum = SysCacheGetAttr(PROCOID, pl_tuple, Anum_pg_proc_prosrc,
+                        &amp;isnull);
+        if (isnull)
+                elog(ERROR, "null prosrc");
+        ReleaseSysCache(pl_tuple);
+
+        source = DatumGetCString(DirectFunctionCall1(textout, pl_datum));
+        elog(LOG, "source text:\n%s", source);
+
+        return 0;
+}
+</programlisting>
+     The variable <structfield>source</structfield> containes the source that
+     needs to be interpreted and executed by the procedurual language handler
+     itself, or by an existing inplementation of the programming language that
+     the source text is written with.
+    </para>
+
+    <para>
+     The following <command>CREATE FUNCTION</command> will set the source text
+     of the function to <literal>This is function's source text.</literal>:
+<programlisting>
+CREATE FUNCTION plsample_func()
+RETURNS VOID
+AS $$
+  This is function's source text.
+$$ LANGUAGE plsample;
+</programlisting>
+    </para>
+
+    <para>
+     Calling the function with the following command will log the function's
+     source text because of the <function>elog()</function> statement towards
+     the end of <function>plsample_call_handler()</function> function:
+<programlisting>
+SELECT plsample_func();
+</programlisting>
+     and emits the following to the log file:
+<screen>
+LOG:  source text:
+
+  This is function's source text.
+
+</screen>
+    </para>
+   </sect1>
+
+   <sect1 id="plhandler-arguments">
+    <title>Analyzing function arguments</title>
+
+    <para>
+     This example introduces new code to <filename>plsample.c</filename> from
+     <xref linkend="plhandler-source"/> to demonstrate one method for analyzing
+     the arguments passed to the function.  This code iterates through all of
+     the arguments to log its position, name and value as text, regardless of
+     its actual data type for ease of demonstration.
+    </para>
+
+    <para>
+     Call <function>get_func_arg_info()</function> to get a pointer to the
+     argument types, names, and modes before iterating through them to retrieve
+     the value of each argument:
+<programlisting>
+    get_func_arg_info(pl_tuple, &amp;argtypes, &amp;argnames, &amp;argmodes);
+</programlisting>
+    </para>
+
+    <para>
+     The pointer to <structname>FunctionCallInfoBaseData</structname>
+     <type>struct</type> contains the number of argument passed to the function
+     <structfield>flinfo-&gt;nargs</structfield>.  The following code
+     demostrates how to iterate through each argument and retrieve its value as
+     text:
+<programlisting>
+    elog(LOG, "number of arguments : %d", fcinfo-&gt;nargs);
+    for (i = 0; i &lt; fcinfo-&gt;nargs; i++)
+    {
+        Oid argtype = pl_struct-&gt;proargtypes.values[i];
+        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-&gt;typoutput, &amp;(arg_out_func[i]), proc_cxt);
+        ReleaseSysCache(type_tuple);
+
+        value = OutputFunctionCall(&amp;arg_out_func[i], fcinfo-&lt;args[i].value);
+
+        elog(LOG, "argument position: %d; name: %s; value: %s", i, argnames[i],
+                value);
+    }
+</programlisting>
+    </para>
+
+    <para>
+     Let's now create a function with three arugments consiting of a number,
+     text and an array of integers:
+<programlisting>
+CREATE FUNCTION plsample_func(a1 NUMERIC, a2 TEXT, a3 INTEGER[])
+RETURNS VOID
+AS $$
+  This is function's source text.
+$$ LANGUAGE plsample;
+</programlisting>
+     Then let's call the function with the following commands:
+<programlisting>
+SELECT plsample_func(1.23, 'abc', '{4, 5, 6}');
+</programlisting>
+     The <function>elog()</function> statements in the example will emit the
+     number of arguments, and the position, name and value of each argument
+     to the log file:
+<screen>
+LOG:  number of arguments : 3
+LOG:  argument position: 0; name: a1; value: 1.23
+LOG:  argument position: 1; name: a2; value: abc
+LOG:  argument position: 2; name: a3; value: {4,5,6}
+</screen>
+    </para>
+
+    <para>
+     The complete example of <filename>plsample.c</filename> follows:
+<programlisting>
+#include &lt;postgres.h&gt;
+#include &lt;fmgr.h&gt;
+#include &lt;funcapi.h&gt;
+#include &lt;access/htup_details.h&gt;
+#include &lt;catalog/pg_proc.h&gt;
+#include &lt;catalog/pg_type.h&gt;
+#include &lt;utils/memutils.h&gt;
+#include &lt;utils/builtins.h&gt;
+#include &lt;utils/syscache.h&gt;
+
+MemoryContext TopMemoryContext = NULL;
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(plsample_call_handler);
+
+/*
+ * Handle function, procedure, and trigger calls.
+ */
+Datum
+plsample_call_handler(PG_FUNCTION_ARGS)
+{
+    HeapTuple pl_tuple;
+    Datum pl_datum;
+    const char *source;
+    bool isnull;
+
+    int i;
+    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 *value;
+
+    /* Fetch the source of the function. */
+
+    pl_tuple = SearchSysCache(PROCOID,
+            ObjectIdGetDatum(fcinfo-&gt;flinfo-&gt;fn_oid), 0, 0, 0);
+    if (!HeapTupleIsValid(pl_tuple))
+        elog(ERROR, "cache lookup failed for function %u",
+                fcinfo-&gt;flinfo-&gt;fn_oid);
+    pl_struct = (Form_pg_proc) GETSTRUCT(pl_tuple);
+
+    pl_datum = SysCacheGetAttr(PROCOID, pl_tuple, Anum_pg_proc_prosrc,
+            &amp;isnull);
+    if (isnull)
+        elog(ERROR, "null prosrc");
+    ReleaseSysCache(pl_tuple);
+
+    source = DatumGetCString(DirectFunctionCall1(textout, pl_datum));
+    elog(LOG, "source text:\n%s", source);
+        
+    arg_out_func = (FmgrInfo *) palloc0(fcinfo-&gt;nargs * sizeof(FmgrInfo));
+    proc_cxt = AllocSetContextCreate(TopMemoryContext,
+            "PL/Sample function", 0, (1 * 1024), (8 * 1024));
+    get_func_arg_info(pl_tuple, &amp;argtypes, &amp;argnames, &amp;argmodes);
+
+    /* Iterate through all of the function arguments. */
+    elog(LOG, "number of arguments : %d", fcinfo-&gt;nargs);
+    for (i = 0; i &lt; fcinfo-&gt;nargs; i++)
+    {
+        Oid argtype = pl_struct-&gt;proargtypes.values[i];
+        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-&gt;typoutput, &amp;(arg_out_func[i]), proc_cxt);
+        ReleaseSysCache(type_tuple);
+
+        value = OutputFunctionCall(&amp;arg_out_func[i], fcinfo-&gt;args[i].value);
+
+        elog(LOG, "argument position: %d; name: %s; value: %s", i, argnames[i],
+                value);
+    }
+
+    return 0;
+}
+</programlisting>
+    </para>
+   </sect1>
+
+   <sect1 id="plhandler-return">
+    <title>Returning data</title>
+
+    <para>
+     This example introduces new code to <filename>plsample.c</filename> from
+     <xref linkend="plhandler-arguments"/> to demonstrate how to return data
+     from the function.  This code fetches the function's return type and
+     attempts to return the function's source text.
+    </para>
+
+    <para>
+     The follow code segments fetches the return type of the function:
+<programlisting>
+    type_tuple = SearchSysCache1(TYPEOID,
+            ObjectIdGetDatum(pl_struct->prorettype));
+    if (!HeapTupleIsValid(type_tuple))
+        elog(ERROR, "cache lookup failed for type %u", pl_struct->prorettype);
+</programlisting>
+    </para>
+
+    <para>
+     The follow code segments takes the function source text, fetched in a
+     previous section, and attempts to return that data:
+<programlisting>
+    ret = InputFunctionCall(&amp;result_in_func, source, result_typioparam, -1);
+    PG_RETURN_DATUM(ret);
+</programlisting>
+    </para>
+
+    <para>
+     The following function can be used to help demostrate the new
+     functionality:
+<programlisting>
+CREATE FUNCTION plsample_func(a1 NUMERIC, a2 TEXT, a3 INTEGER[])
+RETURNS TEXT
+AS $$
+  This is function's source text.
+$$ LANGUAGE plsample;
+</programlisting>
+     It is built upon the function used in the previous examples to now return
+     <type>TEXT</type>.  Running the following command returns the source text
+     of the function:
+<programlisting>
+SELECT plsample_func(1.23, 'abc', '{4, 5, 6}');
+</programlisting>
+<screen>
+           plsample_func
+-----------------------------------
+                                  +
+   This is function's source text.+
+
+(1 row)
+</screen>
+    </para>
+
+    <para>
+     The complete example of <filename>plsample.c</filename> follows:
+<programlisting>
+#include &lt;postgres.h&gt;
+#include &lt;fmgr.h&gt;
+#include &lt;funcapi.h&gt;
+#include &lt;access/htup_details.h&gt;
+#include &lt;catalog/pg_proc.h&gt;
+#include &lt;catalog/pg_type.h&gt;
+#include &lt;utils/memutils.h&gt;
+#include &lt;utils/builtins.h&gt;
+#include &lt;utils/lsyscache.h&gt;
+#include &lt;utils/syscache.h&gt;
+
+MemoryContext TopMemoryContext = NULL;
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(plsample_call_handler);
+
+/*
+ * Handle function, procedure, and trigger calls.
+ */
+Datum
+plsample_call_handler(PG_FUNCTION_ARGS)
+{
+    HeapTuple pl_tuple;
+    Datum ret;
+    char *source;
+    bool isnull;
+
+    int i;
+    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 *value;
+
+    Form_pg_type pg_type_entry;
+    Oid result_typioparam;
+    FmgrInfo result_in_func;
+
+    /* Fetch the source of the function. */
+
+    pl_tuple = SearchSysCache(PROCOID,
+            ObjectIdGetDatum(fcinfo-&gt;flinfo-&gt;fn_oid), 0, 0, 0);
+    if (!HeapTupleIsValid(pl_tuple))
+        elog(ERROR, "cache lookup failed for function %u",
+                fcinfo-&gt;flinfo-&gt;fn_oid);
+    pl_struct = (Form_pg_proc) GETSTRUCT(pl_tuple);
+
+    ret = SysCacheGetAttr(PROCOID, pl_tuple, Anum_pg_proc_prosrc, &amp;isnull);
+    if (isnull)
+        elog(ERROR, "null prosrc");
+    ReleaseSysCache(pl_tuple);
+
+    source = DatumGetCString(DirectFunctionCall1(textout, ret));
+    elog(LOG, "source text:\n%s", source);
+        
+    arg_out_func = (FmgrInfo *) palloc0(fcinfo-&gt;nargs * sizeof(FmgrInfo));
+    proc_cxt = AllocSetContextCreate(TopMemoryContext,
+            "PL/Sample function", 0, (1 * 1024), (8 * 1024));
+    get_func_arg_info(pl_tuple, &amp;argtypes, &amp;argnames, &amp;argmodes);
+
+    /* Iterate through all of the function arguments. */
+    elog(LOG, "number of arguments : %d", fcinfo-&gt;nargs);
+    for (i = 0; i &lt; fcinfo-&gt;nargs; i++)
+    {
+        Oid argtype = pl_struct-&gt;proargtypes.values[i];
+        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-&gt;typoutput, &amp;(arg_out_func[i]), proc_cxt);
+        ReleaseSysCache(type_tuple);
+
+        value = OutputFunctionCall(&amp;arg_out_func[i], fcinfo-&gt;args[i].value);
+
+        elog(LOG, "argument position: %d; name: %s; value: %s", i, argnames[i],
+                value);
+    }
+
+    /* Fetch the return type of the function. */
+
+    type_tuple = SearchSysCache1(TYPEOID,
+            ObjectIdGetDatum(pl_struct-&gt;prorettype));
+    if (!HeapTupleIsValid(type_tuple))
+        elog(ERROR, "cache lookup failed for type %u", pl_struct-&gt;prorettype);
+
+    pg_type_entry = (Form_pg_type) GETSTRUCT(type_tuple);
+
+    proc_cxt = AllocSetContextCreate(TopMemoryContext, "PL/Sample function",
+            ALLOCSET_SMALL_SIZES);
+
+    result_typioparam = getTypeIOParam(type_tuple);
+
+    fmgr_info_cxt(pg_type_entry-&gt;typinput, &amp;result_in_func, proc_cxt);
+    ReleaseSysCache(type_tuple);
+
+    /* Simply return the function source text. */
+    ret = InputFunctionCall(&amp;result_in_func, source, result_typioparam, -1);
+    PG_RETURN_DATUM(ret);
+
+    return 0;
+}
+</programlisting>
+    </para>
+   </sect1>
+
+   <sect1 id="plhandler-notes">
+    <title>Additional notes</title>
+
+    <para>
+     The examples provided in this sections for developing a procedural
+     language handler do not cover all aspects that may need to be considered
+     when introducing a new language.  Here are some additional considerations:
+    </para>
+
+    <itemizedlist  spacing="compact" mark="bullet">
+     <listitem>
+      <para>
+       Caching the source text of the function for performance reasons.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       Handling trigger functions.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       Validating the source text.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       Handling returning sets of rows.
+      </para>
+     </listitem>
+    </itemizedlist>
+   </sect1>
+
  </chapter>
#2Tom Lane
tgl@sss.pgh.pa.us
In reply to: Mark Wong (#1)
Re: doc examples for pghandler

Mark Wong <mark@2ndquadrant.com> writes:

Would some additional procedure language handler code examples in the
documentation be good to add? I've put some together in the attached
patch, and can log it to a future commitfest if people think it would
be a good addition.

Hmm. The existing doc examples are really pretty laughable, because
there's such a large gap between the offered skeleton and a workable
handler. So I agree it'd be nice to do better, but I'm suspicious of
having large chunks of sample code in the docs --- that's a maintenance
problem, if only because we likely won't notice when we break it.
Also, if somebody is hoping to copy-and-paste such code, it isn't
that nice to work from if it's embedded in SGML.

I wonder if it'd be possible to adapt what you have here into some
tiny contrib module that doesn't do very much useful, but can at
least be tested to see that it compiles; moreover it could be
copied verbatim to serve as a starting point for a new PL.

regards, tom lane

#3Mark Wong
mark@2ndquadrant.com
In reply to: Tom Lane (#2)
Re: doc examples for pghandler

On Fri, Jun 12, 2020 at 03:10:20PM -0400, Tom Lane wrote:

Mark Wong <mark@2ndquadrant.com> writes:

Would some additional procedure language handler code examples in the
documentation be good to add? I've put some together in the attached
patch, and can log it to a future commitfest if people think it would
be a good addition.

Hmm. The existing doc examples are really pretty laughable, because
there's such a large gap between the offered skeleton and a workable
handler. So I agree it'd be nice to do better, but I'm suspicious of
having large chunks of sample code in the docs --- that's a maintenance
problem, if only because we likely won't notice when we break it.
Also, if somebody is hoping to copy-and-paste such code, it isn't
that nice to work from if it's embedded in SGML.

I wonder if it'd be possible to adapt what you have here into some
tiny contrib module that doesn't do very much useful, but can at
least be tested to see that it compiles; moreover it could be
copied verbatim to serve as a starting point for a new PL.

I do have the code examples in a repo. [1]https://gitlab.com/markwkm/yappl The 0.4 directory consists
of everything the examples show.

It would be easy enough to adapt that for contrib, and move some of the
content from the doc patch into that. Then touch up the handler chapter
to reference the contrib module.

Does that sound more useful?

[1]: https://gitlab.com/markwkm/yappl

--
Mark Wong
2ndQuadrant - PostgreSQL Solutions for the Enterprise
https://www.2ndQuadrant.com/

#4Tom Lane
tgl@sss.pgh.pa.us
In reply to: Mark Wong (#3)
Re: doc examples for pghandler

Mark Wong <mark@2ndquadrant.com> writes:

On Fri, Jun 12, 2020 at 03:10:20PM -0400, Tom Lane wrote:

I wonder if it'd be possible to adapt what you have here into some
tiny contrib module that doesn't do very much useful, but can at
least be tested to see that it compiles; moreover it could be
copied verbatim to serve as a starting point for a new PL.

I do have the code examples in a repo. [1] The 0.4 directory consists
of everything the examples show.

It would be easy enough to adapt that for contrib, and move some of the
content from the doc patch into that. Then touch up the handler chapter
to reference the contrib module.

On second thought, contrib/ is not quite the right place, because we
typically expect modules there to actually get installed, meaning they
have to have at least some end-user usefulness. The right place for
a toy PL handler is probably src/test/modules/; compare for example
src/test/modules/test_parser/, which is serving quite the same sort
of purpose as a skeleton text search parser.

regards, tom lane

#5Michael Paquier
michael@paquier.xyz
In reply to: Tom Lane (#4)
Re: doc examples for pghandler

On Fri, Jun 12, 2020 at 10:13:41PM -0400, Tom Lane wrote:

On second thought, contrib/ is not quite the right place, because we
typically expect modules there to actually get installed, meaning they
have to have at least some end-user usefulness. The right place for
a toy PL handler is probably src/test/modules/; compare for example
src/test/modules/test_parser/, which is serving quite the same sort
of purpose as a skeleton text search parser.

+1 for src/test/modules/, and if you can provide some low-level API
coverage through this module, that's even better.
--
Michael
#6Mark Wong
mark@2ndquadrant.com
In reply to: Michael Paquier (#5)
1 attachment(s)
Re: doc examples for pghandler

On Sat, Jun 13, 2020 at 01:19:17PM +0900, Michael Paquier wrote:

On Fri, Jun 12, 2020 at 10:13:41PM -0400, Tom Lane wrote:

On second thought, contrib/ is not quite the right place, because we
typically expect modules there to actually get installed, meaning they
have to have at least some end-user usefulness. The right place for
a toy PL handler is probably src/test/modules/; compare for example
src/test/modules/test_parser/, which is serving quite the same sort
of purpose as a skeleton text search parser.

+1 for src/test/modules/, and if you can provide some low-level API
coverage through this module, that's even better.

Sounds good to me. Something more like the attached patch?

Regards,
Mark
--
Mark Wong
2ndQuadrant - PostgreSQL Solutions for the Enterprise
https://www.2ndQuadrant.com/

Attachments:

plsample-v1.patchtext/x-diff; charset=us-asciiDownload
diff --git a/doc/src/sgml/plhandler.sgml b/doc/src/sgml/plhandler.sgml
index e1b0af7a60..7b2c5624c0 100644
--- a/doc/src/sgml/plhandler.sgml
+++ b/doc/src/sgml/plhandler.sgml
@@ -96,62 +96,12 @@
    </para>
 
    <para>
-    This is a template for a procedural-language handler written in C:
-<programlisting>
-#include "postgres.h"
-#include "executor/spi.h"
-#include "commands/trigger.h"
-#include "fmgr.h"
-#include "access/heapam.h"
-#include "utils/syscache.h"
-#include "catalog/pg_proc.h"
-#include "catalog/pg_type.h"
-
-PG_MODULE_MAGIC;
-
-PG_FUNCTION_INFO_V1(plsample_call_handler);
-
-Datum
-plsample_call_handler(PG_FUNCTION_ARGS)
-{
-    Datum          retval;
-
-    if (CALLED_AS_TRIGGER(fcinfo))
-    {
-        /*
-         * Called as a trigger function
-         */
-        TriggerData    *trigdata = (TriggerData *) fcinfo-&gt;context;
-
-        retval = ...
-    }
-    else
-    {
-        /*
-         * Called as a function
-         */
-
-        retval = ...
-    }
-
-    return retval;
-}
-</programlisting>
-    Only a few thousand lines of code have to be added instead of the
-    dots to complete the call handler.
-   </para>
-
-   <para>
-    After having compiled the handler function into a loadable module
-    (see <xref linkend="dfunc"/>), the following commands then
-    register the sample procedural language:
-<programlisting>
-CREATE FUNCTION plsample_call_handler() RETURNS language_handler
-    AS '<replaceable>filename</replaceable>'
-    LANGUAGE C;
-CREATE LANGUAGE plsample
-    HANDLER plsample_call_handler;
-</programlisting>
+    A template for a procedural-language handler written as a C extension is
+    provided in <literal>src/test/modules/plsample</literal>.  This is a
+    working sample demonstrating one way to create a procedural-language
+    handler, process parameters, and return a value.  A few thousand lines of
+    additional code may have to be added to complete a fully functional
+    handler.
    </para>
 
    <para>
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 29de73c060..95144d8d7c 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -9,6 +9,7 @@ SUBDIRS = \
 		  commit_ts \
 		  dummy_index_am \
 		  dummy_seclabel \
+		  plsample \
 		  snapshot_too_old \
 		  test_bloomfilter \
 		  test_ddl_deparse \
diff --git a/src/test/modules/plsample/Makefile b/src/test/modules/plsample/Makefile
new file mode 100644
index 0000000000..757b47c785
--- /dev/null
+++ b/src/test/modules/plsample/Makefile
@@ -0,0 +1,20 @@
+# src/test/modules/plsample/Makefile
+
+PGFILEDESC = "PL/Sample - procedural language"
+
+REGRESS = create_pl create_func select_func
+
+EXTENSION = plsample
+EXTVERSION = 0.1
+
+MODULE_big = plsample
+
+OBJS = plsample.o
+
+DATA = plsample.control plsample--0.1.sql
+
+plsample.o: plsample.c
+
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
diff --git a/src/test/modules/plsample/README b/src/test/modules/plsample/README
new file mode 100644
index 0000000000..7ee213700b
--- /dev/null
+++ b/src/test/modules/plsample/README
@@ -0,0 +1,3 @@
+plsample is an example procedural-language handler.  It is a simple functional
+template that demonstrates some of the things that need to be done in order to
+build a fully functional procedural-language handler.
diff --git a/src/test/modules/plsample/expected/create_func.out b/src/test/modules/plsample/expected/create_func.out
new file mode 100644
index 0000000000..df2b915a97
--- /dev/null
+++ b/src/test/modules/plsample/expected/create_func.out
@@ -0,0 +1,5 @@
+CREATE FUNCTION plsample_func(a1 NUMERIC, a2 TEXT, a3 INTEGER[])
+RETURNS TEXT
+AS $$
+  This is function's source text.
+$$ LANGUAGE plsample;
diff --git a/src/test/modules/plsample/expected/create_pl.out b/src/test/modules/plsample/expected/create_pl.out
new file mode 100644
index 0000000000..5365391284
--- /dev/null
+++ b/src/test/modules/plsample/expected/create_pl.out
@@ -0,0 +1,8 @@
+CREATE FUNCTION plsample_call_handler()
+RETURNS language_handler
+AS '$libdir/plsample'
+LANGUAGE C;
+CREATE LANGUAGE plsample
+HANDLER plsample_call_handler;
+COMMENT ON LANGUAGE plsample
+IS 'PL/Sample procedural language';
diff --git a/src/test/modules/plsample/expected/select_func.out b/src/test/modules/plsample/expected/select_func.out
new file mode 100644
index 0000000000..dc396cbc04
--- /dev/null
+++ b/src/test/modules/plsample/expected/select_func.out
@@ -0,0 +1,8 @@
+SELECT plsample_func(1.23, 'abc', '{4, 5, 6}');
+           plsample_func           
+-----------------------------------
+                                  +
+   This is function's source text.+
+ 
+(1 row)
+
diff --git a/src/test/modules/plsample/plsample--0.1.sql b/src/test/modules/plsample/plsample--0.1.sql
new file mode 100644
index 0000000000..b429b83ceb
--- /dev/null
+++ b/src/test/modules/plsample/plsample--0.1.sql
@@ -0,0 +1,9 @@
+CREATE FUNCTION plsample_call_handler()
+RETURNS language_handler
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
+
+CREATE LANGUAGE plsample
+HANDLER plsample_call_handler;
+
+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 0000000000..15f7d1c55a
--- /dev/null
+++ b/src/test/modules/plsample/plsample.c
@@ -0,0 +1,120 @@
+/*-------------------------------------------------------------------------
+ *
+ * plsample.c
+ *      Handler for the PL/Sample procedural language
+ *
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/plsample.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include <postgres.h>
+#include <fmgr.h>
+#include <funcapi.h>
+#include <access/htup_details.h>
+#include <catalog/pg_proc.h>
+#include <catalog/pg_type.h>
+#include <utils/memutils.h>
+#include <utils/builtins.h>
+#include <utils/lsyscache.h>
+#include <utils/syscache.h>
+
+MemoryContext TopMemoryContext = NULL;
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(plsample_call_handler);
+
+/*
+ * Handle function, procedure, and trigger calls.
+ */
+Datum
+plsample_call_handler(PG_FUNCTION_ARGS)
+{
+	HeapTuple pl_tuple;
+	Datum ret;
+	char *source;
+	bool isnull;
+
+	int i;
+	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 *value;
+
+	Form_pg_type pg_type_entry;
+	Oid result_typioparam;
+	FmgrInfo result_in_func;
+
+	/* Fetch the source 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);
+	pl_struct = (Form_pg_proc) GETSTRUCT(pl_tuple);
+
+	ret = SysCacheGetAttr(PROCOID, pl_tuple, Anum_pg_proc_prosrc, &isnull);
+	if (isnull)
+		elog(ERROR, "null prosrc");
+	ReleaseSysCache(pl_tuple);
+
+	source = DatumGetCString(DirectFunctionCall1(textout, ret));
+	elog(LOG, "source text:\n%s", source);
+		
+	arg_out_func = (FmgrInfo *) palloc0(fcinfo->nargs * sizeof(FmgrInfo));
+	proc_cxt = AllocSetContextCreate(TopMemoryContext,
+			"PL/Sample function", 0, (1 * 1024), (8 * 1024));
+	get_func_arg_info(pl_tuple, &argtypes, &argnames, &argmodes);
+
+	/* Iterate through all of the function arguments. */
+	elog(LOG, "number of arguments : %d", fcinfo->nargs);
+	for (i = 0; i < fcinfo->nargs; i++)
+	{
+		Oid argtype = pl_struct->proargtypes.values[i];
+		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);
+
+		elog(LOG, "argument position: %d; name: %s; value: %s", i, argnames[i],
+				value);
+	}
+
+	/* Fetch the return type of the function. */
+
+	type_tuple = SearchSysCache1(TYPEOID,
+			ObjectIdGetDatum(pl_struct->prorettype));
+	if (!HeapTupleIsValid(type_tuple))
+		elog(ERROR, "cache lookup failed for type %u", pl_struct->prorettype);
+
+	pg_type_entry = (Form_pg_type) GETSTRUCT(type_tuple);
+
+	proc_cxt = AllocSetContextCreate(TopMemoryContext, "PL/Sample function",
+			ALLOCSET_SMALL_SIZES);
+
+	result_typioparam = getTypeIOParam(type_tuple);
+
+	fmgr_info_cxt(pg_type_entry->typinput, &result_in_func, proc_cxt);
+	ReleaseSysCache(type_tuple);
+
+	/* Simply return the function source text. */
+	ret = InputFunctionCall(&result_in_func, source, result_typioparam, -1);
+	PG_RETURN_DATUM(ret);
+
+	return 0;
+}
diff --git a/src/test/modules/plsample/plsample.control b/src/test/modules/plsample/plsample.control
new file mode 100644
index 0000000000..dba58e5abf
--- /dev/null
+++ b/src/test/modules/plsample/plsample.control
@@ -0,0 +1,7 @@
+# plsample extension
+comment = 'PL/Sample'
+default_version = '0.1'
+module_pathname = '$libdir/plsample'
+relocatable = false
+schema = pg_catalog
+superuser = false
diff --git a/src/test/modules/plsample/sql/create_func.sql b/src/test/modules/plsample/sql/create_func.sql
new file mode 100644
index 0000000000..df2b915a97
--- /dev/null
+++ b/src/test/modules/plsample/sql/create_func.sql
@@ -0,0 +1,5 @@
+CREATE FUNCTION plsample_func(a1 NUMERIC, a2 TEXT, a3 INTEGER[])
+RETURNS TEXT
+AS $$
+  This is function's source text.
+$$ LANGUAGE plsample;
diff --git a/src/test/modules/plsample/sql/create_pl.sql b/src/test/modules/plsample/sql/create_pl.sql
new file mode 100644
index 0000000000..c3ace0f1aa
--- /dev/null
+++ b/src/test/modules/plsample/sql/create_pl.sql
@@ -0,0 +1,10 @@
+CREATE FUNCTION plsample_call_handler()
+RETURNS language_handler
+AS '$libdir/plsample'
+LANGUAGE C;
+
+CREATE LANGUAGE plsample
+HANDLER plsample_call_handler;
+
+COMMENT ON LANGUAGE plsample
+IS 'PL/Sample procedural language';
diff --git a/src/test/modules/plsample/sql/select_func.sql b/src/test/modules/plsample/sql/select_func.sql
new file mode 100644
index 0000000000..5ded186984
--- /dev/null
+++ b/src/test/modules/plsample/sql/select_func.sql
@@ -0,0 +1 @@
+SELECT plsample_func(1.23, 'abc', '{4, 5, 6}');
#7Michael Paquier
michael@paquier.xyz
In reply to: Mark Wong (#6)
Re: doc examples for pghandler

On Sun, Jun 14, 2020 at 08:45:17PM -0700, Mark Wong wrote:

Sounds good to me. Something more like the attached patch?

That's the idea. I have not gone in details into what you have here,
but perhaps it would make sense to do a bit more and show how things
are done in the context of a PL function called in a trigger? Your
patch removes from the docs a code block that outlined that.
--
Michael

#8Mark Wong
mark@2ndquadrant.com
In reply to: Michael Paquier (#7)
1 attachment(s)
Re: doc examples for pghandler

On Mon, Jun 15, 2020 at 04:47:01PM +0900, Michael Paquier wrote:

On Sun, Jun 14, 2020 at 08:45:17PM -0700, Mark Wong wrote:

Sounds good to me. Something more like the attached patch?

That's the idea. I have not gone in details into what you have here,
but perhaps it would make sense to do a bit more and show how things
are done in the context of a PL function called in a trigger? Your
patch removes from the docs a code block that outlined that.

Ah, right. For the moment I've added some empty conditionals for
trigger and event trigger handling.

I've created a new entry in the commitfest app. [1]https://commitfest.postgresql.org/29/2678/ I'll keep at it. :)

Regards,
Mark

[1]: https://commitfest.postgresql.org/29/2678/

--
Mark Wong
2ndQuadrant - PostgreSQL Solutions for the Enterprise
https://www.2ndQuadrant.com/

Attachments:

plsample-v2.patchtext/x-diff; charset=us-asciiDownload
diff --git a/doc/src/sgml/plhandler.sgml b/doc/src/sgml/plhandler.sgml
index e1b0af7a60..7b2c5624c0 100644
--- a/doc/src/sgml/plhandler.sgml
+++ b/doc/src/sgml/plhandler.sgml
@@ -96,62 +96,12 @@
    </para>
 
    <para>
-    This is a template for a procedural-language handler written in C:
-<programlisting>
-#include "postgres.h"
-#include "executor/spi.h"
-#include "commands/trigger.h"
-#include "fmgr.h"
-#include "access/heapam.h"
-#include "utils/syscache.h"
-#include "catalog/pg_proc.h"
-#include "catalog/pg_type.h"
-
-PG_MODULE_MAGIC;
-
-PG_FUNCTION_INFO_V1(plsample_call_handler);
-
-Datum
-plsample_call_handler(PG_FUNCTION_ARGS)
-{
-    Datum          retval;
-
-    if (CALLED_AS_TRIGGER(fcinfo))
-    {
-        /*
-         * Called as a trigger function
-         */
-        TriggerData    *trigdata = (TriggerData *) fcinfo-&gt;context;
-
-        retval = ...
-    }
-    else
-    {
-        /*
-         * Called as a function
-         */
-
-        retval = ...
-    }
-
-    return retval;
-}
-</programlisting>
-    Only a few thousand lines of code have to be added instead of the
-    dots to complete the call handler.
-   </para>
-
-   <para>
-    After having compiled the handler function into a loadable module
-    (see <xref linkend="dfunc"/>), the following commands then
-    register the sample procedural language:
-<programlisting>
-CREATE FUNCTION plsample_call_handler() RETURNS language_handler
-    AS '<replaceable>filename</replaceable>'
-    LANGUAGE C;
-CREATE LANGUAGE plsample
-    HANDLER plsample_call_handler;
-</programlisting>
+    A template for a procedural-language handler written as a C extension is
+    provided in <literal>src/test/modules/plsample</literal>.  This is a
+    working sample demonstrating one way to create a procedural-language
+    handler, process parameters, and return a value.  A few thousand lines of
+    additional code may have to be added to complete a fully functional
+    handler.
    </para>
 
    <para>
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 29de73c060..95144d8d7c 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -9,6 +9,7 @@ SUBDIRS = \
 		  commit_ts \
 		  dummy_index_am \
 		  dummy_seclabel \
+		  plsample \
 		  snapshot_too_old \
 		  test_bloomfilter \
 		  test_ddl_deparse \
diff --git a/src/test/modules/plsample/Makefile b/src/test/modules/plsample/Makefile
new file mode 100644
index 0000000000..757b47c785
--- /dev/null
+++ b/src/test/modules/plsample/Makefile
@@ -0,0 +1,20 @@
+# src/test/modules/plsample/Makefile
+
+PGFILEDESC = "PL/Sample - procedural language"
+
+REGRESS = create_pl create_func select_func
+
+EXTENSION = plsample
+EXTVERSION = 0.1
+
+MODULE_big = plsample
+
+OBJS = plsample.o
+
+DATA = plsample.control plsample--0.1.sql
+
+plsample.o: plsample.c
+
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
diff --git a/src/test/modules/plsample/README b/src/test/modules/plsample/README
new file mode 100644
index 0000000000..7ee213700b
--- /dev/null
+++ b/src/test/modules/plsample/README
@@ -0,0 +1,3 @@
+plsample is an example procedural-language handler.  It is a simple functional
+template that demonstrates some of the things that need to be done in order to
+build a fully functional procedural-language handler.
diff --git a/src/test/modules/plsample/expected/create_func.out b/src/test/modules/plsample/expected/create_func.out
new file mode 100644
index 0000000000..df2b915a97
--- /dev/null
+++ b/src/test/modules/plsample/expected/create_func.out
@@ -0,0 +1,5 @@
+CREATE FUNCTION plsample_func(a1 NUMERIC, a2 TEXT, a3 INTEGER[])
+RETURNS TEXT
+AS $$
+  This is function's source text.
+$$ LANGUAGE plsample;
diff --git a/src/test/modules/plsample/expected/create_pl.out b/src/test/modules/plsample/expected/create_pl.out
new file mode 100644
index 0000000000..5365391284
--- /dev/null
+++ b/src/test/modules/plsample/expected/create_pl.out
@@ -0,0 +1,8 @@
+CREATE FUNCTION plsample_call_handler()
+RETURNS language_handler
+AS '$libdir/plsample'
+LANGUAGE C;
+CREATE LANGUAGE plsample
+HANDLER plsample_call_handler;
+COMMENT ON LANGUAGE plsample
+IS 'PL/Sample procedural language';
diff --git a/src/test/modules/plsample/expected/select_func.out b/src/test/modules/plsample/expected/select_func.out
new file mode 100644
index 0000000000..dc396cbc04
--- /dev/null
+++ b/src/test/modules/plsample/expected/select_func.out
@@ -0,0 +1,8 @@
+SELECT plsample_func(1.23, 'abc', '{4, 5, 6}');
+           plsample_func           
+-----------------------------------
+                                  +
+   This is function's source text.+
+ 
+(1 row)
+
diff --git a/src/test/modules/plsample/plsample--0.1.sql b/src/test/modules/plsample/plsample--0.1.sql
new file mode 100644
index 0000000000..b429b83ceb
--- /dev/null
+++ b/src/test/modules/plsample/plsample--0.1.sql
@@ -0,0 +1,9 @@
+CREATE FUNCTION plsample_call_handler()
+RETURNS language_handler
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
+
+CREATE LANGUAGE plsample
+HANDLER plsample_call_handler;
+
+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 0000000000..69f76e6067
--- /dev/null
+++ b/src/test/modules/plsample/plsample.c
@@ -0,0 +1,156 @@
+/*-------------------------------------------------------------------------
+ *
+ * plsample.c
+ *      Handler for the PL/Sample procedural language
+ *
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/plsample.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include <postgres.h>
+#include <fmgr.h>
+#include <funcapi.h>
+#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 <utils/builtins.h>
+#include <utils/elog.h>
+#include <utils/memutils.h>
+#include <utils/lsyscache.h>
+#include <utils/syscache.h>
+
+MemoryContext TopMemoryContext = NULL;
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(plsample_call_handler);
+
+static Datum plsample_func_handler(PG_FUNCTION_ARGS);
+
+/*
+ * Handle function, procedure, and trigger calls.
+ */
+Datum
+plsample_call_handler(PG_FUNCTION_ARGS)
+{
+	Datum retval = (Datum) 0;
+
+	PG_TRY();
+	{
+		/*
+		 * Determine if called as function or trigger and call appropriate
+		 * subhandler.
+		 */
+		if (CALLED_AS_TRIGGER(fcinfo))
+		{
+			/* TODO: Invoke the trigger handler. */
+		}
+		else if (CALLED_AS_EVENT_TRIGGER(fcinfo))
+		{
+			/* TODO: Invoke the event trigger handler. */
+		}
+		else
+		{
+			/* Invoke the regular function handler. */
+			retval = plsample_func_handler(fcinfo);
+		}
+	}
+	PG_FINALLY();
+	{
+	}
+	PG_END_TRY();
+	return retval;
+}
+
+/* Handler for regular function and stored procedure calls. */
+static Datum
+plsample_func_handler(PG_FUNCTION_ARGS)
+{
+	HeapTuple pl_tuple;
+	Datum ret;
+	char *source;
+	bool isnull;
+
+	int i;
+	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 *value;
+
+	Form_pg_type pg_type_entry;
+	Oid result_typioparam;
+	FmgrInfo result_in_func;
+
+	/* Fetch the source 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);
+	pl_struct = (Form_pg_proc) GETSTRUCT(pl_tuple);
+
+	ret = SysCacheGetAttr(PROCOID, pl_tuple, Anum_pg_proc_prosrc, &isnull);
+	if (isnull)
+		elog(ERROR, "null prosrc");
+	ReleaseSysCache(pl_tuple);
+
+	source = DatumGetCString(DirectFunctionCall1(textout, ret));
+	elog(LOG, "source text:\n%s", source);
+
+	arg_out_func = (FmgrInfo *) palloc0(fcinfo->nargs * sizeof(FmgrInfo));
+	proc_cxt = AllocSetContextCreate(TopMemoryContext,
+			"PL/Sample function", 0, (1 * 1024), (8 * 1024));
+	get_func_arg_info(pl_tuple, &argtypes, &argnames, &argmodes);
+
+	/* Iterate through all of the function arguments. */
+	elog(LOG, "number of arguments : %d", fcinfo->nargs);
+	for (i = 0; i < fcinfo->nargs; i++)
+	{
+		Oid argtype = pl_struct->proargtypes.values[i];
+		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);
+
+		elog(LOG, "argument position: %d; name: %s; value: %s", i, argnames[i],
+				value);
+	}
+
+	/* Fetch the return type of the function. */
+
+	type_tuple = SearchSysCache1(TYPEOID,
+			ObjectIdGetDatum(pl_struct->prorettype));
+	if (!HeapTupleIsValid(type_tuple))
+		elog(ERROR, "cache lookup failed for type %u", pl_struct->prorettype);
+
+	pg_type_entry = (Form_pg_type) GETSTRUCT(type_tuple);
+
+	proc_cxt = AllocSetContextCreate(TopMemoryContext, "PL/Sample function",
+			ALLOCSET_SMALL_SIZES);
+
+	result_typioparam = getTypeIOParam(type_tuple);
+
+	fmgr_info_cxt(pg_type_entry->typinput, &result_in_func, proc_cxt);
+	ReleaseSysCache(type_tuple);
+
+	/* Simply return the function source text. */
+	ret = InputFunctionCall(&result_in_func, source, result_typioparam, -1);
+	PG_RETURN_DATUM(ret);
+}
diff --git a/src/test/modules/plsample/plsample.control b/src/test/modules/plsample/plsample.control
new file mode 100644
index 0000000000..dba58e5abf
--- /dev/null
+++ b/src/test/modules/plsample/plsample.control
@@ -0,0 +1,7 @@
+# plsample extension
+comment = 'PL/Sample'
+default_version = '0.1'
+module_pathname = '$libdir/plsample'
+relocatable = false
+schema = pg_catalog
+superuser = false
diff --git a/src/test/modules/plsample/sql/create_func.sql b/src/test/modules/plsample/sql/create_func.sql
new file mode 100644
index 0000000000..df2b915a97
--- /dev/null
+++ b/src/test/modules/plsample/sql/create_func.sql
@@ -0,0 +1,5 @@
+CREATE FUNCTION plsample_func(a1 NUMERIC, a2 TEXT, a3 INTEGER[])
+RETURNS TEXT
+AS $$
+  This is function's source text.
+$$ LANGUAGE plsample;
diff --git a/src/test/modules/plsample/sql/create_pl.sql b/src/test/modules/plsample/sql/create_pl.sql
new file mode 100644
index 0000000000..c3ace0f1aa
--- /dev/null
+++ b/src/test/modules/plsample/sql/create_pl.sql
@@ -0,0 +1,10 @@
+CREATE FUNCTION plsample_call_handler()
+RETURNS language_handler
+AS '$libdir/plsample'
+LANGUAGE C;
+
+CREATE LANGUAGE plsample
+HANDLER plsample_call_handler;
+
+COMMENT ON LANGUAGE plsample
+IS 'PL/Sample procedural language';
diff --git a/src/test/modules/plsample/sql/select_func.sql b/src/test/modules/plsample/sql/select_func.sql
new file mode 100644
index 0000000000..5ded186984
--- /dev/null
+++ b/src/test/modules/plsample/sql/select_func.sql
@@ -0,0 +1 @@
+SELECT plsample_func(1.23, 'abc', '{4, 5, 6}');
#9Michael Paquier
michael@paquier.xyz
In reply to: Mark Wong (#8)
1 attachment(s)
Re: doc examples for pghandler

On Tue, Aug 11, 2020 at 01:01:10PM -0700, Mark Wong wrote:

Ah, right. For the moment I've added some empty conditionals for
trigger and event trigger handling.

I've created a new entry in the commitfest app. [1] I'll keep at it. :)

Thanks for the patch. I have reviewed and reworked it as the
attached. Some comments below.

+PGFILEDESC = "PL/Sample - procedural language"
+
+REGRESS = create_pl create_func select_func
+
+EXTENSION = plsample
+EXTVERSION = 0.1

This makefile has a couple of mistakes, and can be simplified a lot:
- make check does not work, as you forgot a PGXS part.
- MODULES can just be used as there is only one file (forgot WIN32RES
in OBJS for example)
- DATA does not need the .control file.

.gitignore was missing.

We could just use 1.0 instead of 0.1 for the version number. That's
not a big deal one way or another, but 1.0 is more consistent with the
other modules.

plsample--1.0.sql should complain if attempting to load the file from
psql. Also I have cleaned up the README.

Not sure that there is a point in having three different files for the
regression tests. create_pl.sql is actually not necessary as you
can do the same with CREATE EXTENSION.

The header list of plsample.c was inconsistent with the style used
normally in modules, and I have extended a bit the handler function so
as we return a result only if the return type of the procedure is text
for the source text of the function, tweaked the results a bit, etc.
There was a family of small issues, like using ALLOCSET_SMALL_SIZES
for the context creation. We could of course expand the sample
handler more in the future to check for pseudotype results, have a
validator, but that could happen later, if necessary.
--
Michael

Attachments:

0001-Add-PL-sample-to-src-test-modules.patchtext/x-diff; charset=us-asciiDownload
From fb017d6277a76653385ae7179307177d20dbe194 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 14 Aug 2020 14:24:15 +0900
Subject: [PATCH] Add PL/sample to src/test/modules/

---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/plsample/.gitignore          |   3 +
 src/test/modules/plsample/Makefile            |  20 ++
 src/test/modules/plsample/README              |   6 +
 .../modules/plsample/expected/plsample.out    |  36 ++++
 src/test/modules/plsample/plsample--1.0.sql   |  14 ++
 src/test/modules/plsample/plsample.c          | 186 ++++++++++++++++++
 src/test/modules/plsample/plsample.control    |   8 +
 src/test/modules/plsample/sql/plsample.sql    |  15 ++
 doc/src/sgml/plhandler.sgml                   |  62 +-----
 10 files changed, 295 insertions(+), 56 deletions(-)
 create mode 100644 src/test/modules/plsample/.gitignore
 create mode 100644 src/test/modules/plsample/Makefile
 create mode 100644 src/test/modules/plsample/README
 create mode 100644 src/test/modules/plsample/expected/plsample.out
 create mode 100644 src/test/modules/plsample/plsample--1.0.sql
 create mode 100644 src/test/modules/plsample/plsample.c
 create mode 100644 src/test/modules/plsample/plsample.control
 create mode 100644 src/test/modules/plsample/sql/plsample.sql

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 1428529b04..a6d2ffbf9e 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -10,6 +10,7 @@ SUBDIRS = \
 		  delay_execution \
 		  dummy_index_am \
 		  dummy_seclabel \
+		  plsample \
 		  snapshot_too_old \
 		  test_bloomfilter \
 		  test_ddl_deparse \
diff --git a/src/test/modules/plsample/.gitignore b/src/test/modules/plsample/.gitignore
new file mode 100644
index 0000000000..44d119cfcc
--- /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 0000000000..f1bc334bfc
--- /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 0000000000..7d44d7b3f2
--- /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
+kept a maximum simple, and 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 0000000000..a0c318b6df
--- /dev/null
+++ b/src/test/modules/plsample/expected/plsample.out
@@ -0,0 +1,36 @@
+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)
+
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 0000000000..fc5b280bd4
--- /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 0000000000..41d02105fd
--- /dev/null
+++ b/src/test/modules/plsample/plsample.c
@@ -0,0 +1,186 @@
+/*-------------------------------------------------------------------------
+ *
+ * plsample.c
+ *	  Handler for the PL/Sample procedural language
+ *
+ * Portions Copyright (c) 1996-2020, 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 "access/htup_details.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_type.h"
+#include "commands/event_trigger.h"
+#include "commands/trigger.h"
+#include "funcapi.h"
+#include "utils/builtins.h"
+#include "utils/elog.h"
+#include "utils/memutils.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);
+
+/*
+ * Handle function, procedure, and trigger calls.
+ */
+Datum
+plsample_call_handler(PG_FUNCTION_ARGS)
+{
+	Datum		retval = (Datum) 0;
+
+	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.
+			 */
+		}
+		else if (CALLED_AS_EVENT_TRIGGER(fcinfo))
+		{
+			/*
+			 * This function is called as an event trigger function, where
+			 * (EventTriggerData *) fcinfo->context include the information of
+			 * the context.
+			 */
+		}
+		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;
+	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);
+	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);
+	ReleaseSysCache(pl_tuple);
+	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)));
+	}
+
+	/*
+	 * 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 (pl_struct->prorettype != TEXTOID)
+		PG_RETURN_NULL();
+
+	type_tuple = SearchSysCache1(TYPEOID,
+								 ObjectIdGetDatum(pl_struct->prorettype));
+	if (!HeapTupleIsValid(type_tuple))
+		elog(ERROR, "cache lookup failed for type %u", pl_struct->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);
+}
diff --git a/src/test/modules/plsample/plsample.control b/src/test/modules/plsample/plsample.control
new file mode 100644
index 0000000000..1e67251a1e
--- /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 0000000000..bf0fddac7f
--- /dev/null
+++ b/src/test/modules/plsample/sql/plsample.sql
@@ -0,0 +1,15 @@
+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}');
diff --git a/doc/src/sgml/plhandler.sgml b/doc/src/sgml/plhandler.sgml
index e1b0af7a60..7b2c5624c0 100644
--- a/doc/src/sgml/plhandler.sgml
+++ b/doc/src/sgml/plhandler.sgml
@@ -96,62 +96,12 @@
    </para>
 
    <para>
-    This is a template for a procedural-language handler written in C:
-<programlisting>
-#include "postgres.h"
-#include "executor/spi.h"
-#include "commands/trigger.h"
-#include "fmgr.h"
-#include "access/heapam.h"
-#include "utils/syscache.h"
-#include "catalog/pg_proc.h"
-#include "catalog/pg_type.h"
-
-PG_MODULE_MAGIC;
-
-PG_FUNCTION_INFO_V1(plsample_call_handler);
-
-Datum
-plsample_call_handler(PG_FUNCTION_ARGS)
-{
-    Datum          retval;
-
-    if (CALLED_AS_TRIGGER(fcinfo))
-    {
-        /*
-         * Called as a trigger function
-         */
-        TriggerData    *trigdata = (TriggerData *) fcinfo-&gt;context;
-
-        retval = ...
-    }
-    else
-    {
-        /*
-         * Called as a function
-         */
-
-        retval = ...
-    }
-
-    return retval;
-}
-</programlisting>
-    Only a few thousand lines of code have to be added instead of the
-    dots to complete the call handler.
-   </para>
-
-   <para>
-    After having compiled the handler function into a loadable module
-    (see <xref linkend="dfunc"/>), the following commands then
-    register the sample procedural language:
-<programlisting>
-CREATE FUNCTION plsample_call_handler() RETURNS language_handler
-    AS '<replaceable>filename</replaceable>'
-    LANGUAGE C;
-CREATE LANGUAGE plsample
-    HANDLER plsample_call_handler;
-</programlisting>
+    A template for a procedural-language handler written as a C extension is
+    provided in <literal>src/test/modules/plsample</literal>.  This is a
+    working sample demonstrating one way to create a procedural-language
+    handler, process parameters, and return a value.  A few thousand lines of
+    additional code may have to be added to complete a fully functional
+    handler.
    </para>
 
    <para>
-- 
2.28.0

#10Mark Wong
mark@2ndquadrant.com
In reply to: Michael Paquier (#9)
1 attachment(s)
Re: doc examples for pghandler

On Fri, Aug 14, 2020 at 02:25:52PM +0900, Michael Paquier wrote:

On Tue, Aug 11, 2020 at 01:01:10PM -0700, Mark Wong wrote:

Ah, right. For the moment I've added some empty conditionals for
trigger and event trigger handling.

I've created a new entry in the commitfest app. [1] I'll keep at it. :)

Thanks for the patch. I have reviewed and reworked it as the
attached. Some comments below.

+PGFILEDESC = "PL/Sample - procedural language"
+
+REGRESS = create_pl create_func select_func
+
+EXTENSION = plsample
+EXTVERSION = 0.1

This makefile has a couple of mistakes, and can be simplified a lot:
- make check does not work, as you forgot a PGXS part.
- MODULES can just be used as there is only one file (forgot WIN32RES
in OBJS for example)
- DATA does not need the .control file.

.gitignore was missing.

We could just use 1.0 instead of 0.1 for the version number. That's
not a big deal one way or another, but 1.0 is more consistent with the
other modules.

plsample--1.0.sql should complain if attempting to load the file from
psql. Also I have cleaned up the README.

Not sure that there is a point in having three different files for the
regression tests. create_pl.sql is actually not necessary as you
can do the same with CREATE EXTENSION.

The header list of plsample.c was inconsistent with the style used
normally in modules, and I have extended a bit the handler function so
as we return a result only if the return type of the procedure is text
for the source text of the function, tweaked the results a bit, etc.
There was a family of small issues, like using ALLOCSET_SMALL_SIZES
for the context creation. We could of course expand the sample
handler more in the future to check for pseudotype results, have a
validator, but that could happen later, if necessary.

Thanks for fixing all of that up for me. I did have a couple mental
notes for a couple of the last items. :)

I've attached a small word diff to suggest a few different words to use
in the README, if that sounds better?

Regards,
Mark
--
Mark Wong
2ndQuadrant - PostgreSQL Solutions for the Enterprise
https://www.2ndQuadrant.com/

Attachments:

plsample-readme.difftext/plain; charset=us-asciiDownload
diff --git a/src/test/modules/plsample/README b/src/test/modules/plsample/README
index 7d44d7b3f2..afe3fa6402 100644
--- a/src/test/modules/plsample/README
+++ b/src/test/modules/plsample/README
@@ -2,5 +2,5 @@ PL/Sample
=========

PL/Sample is an example template of procedural-language handler.  It is
[-kept-]a [-maximum simple, and-]{+simple implementation, yet+} demonstrates some of the things that can be
done to build a fully functional procedural-language handler.
#11Michael Paquier
michael@paquier.xyz
In reply to: Mark Wong (#10)
Re: doc examples for pghandler

On Mon, Aug 17, 2020 at 04:30:07PM -0700, Mark Wong wrote:

I've attached a small word diff to suggest a few different words to use
in the README, if that sounds better?

Sounds good to me. So applied with those changes. It is really
tempting to add an example of validator (one simple thing would be to
return an error if trying to use TRIGGEROID or EVTTRIGGEROID), but
that may not be the best thing to do for a test module. And what we
have here is already much better than the original docs.
--
Michael