Add string_to_table() function.
authorTom Lane <tgl@sss.pgh.pa.us>
Wed, 2 Sep 2020 22:23:56 +0000 (18:23 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Wed, 2 Sep 2020 22:23:56 +0000 (18:23 -0400)
This splits a string at occurrences of a delimiter.  It is exactly like
string_to_array() except for producing a set of values instead of an
array of values.  Thus, the relationship of these two functions is
the same as between regexp_split_to_table() and regexp_split_to_array().

Although the same results could be had from unnest(string_to_array()),
this is somewhat faster than that, and anyway it seems reasonable to
have it for symmetry with the regexp functions.

Pavel Stehule, reviewed by Peter Smith

Discussion: https://postgr.es/m/CAFj8pRD8HOpjq2TqeTBhSo_QkzjLOhXzGCpKJ4nCs7Y9SQkuPw@mail.gmail.com

doc/src/sgml/func.sgml
src/backend/utils/adt/varlena.c
src/include/catalog/catversion.h
src/include/catalog/pg_proc.dat
src/test/regress/expected/arrays.out
src/test/regress/sql/arrays.sql

index 2efd80baa45055e7dd2476faf832b9c1a628b0c0..e2e618791ee01d80da31cfb77a21fb60d167080f 100644 (file)
@@ -3220,7 +3220,7 @@ repeat('Pg', 4) <returnvalue>PgPgPgPg</returnvalue>
        </para>
        <para>
         Splits <parameter>string</parameter> using a POSIX regular
-        expression as the delimiter; see
+        expression as the delimiter, producing an array of results; see
         <xref linkend="functions-posix-regexp"/>.
        </para>
        <para>
@@ -3239,7 +3239,7 @@ repeat('Pg', 4) <returnvalue>PgPgPgPg</returnvalue>
        </para>
        <para>
         Splits <parameter>string</parameter> using a POSIX regular
-        expression as the delimiter; see
+        expression as the delimiter, producing a set of results; see
         <xref linkend="functions-posix-regexp"/>.
        </para>
        <para>
@@ -3460,6 +3460,65 @@ repeat('Pg', 4) <returnvalue>PgPgPgPg</returnvalue>
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>string_to_array</primary>
+        </indexterm>
+        <function>string_to_array</function> ( <parameter>string</parameter> <type>text</type>, <parameter>delimiter</parameter> <type>text</type> <optional>, <parameter>null_string</parameter> <type>text</type> </optional> )
+        <returnvalue>text[]</returnvalue>
+       </para>
+       <para>
+        Splits the <parameter>string</parameter> at occurrences
+        of <parameter>delimiter</parameter> and forms the resulting fields
+        into a <type>text</type> array.
+        If <parameter>delimiter</parameter> is <literal>NULL</literal>,
+        each character in the <parameter>string</parameter> will become a
+        separate element in the array.
+        If <parameter>delimiter</parameter> is an empty string, then
+        the <parameter>string</parameter> is treated as a single field.
+        If <parameter>null_string</parameter> is supplied and is
+        not <literal>NULL</literal>, fields matching that string are
+        replaced by <literal>NULL</literal>.
+       </para>
+       <para>
+        <literal>string_to_array('xx~~yy~~zz', '~~', 'yy')</literal>
+        <returnvalue>{xx,NULL,zz}</returnvalue>
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>string_to_table</primary>
+        </indexterm>
+        <function>string_to_table</function> ( <parameter>string</parameter> <type>text</type>, <parameter>delimiter</parameter> <type>text</type> <optional>, <parameter>null_string</parameter> <type>text</type> </optional> )
+        <returnvalue>setof text</returnvalue>
+       </para>
+       <para>
+        Splits the <parameter>string</parameter> at occurrences
+        of <parameter>delimiter</parameter> and returns the resulting fields
+        as a set of <type>text</type> rows.
+        If <parameter>delimiter</parameter> is <literal>NULL</literal>,
+        each character in the <parameter>string</parameter> will become a
+        separate row of the result.
+        If <parameter>delimiter</parameter> is an empty string, then
+        the <parameter>string</parameter> is treated as a single field.
+        If <parameter>null_string</parameter> is supplied and is
+        not <literal>NULL</literal>, fields matching that string are
+        replaced by <literal>NULL</literal>.
+       </para>
+       <para>
+        <literal>string_to_table('xx~^~yy~^~zz', '~^~', 'yy')</literal>
+        <returnvalue></returnvalue>
+<programlisting>
+ xx
+ NULL
+ zz
+</programlisting>
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
@@ -17819,33 +17878,6 @@ SELECT NULLIF(value, '(none)') ...
        </para></entry>
       </row>
 
-      <row>
-       <entry role="func_table_entry"><para role="func_signature">
-        <indexterm>
-         <primary>string_to_array</primary>
-        </indexterm>
-        <function>string_to_array</function> ( <parameter>string</parameter> <type>text</type>, <parameter>delimiter</parameter> <type>text</type> <optional>, <parameter>null_string</parameter> <type>text</type> </optional> )
-        <returnvalue>text[]</returnvalue>
-       </para>
-       <para>
-        Splits the <parameter>string</parameter> at occurrences
-        of <parameter>delimiter</parameter> and forms the remaining data
-        into a <type>text</type> array.
-        If <parameter>delimiter</parameter> is <literal>NULL</literal>,
-        each character in the <parameter>string</parameter> will become a
-        separate element in the array.
-        If <parameter>delimiter</parameter> is an empty string, then
-        the <parameter>string</parameter> is treated as a single field.
-        If <parameter>null_string</parameter> is supplied and is
-        not <literal>NULL</literal>, fields matching that string are converted
-        to <literal>NULL</literal> entries.
-       </para>
-       <para>
-        <literal>string_to_array('xx~~yy~~zz', '~~', 'yy')</literal>
-        <returnvalue>{xx,NULL,zz}</returnvalue>
-       </para></entry>
-      </row>
-
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
index df10bfb906ed8c07ee326540c3ad572d153b2523..d7bc330541743d2490eb4b063271f86e45973b92 100644 (file)
@@ -26,6 +26,7 @@
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "miscadmin.h"
+#include "nodes/execnodes.h"
 #include "parser/scansup.h"
 #include "port/pg_bswap.h"
 #include "regex/regex.h"
@@ -92,6 +93,17 @@ typedef struct
        pg_locale_t locale;
 } VarStringSortSupport;
 
+/*
+ * Output data for split_text(): we output either to an array or a table.
+ * tupstore and tupdesc must be set up in advance to output to a table.
+ */
+typedef struct
+{
+       ArrayBuildState *astate;
+       Tuplestorestate *tupstore;
+       TupleDesc       tupdesc;
+} SplitTextOutputData;
+
 /*
  * This should be large enough that most strings will fit, but small enough
  * that we feel comfortable putting it on the stack
@@ -139,7 +151,11 @@ static bytea *bytea_substring(Datum str,
                                                          bool length_not_specified);
 static bytea *bytea_overlay(bytea *t1, bytea *t2, int sp, int sl);
 static void appendStringInfoText(StringInfo str, const text *t);
-static Datum text_to_array_internal(PG_FUNCTION_ARGS);
+static bool split_text(FunctionCallInfo fcinfo, SplitTextOutputData *tstate);
+static void split_text_accum_result(SplitTextOutputData *tstate,
+                                                                       text *field_value,
+                                                                       text *null_string,
+                                                                       Oid collation);
 static text *array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v,
                                                                        const char *fldsep, const char *null_string);
 static StringInfo makeStringAggState(FunctionCallInfo fcinfo);
@@ -4564,13 +4580,13 @@ replace_text_regexp(text *src_text, void *regexp,
 }
 
 /*
- * split_text
+ * split_part
  * parse input string
  * return ord item (1 based)
  * based on provided field separator
  */
 Datum
-split_text(PG_FUNCTION_ARGS)
+split_part(PG_FUNCTION_ARGS)
 {
        text       *inputstring = PG_GETARG_TEXT_PP(0);
        text       *fldsep = PG_GETARG_TEXT_PP(1);
@@ -4599,7 +4615,6 @@ split_text(PG_FUNCTION_ARGS)
        /* empty field separator */
        if (fldsep_len < 1)
        {
-               text_position_cleanup(&state);
                /* if first field, return input string, else empty string */
                if (fldnum == 1)
                        PG_RETURN_TEXT_P(inputstring);
@@ -4679,7 +4694,19 @@ text_isequal(text *txt1, text *txt2, Oid collid)
 Datum
 text_to_array(PG_FUNCTION_ARGS)
 {
-       return text_to_array_internal(fcinfo);
+       SplitTextOutputData tstate;
+
+       /* For array output, tstate should start as all zeroes */
+       memset(&tstate, 0, sizeof(tstate));
+
+       if (!split_text(fcinfo, &tstate))
+               PG_RETURN_NULL();
+
+       if (tstate.astate == NULL)
+               PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
+
+       PG_RETURN_ARRAYTYPE_P(makeArrayResult(tstate.astate,
+                                                                                 CurrentMemoryContext));
 }
 
 /*
@@ -4693,30 +4720,90 @@ text_to_array(PG_FUNCTION_ARGS)
 Datum
 text_to_array_null(PG_FUNCTION_ARGS)
 {
-       return text_to_array_internal(fcinfo);
+       return text_to_array(fcinfo);
+}
+
+/*
+ * text_to_table
+ * parse input string and return table of elements,
+ * based on provided field separator
+ */
+Datum
+text_to_table(PG_FUNCTION_ARGS)
+{
+       ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+       SplitTextOutputData tstate;
+       MemoryContext old_cxt;
+
+       /* check to see if caller supports us returning a tuplestore */
+       if (rsi == NULL || !IsA(rsi, ReturnSetInfo))
+               ereport(ERROR,
+                               (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                errmsg("set-valued function called in context that cannot accept a set")));
+       if (!(rsi->allowedModes & SFRM_Materialize))
+               ereport(ERROR,
+                               (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                errmsg("materialize mode required, but it is not allowed in this context")));
+
+       /* OK, prepare tuplestore in per-query memory */
+       old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory);
+
+       tstate.astate = NULL;
+       tstate.tupdesc = CreateTupleDescCopy(rsi->expectedDesc);
+       tstate.tupstore = tuplestore_begin_heap(true, false, work_mem);
+
+       MemoryContextSwitchTo(old_cxt);
+
+       (void) split_text(fcinfo, &tstate);
+
+       tuplestore_donestoring(tstate.tupstore);
+
+       rsi->returnMode = SFRM_Materialize;
+       rsi->setResult = tstate.tupstore;
+       rsi->setDesc = tstate.tupdesc;
+
+       return (Datum) 0;
+}
+
+/*
+ * text_to_table_null
+ * parse input string and return table of elements,
+ * based on provided field separator and null string
+ *
+ * This is a separate entry point only to prevent the regression tests from
+ * complaining about different argument sets for the same internal function.
+ */
+Datum
+text_to_table_null(PG_FUNCTION_ARGS)
+{
+       return text_to_table(fcinfo);
 }
 
 /*
- * common code for text_to_array and text_to_array_null functions
+ * Common code for text_to_array, text_to_array_null, text_to_table
+ * and text_to_table_null functions.
  *
  * These are not strict so we have to test for null inputs explicitly.
+ * Returns false if result is to be null, else returns true.
+ *
+ * Note that if the result is valid but empty (zero elements), we return
+ * without changing *tstate --- caller must handle that case, too.
  */
-static Datum
-text_to_array_internal(PG_FUNCTION_ARGS)
+static bool
+split_text(FunctionCallInfo fcinfo, SplitTextOutputData *tstate)
 {
        text       *inputstring;
        text       *fldsep;
        text       *null_string;
+       Oid                     collation = PG_GET_COLLATION();
        int                     inputstring_len;
        int                     fldsep_len;
        char       *start_ptr;
        text       *result_text;
-       bool            is_null;
-       ArrayBuildState *astate = NULL;
 
        /* when input string is NULL, then result is NULL too */
        if (PG_ARGISNULL(0))
-               PG_RETURN_NULL();
+               return false;
 
        inputstring = PG_GETARG_TEXT_PP(0);
 
@@ -4743,35 +4830,19 @@ text_to_array_internal(PG_FUNCTION_ARGS)
                inputstring_len = VARSIZE_ANY_EXHDR(inputstring);
                fldsep_len = VARSIZE_ANY_EXHDR(fldsep);
 
-               /* return empty array for empty input string */
+               /* return empty set for empty input string */
                if (inputstring_len < 1)
-                       PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
+                       return true;
 
-               /*
-                * empty field separator: return the input string as a one-element
-                * array
-                */
+               /* empty field separator: return input string as a one-element set */
                if (fldsep_len < 1)
                {
-                       Datum           elems[1];
-                       bool            nulls[1];
-                       int                     dims[1];
-                       int                     lbs[1];
-
-                       /* single element can be a NULL too */
-                       is_null = null_string ? text_isequal(inputstring, null_string, PG_GET_COLLATION()) : false;
-
-                       elems[0] = PointerGetDatum(inputstring);
-                       nulls[0] = is_null;
-                       dims[0] = 1;
-                       lbs[0] = 1;
-                       /* XXX: this hardcodes assumptions about the text type */
-                       PG_RETURN_ARRAYTYPE_P(construct_md_array(elems, nulls,
-                                                                                                        1, dims, lbs,
-                                                                                                        TEXTOID, -1, false, TYPALIGN_INT));
+                       split_text_accum_result(tstate, inputstring,
+                                                                       null_string, collation);
+                       return true;
                }
 
-               text_position_setup(inputstring, fldsep, PG_GET_COLLATION(), &state);
+               text_position_setup(inputstring, fldsep, collation, &state);
 
                start_ptr = VARDATA_ANY(inputstring);
 
@@ -4797,16 +4868,12 @@ text_to_array_internal(PG_FUNCTION_ARGS)
                                chunk_len = end_ptr - start_ptr;
                        }
 
-                       /* must build a temp text datum to pass to accumArrayResult */
+                       /* build a temp text datum to pass to split_text_accum_result */
                        result_text = cstring_to_text_with_len(start_ptr, chunk_len);
-                       is_null = null_string ? text_isequal(result_text, null_string, PG_GET_COLLATION()) : false;
 
                        /* stash away this field */
-                       astate = accumArrayResult(astate,
-                                                                         PointerGetDatum(result_text),
-                                                                         is_null,
-                                                                         TEXTOID,
-                                                                         CurrentMemoryContext);
+                       split_text_accum_result(tstate, result_text,
+                                                                       null_string, collation);
 
                        pfree(result_text);
 
@@ -4821,16 +4888,12 @@ text_to_array_internal(PG_FUNCTION_ARGS)
        else
        {
                /*
-                * When fldsep is NULL, each character in the inputstring becomes an
-                * element in the result array.  The separator is effectively the
-                * space between characters.
+                * When fldsep is NULL, each character in the input string becomes a
+                * separate element in the result set.  The separator is effectively
+                * the space between characters.
                 */
                inputstring_len = VARSIZE_ANY_EXHDR(inputstring);
 
-               /* return empty array for empty input string */
-               if (inputstring_len < 1)
-                       PG_RETURN_ARRAYTYPE_P(construct_empty_array(TEXTOID));
-
                start_ptr = VARDATA_ANY(inputstring);
 
                while (inputstring_len > 0)
@@ -4839,16 +4902,12 @@ text_to_array_internal(PG_FUNCTION_ARGS)
 
                        CHECK_FOR_INTERRUPTS();
 
-                       /* must build a temp text datum to pass to accumArrayResult */
+                       /* build a temp text datum to pass to split_text_accum_result */
                        result_text = cstring_to_text_with_len(start_ptr, chunk_len);
-                       is_null = null_string ? text_isequal(result_text, null_string, PG_GET_COLLATION()) : false;
 
                        /* stash away this field */
-                       astate = accumArrayResult(astate,
-                                                                         PointerGetDatum(result_text),
-                                                                         is_null,
-                                                                         TEXTOID,
-                                                                         CurrentMemoryContext);
+                       split_text_accum_result(tstate, result_text,
+                                                                       null_string, collation);
 
                        pfree(result_text);
 
@@ -4857,8 +4916,47 @@ text_to_array_internal(PG_FUNCTION_ARGS)
                }
        }
 
-       PG_RETURN_ARRAYTYPE_P(makeArrayResult(astate,
-                                                                                 CurrentMemoryContext));
+       return true;
+}
+
+/*
+ * Add text item to result set (table or array).
+ *
+ * This is also responsible for checking to see if the item matches
+ * the null_string, in which case we should emit NULL instead.
+ */
+static void
+split_text_accum_result(SplitTextOutputData *tstate,
+                                               text *field_value,
+                                               text *null_string,
+                                               Oid collation)
+{
+       bool            is_null = false;
+
+       if (null_string && text_isequal(field_value, null_string, collation))
+               is_null = true;
+
+       if (tstate->tupstore)
+       {
+               Datum           values[1];
+               bool            nulls[1];
+
+               values[0] = PointerGetDatum(field_value);
+               nulls[0] = is_null;
+
+               tuplestore_putvalues(tstate->tupstore,
+                                                        tstate->tupdesc,
+                                                        values,
+                                                        nulls);
+       }
+       else
+       {
+               tstate->astate = accumArrayResult(tstate->astate,
+                                                                                 PointerGetDatum(field_value),
+                                                                                 is_null,
+                                                                                 TEXTOID,
+                                                                                 CurrentMemoryContext);
+       }
 }
 
 /*
index 52ca61f8a8e83514946b46cc41484f3980886393..c807f83baddd211c6cda5309801cc0556b2bd543 100644 (file)
@@ -53,6 +53,6 @@
  */
 
 /*                                                     yyyymmddN */
-#define CATALOG_VERSION_NO     202008301
+#define CATALOG_VERSION_NO     202009021
 
 #endif
index 1dd325e0e6fdce4df46aa4d0b4dc8bfe17b37e32..687509ba9265b04f0315602c902db8d6585a0841 100644 (file)
 { oid => '383',
   proname => 'array_cat', proisstrict => 'f', prorettype => 'anyarray',
   proargtypes => 'anyarray anyarray', prosrc => 'array_cat' },
-{ oid => '394', descr => 'split delimited text into text[]',
+{ oid => '394', descr => 'split delimited text',
   proname => 'string_to_array', proisstrict => 'f', prorettype => '_text',
   proargtypes => 'text text', prosrc => 'text_to_array' },
+{ oid => '376', descr => 'split delimited text, with null string',
+  proname => 'string_to_array', proisstrict => 'f', prorettype => '_text',
+  proargtypes => 'text text text', prosrc => 'text_to_array_null' },
+{ oid => '8432', descr => 'split delimited text',
+  proname => 'string_to_table', proisstrict => 'f', prorows => '1000',
+  proretset => 't', prorettype => 'text', proargtypes => 'text text',
+  prosrc => 'text_to_table' },
+{ oid => '8433', descr => 'split delimited text, with null string',
+  proname => 'string_to_table', proisstrict => 'f', prorows => '1000',
+  proretset => 't', prorettype => 'text', proargtypes => 'text text text',
+  prosrc => 'text_to_table_null' },
 { oid => '395',
   descr => 'concatenate array elements, using delimiter, into text',
   proname => 'array_to_string', provolatile => 's', prorettype => 'text',
   proargtypes => 'anyarray text', prosrc => 'array_to_text' },
-{ oid => '376', descr => 'split delimited text into text[], with null string',
-  proname => 'string_to_array', proisstrict => 'f', prorettype => '_text',
-  proargtypes => 'text text text', prosrc => 'text_to_array_null' },
 { oid => '384',
   descr => 'concatenate array elements, using delimiter and null string, into text',
   proname => 'array_to_string', proisstrict => 'f', provolatile => 's',
   prosrc => 'regexp_matches' },
 { oid => '2088', descr => 'split string by field_sep and return field_num',
   proname => 'split_part', prorettype => 'text',
-  proargtypes => 'text text int4', prosrc => 'split_text' },
+  proargtypes => 'text text int4', prosrc => 'split_part' },
 { oid => '2765', descr => 'split string by pattern',
   proname => 'regexp_split_to_table', prorows => '1000', proretset => 't',
   prorettype => 'text', proargtypes => 'text text',
index c730563f0386ca593c0f894a9f596de86ab833e2..f9d9ad6aef904b113de76eeb9f506edfbb8e6800 100644 (file)
@@ -1755,6 +1755,114 @@ select string_to_array('1,2,3,4,*,6', ',', '*');
  {1,2,3,4,NULL,6}
 (1 row)
 
+select v, v is null as "is null" from string_to_table('1|2|3', '|') g(v);
+ v | is null 
+---+---------
+ 1 | f
+ 2 | f
+ 3 | f
+(3 rows)
+
+select v, v is null as "is null" from string_to_table('1|2|3|', '|') g(v);
+ v | is null 
+---+---------
+ 1 | f
+ 2 | f
+ 3 | f
+   | f
+(4 rows)
+
+select v, v is null as "is null" from string_to_table('1||2|3||', '||') g(v);
+  v  | is null 
+-----+---------
+ 1   | f
+ 2|3 | f
+     | f
+(3 rows)
+
+select v, v is null as "is null" from string_to_table('1|2|3', '') g(v);
+   v   | is null 
+-------+---------
+ 1|2|3 | f
+(1 row)
+
+select v, v is null as "is null" from string_to_table('', '|') g(v);
+ v | is null 
+---+---------
+(0 rows)
+
+select v, v is null as "is null" from string_to_table('1|2|3', NULL) g(v);
+ v | is null 
+---+---------
+ 1 | f
+ | | f
+ 2 | f
+ | | f
+ 3 | f
+(5 rows)
+
+select v, v is null as "is null" from string_to_table(NULL, '|') g(v);
+ v | is null 
+---+---------
+(0 rows)
+
+select v, v is null as "is null" from string_to_table('abc', '') g(v);
+  v  | is null 
+-----+---------
+ abc | f
+(1 row)
+
+select v, v is null as "is null" from string_to_table('abc', '', 'abc') g(v);
+ v | is null 
+---+---------
+   | t
+(1 row)
+
+select v, v is null as "is null" from string_to_table('abc', ',') g(v);
+  v  | is null 
+-----+---------
+ abc | f
+(1 row)
+
+select v, v is null as "is null" from string_to_table('abc', ',', 'abc') g(v);
+ v | is null 
+---+---------
+   | t
+(1 row)
+
+select v, v is null as "is null" from string_to_table('1,2,3,4,,6', ',') g(v);
+ v | is null 
+---+---------
+ 1 | f
+ 2 | f
+ 3 | f
+ 4 | f
+   | f
+ 6 | f
+(6 rows)
+
+select v, v is null as "is null" from string_to_table('1,2,3,4,,6', ',', '') g(v);
+ v | is null 
+---+---------
+ 1 | f
+ 2 | f
+ 3 | f
+ 4 | f
+   | t
+ 6 | f
+(6 rows)
+
+select v, v is null as "is null" from string_to_table('1,2,3,4,*,6', ',', '*') g(v);
+ v | is null 
+---+---------
+ 1 | f
+ 2 | f
+ 3 | f
+ 4 | f
+   | t
+ 6 | f
+(6 rows)
+
 select array_to_string(NULL::int4[], ',') IS NULL;
  ?column? 
 ----------
index 25dd4e2c6dedd0ea89ad1ecdaf9fd3e9184bcd4d..2b689ae88f5988e9f9b87c41c6ce28db1158e4c0 100644 (file)
@@ -544,6 +544,21 @@ select string_to_array('1,2,3,4,,6', ',');
 select string_to_array('1,2,3,4,,6', ',', '');
 select string_to_array('1,2,3,4,*,6', ',', '*');
 
+select v, v is null as "is null" from string_to_table('1|2|3', '|') g(v);
+select v, v is null as "is null" from string_to_table('1|2|3|', '|') g(v);
+select v, v is null as "is null" from string_to_table('1||2|3||', '||') g(v);
+select v, v is null as "is null" from string_to_table('1|2|3', '') g(v);
+select v, v is null as "is null" from string_to_table('', '|') g(v);
+select v, v is null as "is null" from string_to_table('1|2|3', NULL) g(v);
+select v, v is null as "is null" from string_to_table(NULL, '|') g(v);
+select v, v is null as "is null" from string_to_table('abc', '') g(v);
+select v, v is null as "is null" from string_to_table('abc', '', 'abc') g(v);
+select v, v is null as "is null" from string_to_table('abc', ',') g(v);
+select v, v is null as "is null" from string_to_table('abc', ',', 'abc') g(v);
+select v, v is null as "is null" from string_to_table('1,2,3,4,,6', ',') g(v);
+select v, v is null as "is null" from string_to_table('1,2,3,4,,6', ',', '') g(v);
+select v, v is null as "is null" from string_to_table('1,2,3,4,*,6', ',', '*') g(v);
+
 select array_to_string(NULL::int4[], ',') IS NULL;
 select array_to_string('{}'::int4[], ',');
 select array_to_string(array[1,2,3,4,NULL,6], ',');