Allow functions that return sets of tuples to return simple NULLs.
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>
This commit is contained in:
parent
976b24fb47
commit
d8411a6c8b
@ -2229,45 +2229,16 @@ ExecMakeTableFunctionResult(ExprState *funcexpr,
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Can't do anything very useful with NULL rowtype values. For a
|
* If first time through, build tuplestore for result. For a
|
||||||
* function returning set, we consider this a protocol violation
|
* scalar function result type, also make a suitable tupdesc.
|
||||||
* (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)
|
if (first_time)
|
||||||
{
|
{
|
||||||
oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
|
oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
|
||||||
if (returnsTuple)
|
tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
|
||||||
|
rsinfo.setResult = tupstore;
|
||||||
|
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
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* Scalar type, so make a single-column descriptor
|
|
||||||
*/
|
|
||||||
tupdesc = CreateTemplateTupleDesc(1, false);
|
tupdesc = CreateTemplateTupleDesc(1, false);
|
||||||
TupleDescInitEntry(tupdesc,
|
TupleDescInitEntry(tupdesc,
|
||||||
(AttrNumber) 1,
|
(AttrNumber) 1,
|
||||||
@ -2275,11 +2246,9 @@ ExecMakeTableFunctionResult(ExprState *funcexpr,
|
|||||||
funcrettype,
|
funcrettype,
|
||||||
-1,
|
-1,
|
||||||
0);
|
0);
|
||||||
|
rsinfo.setDesc = tupdesc;
|
||||||
}
|
}
|
||||||
tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
|
|
||||||
MemoryContextSwitchTo(oldcontext);
|
MemoryContextSwitchTo(oldcontext);
|
||||||
rsinfo.setResult = tupstore;
|
|
||||||
rsinfo.setDesc = tupdesc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -2287,31 +2256,69 @@ ExecMakeTableFunctionResult(ExprState *funcexpr,
|
|||||||
*/
|
*/
|
||||||
if (returnsTuple)
|
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
|
* tuplestore_puttuple needs a HeapTuple not a bare
|
||||||
* case the type is RECORD.
|
* HeapTupleHeader, but it doesn't need all the fields.
|
||||||
*/
|
*/
|
||||||
if (HeapTupleHeaderGetTypeId(td) != tupdesc->tdtypeid ||
|
tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
|
||||||
HeapTupleHeaderGetTypMod(td) != tupdesc->tdtypmod)
|
tmptup.t_data = td;
|
||||||
ereport(ERROR,
|
|
||||||
(errcode(ERRCODE_DATATYPE_MISMATCH),
|
|
||||||
errmsg("rows returned by function are not all of the same row type")));
|
|
||||||
|
|
||||||
/*
|
tuplestore_puttuple(tupstore, &tmptup);
|
||||||
* tuplestore_puttuple needs a HeapTuple not a bare
|
}
|
||||||
* HeapTupleHeader, but it doesn't need all the fields.
|
else
|
||||||
*/
|
{
|
||||||
tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
|
/*
|
||||||
tmptup.t_data = td;
|
* 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
|
else
|
||||||
|
{
|
||||||
|
/* Scalar-type case: just store the function result */
|
||||||
tuplestore_putvalues(tupstore, tupdesc, &result, &fcinfo.isnull);
|
tuplestore_putvalues(tupstore, tupdesc, &result, &fcinfo.isnull);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Are we done?
|
* Are we done?
|
||||||
@ -2343,7 +2350,8 @@ no_function_result:
|
|||||||
/*
|
/*
|
||||||
* If we got nothing from the function (ie, an empty-set or NULL 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
|
* 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)
|
if (rsinfo.setResult == NULL)
|
||||||
{
|
{
|
||||||
@ -2353,15 +2361,12 @@ no_function_result:
|
|||||||
if (!returnsSet)
|
if (!returnsSet)
|
||||||
{
|
{
|
||||||
int natts = expectedDesc->natts;
|
int natts = expectedDesc->natts;
|
||||||
Datum *nulldatums;
|
|
||||||
bool *nullflags;
|
bool *nullflags;
|
||||||
|
|
||||||
MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
|
MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
|
||||||
nulldatums = (Datum *) palloc0(natts * sizeof(Datum));
|
|
||||||
nullflags = (bool *) palloc(natts * sizeof(bool));
|
nullflags = (bool *) palloc(natts * sizeof(bool));
|
||||||
memset(nullflags, true, natts * sizeof(bool));
|
memset(nullflags, true, natts * sizeof(bool));
|
||||||
MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
|
tuplestore_putvalues(tupstore, expectedDesc, NULL, nullflags);
|
||||||
tuplestore_putvalues(tupstore, expectedDesc, nulldatums, nullflags);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2076,3 +2076,33 @@ select x from int8_tbl, extractq2_2_opt(int8_tbl) f(x);
|
|||||||
-4567890123456789
|
-4567890123456789
|
||||||
(5 rows)
|
(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;
|
||||||
|
@ -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);
|
||||||
|
|
||||||
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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user