Allow functions that return sets of tuples to return simple NULLs.
authorTom Lane <tgl@sss.pgh.pa.us>
Wed, 27 Jul 2016 01:33:49 +0000 (21:33 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Wed, 27 Jul 2016 01:34:02 +0000 (21:34 -0400)
ExecMakeTableFunctionResult(), which is used in SELECT FROM function(...)
cases, formerly treated a simple NULL output from a function that both
returnsSet and returnsTuple as a violation of the SRF protocol.  What seems
better is to treat a NULL output as equivalent to ROW(NULL,NULL,...).
Without this, cases such as SELECT FROM unnest(...) on an array of
composite are vulnerable to unexpected and not-very-helpful failures.
Old code comments here suggested an alternative of just ignoring
simple-NULL outputs, but that doesn't seem very principled.

This change had been hung up for a long time due to uncertainty about
how much we wanted to buy into the equivalence of simple NULL and
ROW(NULL,NULL,...).  I think that's been mostly resolved by the discussion
around bug #14235, so let's go ahead and do it.

Per bug #7808 from Joe Van Dyk.  Although this is a pretty old report,
fixing it smells a bit more like a new feature than a bug fix, and the
lack of other similar complaints suggests that we shouldn't take much risk
of destabilization by back-patching.  (Maybe that could be revisited once
this patch has withstood some field usage.)

Andrew Gierth and Tom Lane

Report: <E1TurJE-0006Es-TK@wrigleys.postgresql.org>

src/backend/executor/execQual.c
src/test/regress/expected/rangefuncs.out
src/test/regress/sql/rangefuncs.sql

index 2b9102125ef5302e9ad5e4341379dbbcece59152..69bf65d00bf97a71d7a6ab4781ccb117cd04cb32 100644 (file)
@@ -2229,45 +2229,16 @@ ExecMakeTableFunctionResult(ExprState *funcexpr,
                                break;
 
                        /*
-                        * Can't do anything very useful with NULL rowtype values. For a
-                        * function returning set, we consider this a protocol violation
-                        * (but another alternative would be to just ignore the result and
-                        * "continue" to get another row).  For a function not returning
-                        * set, we fall out of the loop; we'll cons up an all-nulls result
-                        * row below.
-                        */
-                       if (returnsTuple && fcinfo.isnull)
-                       {
-                               if (!returnsSet)
-                                       break;
-                               ereport(ERROR,
-                                               (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-                                                errmsg("function returning set of rows cannot return null value")));
-                       }
-
-                       /*
-                        * If first time through, build tupdesc and tuplestore for result
+                        * If first time through, build tuplestore for result.  For a
+                        * scalar function result type, also make a suitable tupdesc.
                         */
                        if (first_time)
                        {
                                oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
-                               if (returnsTuple)
-                               {
-                                       /*
-                                        * Use the type info embedded in the rowtype Datum to look
-                                        * up the needed tupdesc.  Make a copy for the query.
-                                        */
-                                       HeapTupleHeader td;
-
-                                       td = DatumGetHeapTupleHeader(result);
-                                       tupdesc = lookup_rowtype_tupdesc_copy(HeapTupleHeaderGetTypeId(td),
-                                                                                          HeapTupleHeaderGetTypMod(td));
-                               }
-                               else
+                               tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
+                               rsinfo.setResult = tupstore;
+                               if (!returnsTuple)
                                {
-                                       /*
-                                        * Scalar type, so make a single-column descriptor
-                                        */
                                        tupdesc = CreateTemplateTupleDesc(1, false);
                                        TupleDescInitEntry(tupdesc,
                                                                           (AttrNumber) 1,
@@ -2275,11 +2246,9 @@ ExecMakeTableFunctionResult(ExprState *funcexpr,
                                                                           funcrettype,
                                                                           -1,
                                                                           0);
+                                       rsinfo.setDesc = tupdesc;
                                }
-                               tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
                                MemoryContextSwitchTo(oldcontext);
-                               rsinfo.setResult = tupstore;
-                               rsinfo.setDesc = tupdesc;
                        }
 
                        /*
@@ -2287,31 +2256,69 @@ ExecMakeTableFunctionResult(ExprState *funcexpr,
                         */
                        if (returnsTuple)
                        {
-                               HeapTupleHeader td;
+                               if (!fcinfo.isnull)
+                               {
+                                       HeapTupleHeader td = DatumGetHeapTupleHeader(result);
 
-                               td = DatumGetHeapTupleHeader(result);
+                                       if (tupdesc == NULL)
+                                       {
+                                               /*
+                                                * This is the first non-NULL result from the
+                                                * function.  Use the type info embedded in the
+                                                * rowtype Datum to look up the needed tupdesc.  Make
+                                                * a copy for the query.
+                                                */
+                                               oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
+                                               tupdesc = lookup_rowtype_tupdesc_copy(HeapTupleHeaderGetTypeId(td),
+                                                                                          HeapTupleHeaderGetTypMod(td));
+                                               rsinfo.setDesc = tupdesc;
+                                               MemoryContextSwitchTo(oldcontext);
+                                       }
+                                       else
+                                       {
+                                               /*
+                                                * Verify all later returned rows have same subtype;
+                                                * necessary in case the type is RECORD.
+                                                */
+                                               if (HeapTupleHeaderGetTypeId(td) != tupdesc->tdtypeid ||
+                                                       HeapTupleHeaderGetTypMod(td) != tupdesc->tdtypmod)
+                                                       ereport(ERROR,
+                                                                       (errcode(ERRCODE_DATATYPE_MISMATCH),
+                                                                        errmsg("rows returned by function are not all of the same row type")));
+                                       }
 
-                               /*
-                                * Verify all returned rows have same subtype; necessary in
-                                * case the type is RECORD.
-                                */
-                               if (HeapTupleHeaderGetTypeId(td) != tupdesc->tdtypeid ||
-                                       HeapTupleHeaderGetTypMod(td) != tupdesc->tdtypmod)
-                                       ereport(ERROR,
-                                                       (errcode(ERRCODE_DATATYPE_MISMATCH),
-                                                        errmsg("rows returned by function are not all of the same row type")));
+                                       /*
+                                        * tuplestore_puttuple needs a HeapTuple not a bare
+                                        * HeapTupleHeader, but it doesn't need all the fields.
+                                        */
+                                       tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
+                                       tmptup.t_data = td;
 
-                               /*
-                                * tuplestore_puttuple needs a HeapTuple not a bare
-                                * HeapTupleHeader, but it doesn't need all the fields.
-                                */
-                               tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
-                               tmptup.t_data = td;
+                                       tuplestore_puttuple(tupstore, &tmptup);
+                               }
+                               else
+                               {
+                                       /*
+                                        * NULL result from a tuple-returning function; expand it
+                                        * to a row of all nulls.  We rely on the expectedDesc to
+                                        * form such rows.  (Note: this would be problematic if
+                                        * tuplestore_putvalues saved the tdtypeid/tdtypmod from
+                                        * the provided descriptor, since that might not match
+                                        * what we get from the function itself.  But it doesn't.)
+                                        */
+                                       int                     natts = expectedDesc->natts;
+                                       bool       *nullflags;
 
-                               tuplestore_puttuple(tupstore, &tmptup);
+                                       nullflags = (bool *) palloc(natts * sizeof(bool));
+                                       memset(nullflags, true, natts * sizeof(bool));
+                                       tuplestore_putvalues(tupstore, expectedDesc, NULL, nullflags);
+                               }
                        }
                        else
+                       {
+                               /* Scalar-type case: just store the function result */
                                tuplestore_putvalues(tupstore, tupdesc, &result, &fcinfo.isnull);
+                       }
 
                        /*
                         * Are we done?
@@ -2343,7 +2350,8 @@ no_function_result:
        /*
         * If we got nothing from the function (ie, an empty-set or NULL result),
         * we have to create the tuplestore to return, and if it's a
-        * non-set-returning function then insert a single all-nulls row.
+        * non-set-returning function then insert a single all-nulls row.  As
+        * above, we depend on the expectedDesc to manufacture the dummy row.
         */
        if (rsinfo.setResult == NULL)
        {
@@ -2353,15 +2361,12 @@ no_function_result:
                if (!returnsSet)
                {
                        int                     natts = expectedDesc->natts;
-                       Datum      *nulldatums;
                        bool       *nullflags;
 
                        MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
-                       nulldatums = (Datum *) palloc0(natts * sizeof(Datum));
                        nullflags = (bool *) palloc(natts * sizeof(bool));
                        memset(nullflags, true, natts * sizeof(bool));
-                       MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
-                       tuplestore_putvalues(tupstore, expectedDesc, nulldatums, nullflags);
+                       tuplestore_putvalues(tupstore, expectedDesc, NULL, nullflags);
                }
        }
 
index 00ef4210549879668afa69344e8b51bccbfb3afb..f06cfa4b21dc470a3ae1dc86857e4167bc3f736f 100644 (file)
@@ -2076,3 +2076,33 @@ select x from int8_tbl, extractq2_2_opt(int8_tbl) f(x);
  -4567890123456789
 (5 rows)
 
+-- check handling of nulls in SRF results (bug #7808)
+create type foo2 as (a integer, b text);
+select *, row_to_json(u) from unnest(array[(1,'foo')::foo2, null::foo2]) u;
+ a |  b  |     row_to_json     
+---+-----+---------------------
+ 1 | foo | {"a":1,"b":"foo"}
+   |     | {"a":null,"b":null}
+(2 rows)
+
+select *, row_to_json(u) from unnest(array[null::foo2, null::foo2]) u;
+ a | b |     row_to_json     
+---+---+---------------------
+   |   | {"a":null,"b":null}
+   |   | {"a":null,"b":null}
+(2 rows)
+
+select *, row_to_json(u) from unnest(array[null::foo2, (1,'foo')::foo2, null::foo2]) u;
+ a |  b  |     row_to_json     
+---+-----+---------------------
+   |     | {"a":null,"b":null}
+ 1 | foo | {"a":1,"b":"foo"}
+   |     | {"a":null,"b":null}
+(3 rows)
+
+select *, row_to_json(u) from unnest(array[]::foo2[]) u;
+ a | b | row_to_json 
+---+---+-------------
+(0 rows)
+
+drop type foo2;
index 9484023f97b79531c11faba7e3dc730b58eb6f75..c8edc553bc39c7b64d6521b86c4ce038348b776b 100644 (file)
@@ -639,3 +639,14 @@ explain (verbose, costs off)
 select x from int8_tbl, extractq2_2_opt(int8_tbl) f(x);
 
 select x from int8_tbl, extractq2_2_opt(int8_tbl) f(x);
+
+-- check handling of nulls in SRF results (bug #7808)
+
+create type foo2 as (a integer, b text);
+
+select *, row_to_json(u) from unnest(array[(1,'foo')::foo2, null::foo2]) u;
+select *, row_to_json(u) from unnest(array[null::foo2, null::foo2]) u;
+select *, row_to_json(u) from unnest(array[null::foo2, (1,'foo')::foo2, null::foo2]) u;
+select *, row_to_json(u) from unnest(array[]::foo2[]) u;
+
+drop type foo2;