Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit d8411a6

Browse files
committed
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>
1 parent 976b24f commit d8411a6

File tree

3 files changed

+106
-60
lines changed

3 files changed

+106
-60
lines changed

src/backend/executor/execQual.c

+65-60
Original file line numberDiff line numberDiff line change
@@ -2229,89 +2229,96 @@ ExecMakeTableFunctionResult(ExprState *funcexpr,
22292229
break;
22302230

22312231
/*
2232-
* Can't do anything very useful with NULL rowtype values. For a
2233-
* function returning set, we consider this a protocol violation
2234-
* (but another alternative would be to just ignore the result and
2235-
* "continue" to get another row). For a function not returning
2236-
* set, we fall out of the loop; we'll cons up an all-nulls result
2237-
* row below.
2238-
*/
2239-
if (returnsTuple && fcinfo.isnull)
2240-
{
2241-
if (!returnsSet)
2242-
break;
2243-
ereport(ERROR,
2244-
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
2245-
errmsg("function returning set of rows cannot return null value")));
2246-
}
2247-
2248-
/*
2249-
* If first time through, build tupdesc and tuplestore for result
2232+
* If first time through, build tuplestore for result. For a
2233+
* scalar function result type, also make a suitable tupdesc.
22502234
*/
22512235
if (first_time)
22522236
{
22532237
oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
2254-
if (returnsTuple)
2255-
{
2256-
/*
2257-
* Use the type info embedded in the rowtype Datum to look
2258-
* up the needed tupdesc. Make a copy for the query.
2259-
*/
2260-
HeapTupleHeader td;
2261-
2262-
td = DatumGetHeapTupleHeader(result);
2263-
tupdesc = lookup_rowtype_tupdesc_copy(HeapTupleHeaderGetTypeId(td),
2264-
HeapTupleHeaderGetTypMod(td));
2265-
}
2266-
else
2238+
tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
2239+
rsinfo.setResult = tupstore;
2240+
if (!returnsTuple)
22672241
{
2268-
/*
2269-
* Scalar type, so make a single-column descriptor
2270-
*/
22712242
tupdesc = CreateTemplateTupleDesc(1, false);
22722243
TupleDescInitEntry(tupdesc,
22732244
(AttrNumber) 1,
22742245
"column",
22752246
funcrettype,
22762247
-1,
22772248
0);
2249+
rsinfo.setDesc = tupdesc;
22782250
}
2279-
tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
22802251
MemoryContextSwitchTo(oldcontext);
2281-
rsinfo.setResult = tupstore;
2282-
rsinfo.setDesc = tupdesc;
22832252
}
22842253

22852254
/*
22862255
* Store current resultset item.
22872256
*/
22882257
if (returnsTuple)
22892258
{
2290-
HeapTupleHeader td;
2259+
if (!fcinfo.isnull)
2260+
{
2261+
HeapTupleHeader td = DatumGetHeapTupleHeader(result);
22912262

2292-
td = DatumGetHeapTupleHeader(result);
2263+
if (tupdesc == NULL)
2264+
{
2265+
/*
2266+
* This is the first non-NULL result from the
2267+
* function. Use the type info embedded in the
2268+
* rowtype Datum to look up the needed tupdesc. Make
2269+
* a copy for the query.
2270+
*/
2271+
oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
2272+
tupdesc = lookup_rowtype_tupdesc_copy(HeapTupleHeaderGetTypeId(td),
2273+
HeapTupleHeaderGetTypMod(td));
2274+
rsinfo.setDesc = tupdesc;
2275+
MemoryContextSwitchTo(oldcontext);
2276+
}
2277+
else
2278+
{
2279+
/*
2280+
* Verify all later returned rows have same subtype;
2281+
* necessary in case the type is RECORD.
2282+
*/
2283+
if (HeapTupleHeaderGetTypeId(td) != tupdesc->tdtypeid ||
2284+
HeapTupleHeaderGetTypMod(td) != tupdesc->tdtypmod)
2285+
ereport(ERROR,
2286+
(errcode(ERRCODE_DATATYPE_MISMATCH),
2287+
errmsg("rows returned by function are not all of the same row type")));
2288+
}
22932289

2294-
/*
2295-
* Verify all returned rows have same subtype; necessary in
2296-
* case the type is RECORD.
2297-
*/
2298-
if (HeapTupleHeaderGetTypeId(td) != tupdesc->tdtypeid ||
2299-
HeapTupleHeaderGetTypMod(td) != tupdesc->tdtypmod)
2300-
ereport(ERROR,
2301-
(errcode(ERRCODE_DATATYPE_MISMATCH),
2302-
errmsg("rows returned by function are not all of the same row type")));
2290+
/*
2291+
* tuplestore_puttuple needs a HeapTuple not a bare
2292+
* HeapTupleHeader, but it doesn't need all the fields.
2293+
*/
2294+
tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
2295+
tmptup.t_data = td;
23032296

2304-
/*
2305-
* tuplestore_puttuple needs a HeapTuple not a bare
2306-
* HeapTupleHeader, but it doesn't need all the fields.
2307-
*/
2308-
tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
2309-
tmptup.t_data = td;
2297+
tuplestore_puttuple(tupstore, &tmptup);
2298+
}
2299+
else
2300+
{
2301+
/*
2302+
* NULL result from a tuple-returning function; expand it
2303+
* to a row of all nulls. We rely on the expectedDesc to
2304+
* form such rows. (Note: this would be problematic if
2305+
* tuplestore_putvalues saved the tdtypeid/tdtypmod from
2306+
* the provided descriptor, since that might not match
2307+
* what we get from the function itself. But it doesn't.)
2308+
*/
2309+
int natts = expectedDesc->natts;
2310+
bool *nullflags;
23102311

2311-
tuplestore_puttuple(tupstore, &tmptup);
2312+
nullflags = (bool *) palloc(natts * sizeof(bool));
2313+
memset(nullflags, true, natts * sizeof(bool));
2314+
tuplestore_putvalues(tupstore, expectedDesc, NULL, nullflags);
2315+
}
23122316
}
23132317
else
2318+
{
2319+
/* Scalar-type case: just store the function result */
23142320
tuplestore_putvalues(tupstore, tupdesc, &result, &fcinfo.isnull);
2321+
}
23152322

23162323
/*
23172324
* Are we done?
@@ -2343,7 +2350,8 @@ ExecMakeTableFunctionResult(ExprState *funcexpr,
23432350
/*
23442351
* If we got nothing from the function (ie, an empty-set or NULL result),
23452352
* we have to create the tuplestore to return, and if it's a
2346-
* non-set-returning function then insert a single all-nulls row.
2353+
* non-set-returning function then insert a single all-nulls row. As
2354+
* above, we depend on the expectedDesc to manufacture the dummy row.
23472355
*/
23482356
if (rsinfo.setResult == NULL)
23492357
{
@@ -2353,15 +2361,12 @@ ExecMakeTableFunctionResult(ExprState *funcexpr,
23532361
if (!returnsSet)
23542362
{
23552363
int natts = expectedDesc->natts;
2356-
Datum *nulldatums;
23572364
bool *nullflags;
23582365

23592366
MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
2360-
nulldatums = (Datum *) palloc0(natts * sizeof(Datum));
23612367
nullflags = (bool *) palloc(natts * sizeof(bool));
23622368
memset(nullflags, true, natts * sizeof(bool));
2363-
MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
2364-
tuplestore_putvalues(tupstore, expectedDesc, nulldatums, nullflags);
2369+
tuplestore_putvalues(tupstore, expectedDesc, NULL, nullflags);
23652370
}
23662371
}
23672372

src/test/regress/expected/rangefuncs.out

+30
Original file line numberDiff line numberDiff line change
@@ -2076,3 +2076,33 @@ select x from int8_tbl, extractq2_2_opt(int8_tbl) f(x);
20762076
-4567890123456789
20772077
(5 rows)
20782078

2079+
-- check handling of nulls in SRF results (bug #7808)
2080+
create type foo2 as (a integer, b text);
2081+
select *, row_to_json(u) from unnest(array[(1,'foo')::foo2, null::foo2]) u;
2082+
a | b | row_to_json
2083+
---+-----+---------------------
2084+
1 | foo | {"a":1,"b":"foo"}
2085+
| | {"a":null,"b":null}
2086+
(2 rows)
2087+
2088+
select *, row_to_json(u) from unnest(array[null::foo2, null::foo2]) u;
2089+
a | b | row_to_json
2090+
---+---+---------------------
2091+
| | {"a":null,"b":null}
2092+
| | {"a":null,"b":null}
2093+
(2 rows)
2094+
2095+
select *, row_to_json(u) from unnest(array[null::foo2, (1,'foo')::foo2, null::foo2]) u;
2096+
a | b | row_to_json
2097+
---+-----+---------------------
2098+
| | {"a":null,"b":null}
2099+
1 | foo | {"a":1,"b":"foo"}
2100+
| | {"a":null,"b":null}
2101+
(3 rows)
2102+
2103+
select *, row_to_json(u) from unnest(array[]::foo2[]) u;
2104+
a | b | row_to_json
2105+
---+---+-------------
2106+
(0 rows)
2107+
2108+
drop type foo2;

src/test/regress/sql/rangefuncs.sql

+11
Original file line numberDiff line numberDiff line change
@@ -639,3 +639,14 @@ explain (verbose, costs off)
639639
select x from int8_tbl, extractq2_2_opt(int8_tbl) f(x);
640640

641641
select x from int8_tbl, extractq2_2_opt(int8_tbl) f(x);
642+
643+
-- check handling of nulls in SRF results (bug #7808)
644+
645+
create type foo2 as (a integer, b text);
646+
647+
select *, row_to_json(u) from unnest(array[(1,'foo')::foo2, null::foo2]) u;
648+
select *, row_to_json(u) from unnest(array[null::foo2, null::foo2]) u;
649+
select *, row_to_json(u) from unnest(array[null::foo2, (1,'foo')::foo2, null::foo2]) u;
650+
select *, row_to_json(u) from unnest(array[]::foo2[]) u;
651+
652+
drop type foo2;

0 commit comments

Comments
 (0)