Fix concat() and format() to handle VARIADIC-labeled arguments correctly.
authorTom Lane <tgl@sss.pgh.pa.us>
Fri, 25 Jan 2013 05:19:18 +0000 (00:19 -0500)
committerTom Lane <tgl@sss.pgh.pa.us>
Fri, 25 Jan 2013 05:19:56 +0000 (00:19 -0500)
Previously, the VARIADIC labeling was effectively ignored, but now these
functions act as though the array elements had all been given as separate
arguments.

Pavel Stehule

doc/src/sgml/func.sgml
doc/src/sgml/xfunc.sgml
src/backend/utils/adt/varlena.c
src/test/regress/expected/text.out
src/test/regress/sql/text.sql

index 35c7f75eab2626c3d7a644c39e42101d557b995c..e9dbcb9f8a005731f863b575d44cd2c8b4baa3ad 100644 (file)
        </entry>
        <entry><type>text</type></entry>
        <entry>
-        Concatenate all arguments. NULL arguments are ignored.
+        Concatenate the text representations of all the arguments.
+        NULL arguments are ignored.
        </entry>
        <entry><literal>concat('abcde', 2, NULL, 22)</literal></entry>
        <entry><literal>abcde222</literal></entry>
        </entry>
        <entry><type>text</type></entry>
        <entry>
-        Concatenate all but first arguments with separators. The first
-        parameter is used as a separator. NULL arguments are ignored.
+        Concatenate all but the first argument with separators. The first
+        argument is used as the separator string. NULL arguments are ignored.
        </entry>
        <entry><literal>concat_ws(',', 'abcde', 2, NULL, 22)</literal></entry>
        <entry><literal>abcde,2,22</literal></entry>
        </entry>
        <entry><type>text</type></entry>
        <entry>
-         Format a string.  This function is similar to the C function
-         <function>sprintf</>; but only the following conversion specifications
+         Format arguments according to a format string.
+         This function is similar to the C function
+         <function>sprintf</>, but only the following conversion specifications
          are recognized: <literal>%s</literal> interpolates the corresponding
          argument as a string; <literal>%I</literal> escapes its argument as
          an SQL identifier; <literal>%L</literal> escapes its argument as an
     </tgroup>
    </table>
 
+   <para>
+    The <function>concat</function>, <function>concat_ws</function> and
+    <function>format</function> functions are variadic, so it is possible to
+    pass the values to be concatenated or formatted as an array marked with
+    the <literal>VARIADIC</literal> keyword (see <xref
+    linkend="xfunc-sql-variadic-functions">).  The array's elements are
+    treated as if they were separate ordinary arguments to the function.
+    If the variadic array argument is NULL, <function>concat</function>
+    and <function>concat_ws</function> return NULL, but
+    <function>format</function> treats a NULL as a zero-element array.
+   </para>
+
    <para>
    See also the aggregate function <function>string_agg</function> in
    <xref linkend="functions-aggregate">.
index 85539feb0d255daf486246dee911802b1a274e17..4fb42842c6f88d1e77f8fd4b3cea475194c67e45 100644 (file)
@@ -3153,6 +3153,10 @@ CREATE OR REPLACE FUNCTION retcomposite(IN integer, IN integer,
      <literal>fcinfo-&gt;flinfo</>. The parameter <literal>argnum</>
      is zero based.  <function>get_call_result_type</> can also be used
      as an alternative to <function>get_fn_expr_rettype</>.
+     There is also <function>get_fn_expr_variadic</>, which can be used to
+     find out whether the call contained an explicit <literal>VARIADIC</>
+     keyword.  This is primarily useful for <literal>VARIADIC "any"</>
+     functions, as described below.
     </para>
 
     <para>
@@ -3229,7 +3233,12 @@ CREATE FUNCTION make_array(anyelement) RETURNS anyarray
      as happens with normal variadic functions; they will just be passed to
      the function separately.  The <function>PG_NARGS()</> macro and the
      methods described above must be used to determine the number of actual
-     arguments and their types when using this feature.
+     arguments and their types when using this feature.  Also, users of such
+     a function might wish to use the <literal>VARIADIC</> keyword in their
+     function call, with the expectation that the function would treat the
+     array elements as separate arguments.  The function itself must implement
+     that behavior if wanted, after using <function>get_fn_expr_variadic</> to
+     detect that the actual argument was marked with <literal>VARIADIC</>.
     </para>
    </sect2>
 
index 95e41bf30af1d5958d628b72065abe38b4a98430..e69b7dd3e6b6d5f966fda7a9ccffbd898af519e5 100644 (file)
@@ -76,12 +76,12 @@ static bytea *bytea_substring(Datum str,
                bool length_not_specified);
 static bytea *bytea_overlay(bytea *t1, bytea *t2, int sp, int sl);
 static StringInfo makeStringAggState(FunctionCallInfo fcinfo);
-void text_format_string_conversion(StringInfo buf, char conversion,
-                             Oid typid, Datum value, bool isNull);
-
+static void text_format_string_conversion(StringInfo buf, char conversion,
+                             FmgrInfo *typOutputInfo,
+                             Datum value, bool isNull);
 static Datum text_to_array_internal(PG_FUNCTION_ARGS);
 static text *array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v,
-                      char *fldsep, char *null_string);
+                      const char *fldsep, const char *null_string);
 
 
 /*****************************************************************************
@@ -3451,7 +3451,7 @@ array_to_text_null(PG_FUNCTION_ARGS)
  */
 static text *
 array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v,
-                      char *fldsep, char *null_string)
+                      const char *fldsep, const char *null_string)
 {
    text       *result;
    int         nitems,
@@ -3791,11 +3791,12 @@ string_agg_finalfn(PG_FUNCTION_ARGS)
 /*
  * Implementation of both concat() and concat_ws().
  *
- * sepstr/seplen describe the separator.  argidx is the first argument
- * to concatenate (counting from zero).
+ * sepstr is the separator string to place between values.
+ * argidx identifies the first argument to concatenate (counting from zero).
+ * Returns NULL if result should be NULL, else text value.
  */
 static text *
-concat_internal(const char *sepstr, int seplen, int argidx,
+concat_internal(const char *sepstr, int argidx,
                FunctionCallInfo fcinfo)
 {
    text       *result;
@@ -3803,6 +3804,48 @@ concat_internal(const char *sepstr, int seplen, int argidx,
    bool        first_arg = true;
    int         i;
 
+   /*
+    * concat(VARIADIC some-array) is essentially equivalent to
+    * array_to_text(), ie concat the array elements with the given separator.
+    * So we just pass the case off to that code.
+    */
+   if (get_fn_expr_variadic(fcinfo->flinfo))
+   {
+       Oid         arr_typid;
+       ArrayType  *arr;
+
+       /* Should have just the one argument */
+       Assert(argidx == PG_NARGS() - 1);
+
+       /* concat(VARIADIC NULL) is defined as NULL */
+       if (PG_ARGISNULL(argidx))
+           return NULL;
+
+       /*
+        * Non-null argument had better be an array.  The parser doesn't
+        * enforce this for VARIADIC ANY functions (maybe it should?), so that
+        * check uses ereport not just elog.
+        */
+       arr_typid = get_fn_expr_argtype(fcinfo->flinfo, argidx);
+       if (!OidIsValid(arr_typid))
+           elog(ERROR, "could not determine data type of concat() input");
+
+       if (!OidIsValid(get_element_type(arr_typid)))
+           ereport(ERROR,
+                   (errcode(ERRCODE_DATATYPE_MISMATCH),
+                    errmsg("VARIADIC argument must be an array")));
+
+       /* OK, safe to fetch the array value */
+       arr = PG_GETARG_ARRAYTYPE_P(argidx);
+
+       /*
+        * And serialize the array.  We tell array_to_text to ignore null
+        * elements, which matches the behavior of the loop below.
+        */
+       return array_to_text_internal(fcinfo, arr, sepstr, NULL);
+   }
+
+   /* Normal case without explicit VARIADIC marker */
    initStringInfo(&str);
 
    for (i = argidx; i < PG_NARGS(); i++)
@@ -3818,7 +3861,7 @@ concat_internal(const char *sepstr, int seplen, int argidx,
            if (first_arg)
                first_arg = false;
            else
-               appendBinaryStringInfo(&str, sepstr, seplen);
+               appendStringInfoString(&str, sepstr);
 
            /* call the appropriate type output function, append the result */
            valtype = get_fn_expr_argtype(fcinfo->flinfo, i);
@@ -3842,7 +3885,12 @@ concat_internal(const char *sepstr, int seplen, int argidx,
 Datum
 text_concat(PG_FUNCTION_ARGS)
 {
-   PG_RETURN_TEXT_P(concat_internal("", 0, 0, fcinfo));
+   text       *result;
+
+   result = concat_internal("", 0, fcinfo);
+   if (result == NULL)
+       PG_RETURN_NULL();
+   PG_RETURN_TEXT_P(result);
 }
 
 /*
@@ -3852,16 +3900,18 @@ text_concat(PG_FUNCTION_ARGS)
 Datum
 text_concat_ws(PG_FUNCTION_ARGS)
 {
-   text       *sep;
+   char       *sep;
+   text       *result;
 
    /* return NULL when separator is NULL */
    if (PG_ARGISNULL(0))
        PG_RETURN_NULL();
+   sep = text_to_cstring(PG_GETARG_TEXT_PP(0));
 
-   sep = PG_GETARG_TEXT_PP(0);
-
-   PG_RETURN_TEXT_P(concat_internal(VARDATA_ANY(sep), VARSIZE_ANY_EXHDR(sep),
-                                    1, fcinfo));
+   result = concat_internal(sep, 1, fcinfo);
+   if (result == NULL)
+       PG_RETURN_NULL();
+   PG_RETURN_TEXT_P(result);
 }
 
 /*
@@ -3959,11 +4009,73 @@ text_format(PG_FUNCTION_ARGS)
    const char *end_ptr;
    text       *result;
    int         arg = 0;
+   bool        funcvariadic;
+   int         nargs;
+   Datum      *elements = NULL;
+   bool       *nulls = NULL;
+   Oid         element_type = InvalidOid;
+   Oid         prev_type = InvalidOid;
+   FmgrInfo    typoutputfinfo;
 
    /* When format string is null, returns null */
    if (PG_ARGISNULL(0))
        PG_RETURN_NULL();
 
+   /* If argument is marked VARIADIC, expand array into elements */
+   if (get_fn_expr_variadic(fcinfo->flinfo))
+   {
+       Oid         arr_typid;
+       ArrayType  *arr;
+       int16       elmlen;
+       bool        elmbyval;
+       char        elmalign;
+       int         nitems;
+
+       /* Should have just the one argument */
+       Assert(PG_NARGS() == 2);
+
+       /* If argument is NULL, we treat it as zero-length array */
+       if (PG_ARGISNULL(1))
+           nitems = 0;
+       else
+       {
+           /*
+            * Non-null argument had better be an array.  The parser doesn't
+            * enforce this for VARIADIC ANY functions (maybe it should?), so
+            * that check uses ereport not just elog.
+            */
+           arr_typid = get_fn_expr_argtype(fcinfo->flinfo, 1);
+           if (!OidIsValid(arr_typid))
+               elog(ERROR, "could not determine data type of format() input");
+
+           if (!OidIsValid(get_element_type(arr_typid)))
+               ereport(ERROR,
+                       (errcode(ERRCODE_DATATYPE_MISMATCH),
+                        errmsg("VARIADIC argument must be an array")));
+
+           /* OK, safe to fetch the array value */
+           arr = PG_GETARG_ARRAYTYPE_P(1);
+
+           /* Get info about array element type */
+           element_type = ARR_ELEMTYPE(arr);
+           get_typlenbyvalalign(element_type,
+                                &elmlen, &elmbyval, &elmalign);
+
+           /* Extract all array elements */
+           deconstruct_array(arr, element_type, elmlen, elmbyval, elmalign,
+                             &elements, &nulls, &nitems);
+       }
+
+       nargs = nitems + 1;
+       funcvariadic = true;
+   }
+   else
+   {
+       /* Non-variadic case, we'll process the arguments individually */
+       nargs = PG_NARGS();
+       funcvariadic = false;
+   }
+
    /* Setup for main loop. */
    fmt = PG_GETARG_TEXT_PP(0);
    start_ptr = VARDATA_ANY(fmt);
@@ -4062,26 +4174,54 @@ text_format(PG_FUNCTION_ARGS)
        }
 
        /* Not enough arguments?  Deduct 1 to avoid counting format string. */
-       if (arg > PG_NARGS() - 1)
+       if (arg > nargs - 1)
            ereport(ERROR,
                    (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                     errmsg("too few arguments for format")));
 
+       /* Get the value and type of the selected argument */
+       if (!funcvariadic)
+       {
+           value = PG_GETARG_DATUM(arg);
+           isNull = PG_ARGISNULL(arg);
+           typid = get_fn_expr_argtype(fcinfo->flinfo, arg);
+       }
+       else
+       {
+           value = elements[arg - 1];
+           isNull = nulls[arg - 1];
+           typid = element_type;
+       }
+       if (!OidIsValid(typid))
+           elog(ERROR, "could not determine data type of format() input");
+
+       /*
+        * Get the appropriate typOutput function, reusing previous one if
+        * same type as previous argument.  That's particularly useful in the
+        * variadic-array case, but often saves work even for ordinary calls.
+        */
+       if (typid != prev_type)
+       {
+           Oid         typoutputfunc;
+           bool        typIsVarlena;
+
+           getTypeOutputInfo(typid, &typoutputfunc, &typIsVarlena);
+           fmgr_info(typoutputfunc, &typoutputfinfo);
+           prev_type = typid;
+       }
+
        /*
         * At this point, we should see the main conversion specifier. Whether
         * or not an argument position was present, it's known that at least
         * one character remains in the string at this point.
         */
-       value = PG_GETARG_DATUM(arg);
-       isNull = PG_ARGISNULL(arg);
-       typid = get_fn_expr_argtype(fcinfo->flinfo, arg);
-
        switch (*cp)
        {
            case 's':
            case 'I':
            case 'L':
-               text_format_string_conversion(&str, *cp, typid, value, isNull);
+               text_format_string_conversion(&str, *cp, &typoutputfinfo,
+                                             value, isNull);
                break;
            default:
                ereport(ERROR,
@@ -4091,6 +4231,12 @@ text_format(PG_FUNCTION_ARGS)
        }
    }
 
+   /* Don't need deconstruct_array results anymore. */
+   if (elements != NULL)
+       pfree(elements);
+   if (nulls != NULL)
+       pfree(nulls);
+
    /* Generate results. */
    result = cstring_to_text_with_len(str.data, str.len);
    pfree(str.data);
@@ -4099,12 +4245,11 @@ text_format(PG_FUNCTION_ARGS)
 }
 
 /* Format a %s, %I, or %L conversion. */
-void
+static void
 text_format_string_conversion(StringInfo buf, char conversion,
-                             Oid typid, Datum value, bool isNull)
+                             FmgrInfo *typOutputInfo,
+                             Datum value, bool isNull)
 {
-   Oid         typOutput;
-   bool        typIsVarlena;
    char       *str;
 
    /* Handle NULL arguments before trying to stringify the value. */
@@ -4120,8 +4265,7 @@ text_format_string_conversion(StringInfo buf, char conversion,
    }
 
    /* Stringify. */
-   getTypeOutputInfo(typid, &typOutput, &typIsVarlena);
-   str = OidOutputFunctionCall(typOutput, value);
+   str = OutputFunctionCall(typOutputInfo, value);
 
    /* Escape. */
    if (conversion == 'I')
index e45714f77ecd1dcef0160036ce5f6b30d30c5d59..b7565830d6f165035625b9c856c6a15118d2f731 100644 (file)
@@ -136,6 +136,40 @@ select quote_literal(e'\\');
  E'\\'
 (1 row)
 
+-- check variadic labeled argument
+select concat(variadic array[1,2,3]);
+ concat 
+--------
+ 123
+(1 row)
+
+select concat_ws(',', variadic array[1,2,3]);
+ concat_ws 
+-----------
+ 1,2,3
+(1 row)
+
+select concat_ws(',', variadic NULL);
+ concat_ws 
+-----------
+(1 row)
+
+select concat(variadic NULL) is NULL;
+ ?column? 
+----------
+ t
+(1 row)
+
+select concat(variadic '{}'::int[]) = '';
+ ?column? 
+----------
+ t
+(1 row)
+
+--should fail
+select concat_ws(',', variadic 10);
+ERROR:  VARIADIC argument must be an array
 /*
  * format
  */
@@ -228,7 +262,7 @@ select format('%1$', 1);
 ERROR:  unterminated conversion specifier
 select format('%1$1', 1);
 ERROR:  unrecognized conversion specifier "1"
---checkk mix of positional and ordered placeholders
+-- check mix of positional and ordered placeholders
 select format('Hello %s %1$s %s', 'World', 'Hello again');
             format             
 -------------------------------
@@ -241,3 +275,56 @@ select format('Hello %s %s, %2$s %2$s', 'World', 'Hello again');
  Hello World Hello again, Hello again Hello again
 (1 row)
 
+-- check variadic labeled arguments
+select format('%s, %s', variadic array['Hello','World']);
+    format    
+--------------
+ Hello, World
+(1 row)
+
+select format('%s, %s', variadic array[1, 2]);
+ format 
+--------
+ 1, 2
+(1 row)
+
+select format('%s, %s', variadic array[true, false]);
+ format 
+--------
+ t, f
+(1 row)
+
+select format('%s, %s', variadic array[true, false]::text[]);
+   format    
+-------------
+ true, false
+(1 row)
+
+-- check variadic with positional placeholders
+select format('%2$s, %1$s', variadic array['first', 'second']);
+    format     
+---------------
+ second, first
+(1 row)
+
+select format('%2$s, %1$s', variadic array[1, 2]);
+ format 
+--------
+ 2, 1
+(1 row)
+
+-- variadic argument can be NULL, but should not be referenced
+select format('Hello', variadic NULL);
+ format 
+--------
+ Hello
+(1 row)
+
+-- variadic argument allows simulating more than FUNC_MAX_ARGS parameters
+select format(string_agg('%s',','), variadic array_agg(i))
+from generate_series(1,200) g(i);
+                                                                                                                                                                                                                                                                                                                                                       format                                                                                                                                                                                                                                                                                                                                                        
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200
+(1 row)
+
index 96e425d3cf70509e3188b8f94ecf313da46d55cd..a96e9f7d1e7e4e3817e67f1223f74ed6a3d7142c 100644 (file)
@@ -44,6 +44,14 @@ select i, left('ahoj', i), right('ahoj', i) from generate_series(-5, 5) t(i) ord
 select quote_literal('');
 select quote_literal('abc''');
 select quote_literal(e'\\');
+-- check variadic labeled argument
+select concat(variadic array[1,2,3]);
+select concat_ws(',', variadic array[1,2,3]);
+select concat_ws(',', variadic NULL);
+select concat(variadic NULL) is NULL;
+select concat(variadic '{}'::int[]) = '';
+--should fail
+select concat_ws(',', variadic 10);
 
 /*
  * format
@@ -73,6 +81,19 @@ select format('%1$s %13$s', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
 select format('%1s', 1);
 select format('%1$', 1);
 select format('%1$1', 1);
---checkk mix of positional and ordered placeholders
+-- check mix of positional and ordered placeholders
 select format('Hello %s %1$s %s', 'World', 'Hello again');
 select format('Hello %s %s, %2$s %2$s', 'World', 'Hello again');
+-- check variadic labeled arguments
+select format('%s, %s', variadic array['Hello','World']);
+select format('%s, %s', variadic array[1, 2]);
+select format('%s, %s', variadic array[true, false]);
+select format('%s, %s', variadic array[true, false]::text[]);
+-- check variadic with positional placeholders
+select format('%2$s, %1$s', variadic array['first', 'second']);
+select format('%2$s, %1$s', variadic array[1, 2]);
+-- variadic argument can be NULL, but should not be referenced
+select format('Hello', variadic NULL);
+-- variadic argument allows simulating more than FUNC_MAX_ARGS parameters
+select format(string_agg('%s',','), variadic array_agg(i))
+from generate_series(1,200) g(i);