Allow agtype_build_map to use AGTYPE keys (#1666)

Added the ability for agtype_build_map to use AGTYPE keys.

In doing this, age_tostring needed to be broken up and rewritten
to handle UNKNOWNOID. This rewrite allows other functions to use
the logic in age_tostring without using age_tostring directly.

Additionally, rewrote age_tostring as a ("any") type function; it
used to be a (variadic "any"). The logic here can be used to re-
write other (variadic "any") functions that have a set number of
input parameters.

Added regression tests.
diff --git a/age--1.5.0--y.y.y.sql b/age--1.5.0--y.y.y.sql
index 23287a8..0239d43 100644
--- a/age--1.5.0--y.y.y.sql
+++ b/age--1.5.0--y.y.y.sql
@@ -88,3 +88,13 @@
   FUNCTION 6 ag_catalog.gin_triconsistent_agtype(internal, int2, agtype, int4,
                                                  internal, internal, internal),
 STORAGE text;
+
+-- this function went from variadic "any" to just "any" type
+CREATE OR REPLACE FUNCTION ag_catalog.age_tostring("any")
+    RETURNS agtype
+    LANGUAGE c
+    IMMUTABLE
+RETURNS NULL ON NULL INPUT
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
diff --git a/regress/expected/agtype.out b/regress/expected/agtype.out
index 66344c6..b3750bc 100644
--- a/regress/expected/agtype.out
+++ b/regress/expected/agtype.out
@@ -3139,7 +3139,7 @@
 --
 --Invalid Map Key (should fail)
 SELECT agtype_build_map('[0]'::agtype, null);
-ERROR:  key value must be scalar, not array, composite, or json
+ERROR:  agtype_build_map_as_agtype_value only supports scalar arguments
 --
 -- Test agtype object/array access operators object.property, object["property"], and array[element]
 -- Note: At this point, object.property and object["property"] are equivalent.
@@ -3908,8 +3908,50 @@
 SELECT ag_catalog.agtype_volatile_wrapper(-32768::int2);
 ERROR:  smallint out of range
 --
+-- test that age_tostring can handle an UNKNOWNOID type
+--
+SELECT age_tostring('a');
+ age_tostring 
+--------------
+ "a"
+(1 row)
+
+--
+-- test agtype_build_map_as_agtype_value via agtype_build_map
+--
+SELECT * FROM create_graph('agtype_build_map');
+NOTICE:  graph "agtype_build_map" has been created
+ create_graph 
+--------------
+ 
+(1 row)
+
+SELECT * FROM cypher('agtype_build_map', $$ RETURN ag_catalog.agtype_build_map('1', '1', 2, 2, 3.14, 3.14, 'e', 2.71)
+                                         $$) AS (results agtype);
+                   results                   
+---------------------------------------------
+ {"1": "1", "2": 2, "e": 2.71, "3.14": 3.14}
+(1 row)
+
+SELECT agtype_build_map('1', '1', 2, 2, 3.14, 3.14, 'e', 2.71);
+                       agtype_build_map                        
+---------------------------------------------------------------
+ {"1": "1", "2": 2, "e": 2.71::numeric, "3.14": 3.14::numeric}
+(1 row)
+
+--
 -- Cleanup
 --
+SELECT drop_graph('agtype_build_map', true);
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to table agtype_build_map._ag_label_vertex
+drop cascades to table agtype_build_map._ag_label_edge
+NOTICE:  graph "agtype_build_map" has been dropped
+ drop_graph 
+------------
+ 
+(1 row)
+
 DROP TABLE agtype_table;
 --
 -- End of AGTYPE data type regression tests
diff --git a/regress/sql/agtype.sql b/regress/sql/agtype.sql
index 4089d52..9e8d44e 100644
--- a/regress/sql/agtype.sql
+++ b/regress/sql/agtype.sql
@@ -1098,9 +1098,25 @@
 -- These should fail
 SELECT ag_catalog.agtype_volatile_wrapper(32768::int2);
 SELECT ag_catalog.agtype_volatile_wrapper(-32768::int2);
+
+--
+-- test that age_tostring can handle an UNKNOWNOID type
+--
+SELECT age_tostring('a');
+
+--
+-- test agtype_build_map_as_agtype_value via agtype_build_map
+--
+SELECT * FROM create_graph('agtype_build_map');
+SELECT * FROM cypher('agtype_build_map', $$ RETURN ag_catalog.agtype_build_map('1', '1', 2, 2, 3.14, 3.14, 'e', 2.71)
+                                         $$) AS (results agtype);
+SELECT agtype_build_map('1', '1', 2, 2, 3.14, 3.14, 'e', 2.71);
+
 --
 -- Cleanup
 --
+SELECT drop_graph('agtype_build_map', true);
+
 DROP TABLE agtype_table;
 
 --
diff --git a/sql/age_scalar.sql b/sql/age_scalar.sql
index 77f7689..5014fbb 100644
--- a/sql/age_scalar.sql
+++ b/sql/age_scalar.sql
@@ -149,7 +149,7 @@
 PARALLEL SAFE
 AS 'MODULE_PATHNAME';
 
-CREATE FUNCTION ag_catalog.age_tostring(variadic "any")
+CREATE FUNCTION ag_catalog.age_tostring("any")
     RETURNS agtype
     LANGUAGE c
     IMMUTABLE
diff --git a/src/backend/utils/adt/agtype.c b/src/backend/utils/adt/agtype.c
index 9de22c7..9fd0bb8 100644
--- a/src/backend/utils/adt/agtype.c
+++ b/src/backend/utils/adt/agtype.c
@@ -176,6 +176,7 @@
                                      int min_num_args);
 static agtype_value *agtype_build_map_as_agtype_value(FunctionCallInfo fcinfo);
 agtype_value *agtype_composite_to_agtype_value_binary(agtype *a);
+static agtype_value *tostring_helper(Datum arg, Oid type, char *msghdr);
 
 /* global storage of  OID for agtype and _agtype */
 static Oid g_AGTYPEOID = InvalidOid;
@@ -2379,6 +2380,7 @@
     result.res = push_agtype_value(&result.parse_state, WAGT_BEGIN_OBJECT,
                                    NULL);
 
+    /* iterate through the arguments and build the object */
     for (i = 0; i < nargs; i += 2)
     {
         /* process key */
@@ -2389,7 +2391,25 @@
                      errmsg("argument %d: key must not be null", i + 1)));
         }
 
-        add_agtype(args[i], false, &result, types[i], true);
+        /*
+         * If the key is agtype, we need to extract it as an agtype string and
+         * push the value.
+         */
+        if (types[i] == AGTYPEOID)
+        {
+            agtype_value *agtv = NULL;
+
+            agtv = tostring_helper(args[i], types[i],
+                                   "agtype_build_map_as_agtype_value");
+            result.res = push_agtype_value(&result.parse_state, WAGT_KEY, agtv);
+
+            /* free the agtype_value from tostring_helper */
+            pfree(agtv);
+        }
+        else
+        {
+            add_agtype(args[i], false, &result, types[i], true);
+        }
 
         /* process value */
         add_agtype(args[i + 1], nulls[i + 1], &result, types[i + 1], false);
@@ -6748,16 +6768,12 @@
 Datum age_tostring(PG_FUNCTION_ARGS)
 {
     int nargs;
-    Datum *args;
     Datum arg;
-    bool *nulls;
-    Oid *types;
-    agtype_value agtv_result;
-    char *string = NULL;
-    Oid type;
+    Oid type = InvalidOid;
+    agtype *agt = NULL;
+    agtype_value *agtv = NULL;
 
-    /* extract argument values */
-    nargs = extract_variadic_args(fcinfo, 0, true, &args, &types, &nulls);
+    nargs = PG_NARGS();
 
     /* check number of args */
     if (nargs > 1)
@@ -6767,19 +6783,70 @@
     }
 
     /* check for null */
-    if (nargs < 0 || nulls[0])
+    if (nargs < 1 || PG_ARGISNULL(0))
     {
         PG_RETURN_NULL();
     }
 
+    /* get the argument and type */
+    arg = PG_GETARG_DATUM(0);
+    type = get_fn_expr_argtype(fcinfo->flinfo, 0);
+
+    /* verify that if the type is UNKNOWNOID it can be converted */
+    if (type == UNKNOWNOID && !get_fn_expr_arg_stable(fcinfo->flinfo, 0))
+    {
+        ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                        errmsg("toString() UNKNOWNOID and not stable")));
+    }
+
     /*
      * toString() supports integer, float, numeric, text, cstring, boolean,
      * regtype or the agtypes: integer, float, numeric, string, boolean input
      */
-    arg = args[0];
-    type = types[0];
+    agtv = tostring_helper(arg, type, "toString()");
 
-    if (type != AGTYPEOID)
+    /* if we get a NULL back we need to return NULL */
+    if (agtv == NULL)
+    {
+        PG_RETURN_NULL();
+    }
+
+    /* convert to agtype and free the agtype_value */
+    agt = agtype_value_to_agtype(agtv);
+    pfree(agtv);
+
+    PG_RETURN_POINTER(agt);
+}
+
+/*
+ * Helper function to take any valid type and convert it to an agtype string.
+ * Returns NULL for NULL output.
+ */
+static agtype_value *tostring_helper(Datum arg, Oid type, char *msghdr)
+{
+    agtype_value *agtv_result = NULL;
+    char *string = NULL;
+
+    agtv_result = palloc0(sizeof(agtype_value));
+
+    /*
+     * toString() supports: unknown, integer, float, numeric, text, cstring,
+     * boolean, regtype or the agtypes: integer, float, numeric, string, and
+     * boolean input.
+     */
+
+    /*
+     * If the type is UNKNOWNOID convert it to a cstring. Prior to passing an
+     * UNKNOWNOID it should be verified to be stable.
+     */
+    if (type == UNKNOWNOID)
+    {
+        char *str = DatumGetPointer(arg);
+
+        string = pnstrdup(str, strlen(str));
+    }
+    /* if it is not an AGTYPEOID */
+    else if (type != AGTYPEOID)
     {
         if (type == INT2OID)
         {
@@ -6826,11 +6893,12 @@
         else
         {
             ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                            errmsg("toString() unsupported argument type %d",
-                                   type)));
+                            errmsg("%s unsupported argument type %d",
+                                   msghdr, type)));
         }
     }
-    else
+    /* if it is an AGTYPEOID */
+    else if (type == AGTYPEOID)
     {
         agtype *agt_arg;
         agtype_value *agtv_value;
@@ -6841,14 +6909,15 @@
         if (!AGT_ROOT_IS_SCALAR(agt_arg))
         {
             ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                            errmsg("toString() only supports scalar arguments")));
+                            errmsg("%s only supports scalar arguments",
+                                   msghdr)));
         }
 
         agtv_value = get_ith_agtype_value_from_container(&agt_arg->root, 0);
 
         if (agtv_value->type == AGTV_NULL)
         {
-            PG_RETURN_NULL();
+            return NULL;
         }
         else if (agtv_value->type == AGTV_INTEGER)
         {
@@ -6877,17 +6946,24 @@
         else
         {
             ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                            errmsg("toString() unsupported argument agtype %d",
-                                   agtv_value->type)));
+                            errmsg("%s unsupported argument agtype %d",
+                                   msghdr, agtv_value->type)));
         }
     }
+    /* it is an unknown type */
+    else
+    {
+        ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                            errmsg("%s unknown argument agtype %d",
+                                   msghdr, type)));
+    }
 
     /* build the result */
-    agtv_result.type = AGTV_STRING;
-    agtv_result.val.string.val = string;
-    agtv_result.val.string.len = strlen(string);
+    agtv_result->type = AGTV_STRING;
+    agtv_result->val.string.val = string;
+    agtv_result->val.string.len = strlen(string);
 
-    PG_RETURN_POINTER(agtype_value_to_agtype(&agtv_result));
+    return agtv_result;
 }
 
 PG_FUNCTION_INFO_V1(age_tostringlist);