From 339b85536a26be71b1b5eeaba6ae2f483f8a857b Mon Sep 17 00:00:00 2001
From: Chapman Flack <chap@anastigmatix.net>
Date: Thu, 7 Apr 2022 20:52:18 -0400
Subject: [PATCH v2 4/4] PL/Sample "with" TRANSFORM FOR TYPE

Add just enough TRANSFORM handling to PL/Sample for it to announce
what functions would be called (in a real implementation) on which
return or argument types.

Add a test based on creating a completely bogus transform out of
existing functions that have the right parameter and return types
but otherwise no business at all being used for a transform. Because
PL/Sample will not really call them, that will work as a test.

In passing: turn a SearchSysCache(..., 0, 0, 0) to SearchSysCache1
so the example code is following the recommended practice.
---
 src/test/modules/plsample/expected/plsample.out | 31 ++++++++++
 src/test/modules/plsample/plsample.c            | 80 +++++++++++++++++++++++++
 src/test/modules/plsample/sql/plsample.sql      | 18 ++++++
 3 files changed, 129 insertions(+)

diff --git a/src/test/modules/plsample/expected/plsample.out b/src/test/modules/plsample/expected/plsample.out
index 8ad5f7a..bab1cb3 100644
--- a/src/test/modules/plsample/expected/plsample.out
+++ b/src/test/modules/plsample/expected/plsample.out
@@ -34,6 +34,37 @@ NOTICE:  argument: 0; name: a1; value: {foo,bar,hoge}
  
 (1 row)
 
+/*
+ * Create a bogus transform using a couple existing functions that
+ * happen to have the right parameter and return types. Calamity does
+ * not ensue, because plsample will merely announce what transform functions
+ * it "would" apply, and not really call them.
+ */
+CREATE TRANSFORM FOR text LANGUAGE plsample (
+  FROM SQL WITH FUNCTION prsd_lextype, TO SQL WITH FUNCTION textrecv
+);
+CREATE OR REPLACE FUNCTION
+  plsample_result_text(a1 numeric, a2 text, a3 integer[])
+RETURNS text
+TRANSFORM FOR TYPE 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; transformed with: prsd_lextype(internal)
+NOTICE:  argument: 2; name: a3; value: {4,5,6}
+NOTICE:  return value: transformed with: textrecv(internal)
+         plsample_result_text          
+---------------------------------------
+                                      +
+   Example of source with text result.+
+ 
+(1 row)
+
 CREATE FUNCTION my_trigger_func() RETURNS trigger AS $$
 if TD_event == "INSERT"
     return TD_NEW
diff --git a/src/test/modules/plsample/plsample.c b/src/test/modules/plsample/plsample.c
index 780db72..51fee16 100644
--- a/src/test/modules/plsample/plsample.c
+++ b/src/test/modules/plsample/plsample.c
@@ -23,6 +23,7 @@
 #include "funcapi.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
+#include "utils/regproc.h"   /* only for format_procedure() */
 #include "utils/syscache.h"
 
 PG_MODULE_MAGIC;
@@ -102,12 +103,15 @@ plsample_func_handler(PG_FUNCTION_ARGS)
 	Form_pg_proc pl_struct;
 	volatile MemoryContext proc_cxt = NULL;
 	Oid		   *argtypes;
+	List	   *trftypes;
 	char	  **argnames;
 	char	   *argmodes;
 	char	   *proname;
 	Form_pg_type pg_type_entry;
 	Oid			result_typioparam;
 	Oid			prorettype;
+	Oid			prolang;
+	Oid			tosql;
 	FmgrInfo	result_in_func;
 	int			numargs;
 
@@ -123,6 +127,7 @@ plsample_func_handler(PG_FUNCTION_ARGS)
 	 * a base for the function validation and execution.
 	 */
 	pl_struct = (Form_pg_proc) GETSTRUCT(pl_tuple);
+	prolang = pl_struct->prolang;
 	proname = pstrdup(NameStr(pl_struct->proname));
 	ret = SysCacheGetAttr(PROCOID, pl_tuple, Anum_pg_proc_prosrc, &isnull);
 	if (isnull)
@@ -141,26 +146,92 @@ plsample_func_handler(PG_FUNCTION_ARGS)
 									 "PL/Sample function",
 									 ALLOCSET_SMALL_SIZES);
 
+	/*
+	 * This array of FmgrInfo will hold, for each supplied input argument,
+	 * the resolved output-to-text function for that argument's type.
+	 * fcinfo->nargs comes from the caller, and reflects the number of input
+	 * arguments supplied in this call.
+	 */
 	arg_out_func = (FmgrInfo *) palloc0(fcinfo->nargs * sizeof(FmgrInfo));
+
+	/*
+	 * Obtain the types, names, and modes (IN/OUT/INOUT/...) for all of
+	 * the function's statically declared arguments, returning their number
+	 * (giving the size of each result array). numargs can be larger than
+	 * fcinfo->nargs (which only reflects IN arguments). numargs can also be
+	 * smaller than fcinfo->nargs (in the case of a function declared
+	 * VARIADIC "any", but it is rare for a PL to support that).
+	 */
 	numargs = get_func_arg_info(pl_tuple, &argtypes, &argnames, &argmodes);
 
 	/*
+	 * For now, PL/Sample does not support any argument with mode other than IN.
+	 * When mode is IN for every argument, argmodes will be NULL, so report
+	 * an error if it is not. To more helpfully report the error at the time
+	 * of declaration, a validator function can check this too.
+	 */
+	if ( argmodes != NULL )
+		ereport(ERROR,
+				(errmsg("only IN arguments supported for PL/Sample")));
+
+	/*
+	 * Obtain the types for which transforms should be applied.
+	 * For a PL that does not support transforms, it could be considerate to
+	 * report an error here (or earlier, in a validator) if trftypes is not NIL,
+	 * rather than simply ignoring any transforms that might be mistakenly
+	 * declared.
+	 */
+	trftypes = get_call_trftypes(pl_tuple);
+
+	/*
 	 * 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];
+		Oid			fromsql;
 		char	   *value;
 
+		/*
+		 * Get the Oid of the "from SQL" transform function if this arg's type
+		 * is one of those to which transforms should be applied, or InvalidOid
+		 * if not.
+		 */
+		fromsql = get_transform_fromsql(argtype, prolang, trftypes);
+
 		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);
+
+		/*
+		 * Look up the type's output function and save the details
+		 * in arg_out_func[i].
+		 */
 		fmgr_info_cxt(type_struct->typoutput, &(arg_out_func[i]), proc_cxt);
+
 		ReleaseSysCache(type_tuple);
 
+		/*
+		 * If a transform function was found for this arg, use it instead of
+		 * the default conversion below. In this example, "use it" means
+		 * reporting the transform function's name. A real PL would, of course,
+		 * apply that function to the argument value and use the result.
+		 */
+		if ( OidIsValid(fromsql) )
+		{
+			ereport(NOTICE,
+					(errmsg("argument: %d; name: %s; transformed with: %s",
+							i, argnames[i], format_procedure(fromsql))));
+			continue;
+		}
+
+		/*
+		 * No transform, so apply the default conversion that calls the type's
+		 * output function to obtain a string.
+		 */
 		value = OutputFunctionCall(&arg_out_func[i], fcinfo->args[i].value);
 		ereport(NOTICE,
 				(errmsg("argument: %d; name: %s; value: %s",
@@ -182,6 +253,15 @@ plsample_func_handler(PG_FUNCTION_ARGS)
 	if (prorettype != TEXTOID)
 		PG_RETURN_NULL();
 
+	tosql = get_transform_tosql(prorettype, prolang, trftypes);
+
+	if ( OidIsValid(tosql) )
+	{
+		ereport(NOTICE,
+				(errmsg("return value: transformed with: %s",
+						format_procedure(tosql))));
+	}
+
 	type_tuple = SearchSysCache1(TYPEOID,
 								 ObjectIdGetDatum(prorettype));
 	if (!HeapTupleIsValid(type_tuple))
diff --git a/src/test/modules/plsample/sql/plsample.sql b/src/test/modules/plsample/sql/plsample.sql
index cf652ad..4a31876 100644
--- a/src/test/modules/plsample/sql/plsample.sql
+++ b/src/test/modules/plsample/sql/plsample.sql
@@ -14,6 +14,24 @@ AS $$
 $$ LANGUAGE plsample;
 SELECT plsample_result_void('{foo, bar, hoge}');
 
+/*
+ * Create a bogus transform using a couple existing functions that
+ * happen to have the right parameter and return types. Calamity does
+ * not ensue, because plsample will merely announce what transform functions
+ * it "would" apply, and not really call them.
+ */
+CREATE TRANSFORM FOR text LANGUAGE plsample (
+  FROM SQL WITH FUNCTION prsd_lextype, TO SQL WITH FUNCTION textrecv
+);
+CREATE OR REPLACE FUNCTION
+  plsample_result_text(a1 numeric, a2 text, a3 integer[])
+RETURNS text
+TRANSFORM FOR TYPE text
+AS $$
+  Example of source with text result.
+$$ LANGUAGE plsample;
+SELECT plsample_result_text(1.23, 'abc', '{4, 5, 6}');
+
 CREATE FUNCTION my_trigger_func() RETURNS trigger AS $$
 if TD_event == "INSERT"
     return TD_NEW
-- 
2.7.3

