>From 39b7ee10f7a5e13826bd77e7f9bb07c8a0d92653 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Fri, 26 Aug 2016 16:19:58 -0700
Subject: [PATCH 4/6] Allow ROWS FROM to return functions as single record
 column.

Using an empty AS list inside ROWS FROM (e.g.
SELECT * FROM ROWS FROM(aclexplode('{=r/andres}') AS ())), previously
not permitted by the grammar, now returns the results of the function as
a single record/composite field.

This is primarily interesting to allow for the conversion of SELECT
record_returning_srf(); into ROWS FROM. Without a facility like this
there'd be no way to do that for functions returning record, and it'd be
more complicated for composite returning functions.

Todo:
- cleanup code a bit, move some of the changes to previous commit
---
 src/backend/executor/nodeFunctionscan.c  | 267 ++++++++++++++++++++++---------
 src/backend/nodes/copyfuncs.c            |  16 ++
 src/backend/nodes/equalfuncs.c           |  14 ++
 src/backend/nodes/outfuncs.c             |  14 ++
 src/backend/nodes/readfuncs.c            |   1 +
 src/backend/optimizer/util/clauses.c     |   4 +
 src/backend/parser/gram.y                |  43 +++--
 src/backend/parser/parse_clause.c        |  18 +--
 src/backend/parser/parse_relation.c      |  66 +++++++-
 src/include/nodes/nodes.h                |   1 +
 src/include/nodes/parsenodes.h           |  21 ++-
 src/include/nodes/pg_list.h              |   9 ++
 src/include/parser/parse_relation.h      |   1 +
 src/test/regress/expected/rangefuncs.out |  85 ++++++++++
 src/test/regress/sql/rangefuncs.sql      |  18 +++
 15 files changed, 468 insertions(+), 110 deletions(-)

diff --git a/src/backend/executor/nodeFunctionscan.c b/src/backend/executor/nodeFunctionscan.c
index 4885f75..1da4fde 100644
--- a/src/backend/executor/nodeFunctionscan.c
+++ b/src/backend/executor/nodeFunctionscan.c
@@ -44,12 +44,15 @@
 typedef struct FunctionScanPerFuncState
 {
 	ExprState  *funcexpr;		/* state of the expression being evaluated */
-	TupleDesc	tupdesc;		/* desc of the function result type */
 	int			colcount;		/* expected number of result columns */
 	Tuplestorestate *tstore;	/* holds the function result set */
 	TupleTableSlot *func_slot;	/* function result slot (or NULL) */
+	TupleDesc	func_desc;		/* desc of the function result type */
+	TupleTableSlot *scan_slot;	/* function result slot (or NULL) */
+	TupleDesc	scan_desc;		/* tupdesc for returning a tuple as one col */
 	bool		started;
 	bool		returnsTuple;
+	bool		toRecord;
 	FunctionCallInfoData fcinfo;
 	ReturnSetInfo rsinfo;
 } FunctionScanPerFuncState;
@@ -109,7 +112,7 @@ FunctionNext(FunctionScanState *node)
 		else
 			ExecNextFunctionResult(node, fs);
 
-		scanslot = fs->func_slot;
+		scanslot = fs->scan_slot;
 
 		return scanslot;
 	}
@@ -125,7 +128,7 @@ FunctionNext(FunctionScanState *node)
 	/*
 	 * Main loop over functions.
 	 *
-	 * We fetch the function results into func_slots (which match the function
+	 * We fetch the function results into scan_slots (which match the function
 	 * return types), and then copy the values to scanslot (which matches the
 	 * scan result type), setting the ordinal column (if any) as well.
 	 */
@@ -147,7 +150,7 @@ FunctionNext(FunctionScanState *node)
 		else
 			ExecNextFunctionResult(node, fs);
 
-		if (TupIsNull(fs->func_slot))
+		if (TupIsNull(fs->scan_slot))
 		{
 			/*
 			 * populate the result cols with nulls
@@ -164,12 +167,12 @@ FunctionNext(FunctionScanState *node)
 			/*
 			 * we have a result, so just copy it to the result cols.
 			 */
-			slot_getallattrs(fs->func_slot);
+			slot_getallattrs(fs->scan_slot);
 
 			for (i = 0; i < fs->colcount; i++)
 			{
-				scanslot->tts_values[att] = fs->func_slot->tts_values[i];
-				scanslot->tts_isnull[att] = fs->func_slot->tts_isnull[i];
+				scanslot->tts_values[att] = fs->scan_slot->tts_values[i];
+				scanslot->tts_isnull[att] = fs->scan_slot->tts_isnull[i];
 				att++;
 			}
 
@@ -309,7 +312,6 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 		FunctionScanPerFuncState *fs = &scanstate->funcstates[i];
 		TypeFuncClass functypclass;
 		Oid			funcrettype;
-		TupleDesc	tupdesc;
 
 		fs->funcexpr = ExecInitExpr((Expr *) funcexpr, (PlanState *) scanstate);
 
@@ -321,6 +323,11 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 		fs->tstore = NULL;
 		fs->started = false;
 		fs->rsinfo.setDesc = NULL;
+		fs->toRecord = false;
+		fs->scan_desc = NULL;
+		fs->func_desc = NULL;
+		fs->scan_slot = NULL;
+		fs->func_slot = NULL;
 
 		/*
 		 * Now determine if the function returns a simple or composite type,
@@ -330,45 +337,77 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 		 */
 		functypclass = get_expr_result_type(funcexpr,
 											&funcrettype,
-											&tupdesc);
+											&fs->func_desc);
 
-		if (functypclass == TYPEFUNC_COMPOSITE)
+		if (rtfunc->funcasrecord)
 		{
-			/* Composite data type, e.g. a table's row type */
-			Assert(tupdesc);
-			Assert(tupdesc->natts >= colcount);
-			/* Must copy it out of typcache for safety */
-			tupdesc = CreateTupleDescCopy(tupdesc);
-			fs->returnsTuple = true;
-		}
-		else if (functypclass == TYPEFUNC_SCALAR)
-		{
-			/* Base data type, i.e. scalar */
-			tupdesc = CreateTemplateTupleDesc(1, false);
-			TupleDescInitEntry(tupdesc,
+			/*
+			 * Returning a composite / record type as one column, instead of
+			 * split up. Need to compute two tuple descs: One to pass to the
+			 * function (important when return type is determined in catalog),
+			 * and one specifying what is eventually returned to the user.
+			 */
+			Assert(functypclass == TYPEFUNC_COMPOSITE ||
+				   functypclass == TYPEFUNC_RECORD);
+
+			/*
+			 * fs->func_desc contains what the function returns, if known,
+			 * scan_desc what we make it return
+			 */
+			if (fs->func_desc)
+				fs->func_desc = CreateTupleDescCopy(fs->func_desc);
+			fs->scan_desc = CreateTemplateTupleDesc(1, false);
+			TupleDescInitEntry(fs->scan_desc,
 							   (AttrNumber) 1,
 							   NULL,	/* don't care about the name here */
 							   funcrettype,
 							   -1,
 							   0);
-			TupleDescInitEntryCollation(tupdesc,
+			TupleDescInitEntryCollation(fs->scan_desc,
 										(AttrNumber) 1,
 										exprCollation(funcexpr));
+			fs->toRecord = true;
+			fs->returnsTuple = true;
+		}
+		else if (functypclass == TYPEFUNC_COMPOSITE)
+		{
+			/* Composite data type, e.g. a table's row type */
+			Assert(fs->func_desc);
+			Assert(fs->func_desc->natts >= colcount);
+			/* Must copy it out of typcache for safety */
+			fs->func_desc = CreateTupleDescCopy(fs->func_desc);
+			fs->scan_desc = fs->func_desc;
+			fs->returnsTuple = true;
+		}
+		else if (functypclass == TYPEFUNC_SCALAR)
+		{
+			/* Base data type, i.e. scalar */
+			fs->func_desc = CreateTemplateTupleDesc(1, false);
+			TupleDescInitEntry(fs->func_desc,
+							   (AttrNumber) 1,
+							   NULL,	/* don't care about the name here */
+							   funcrettype,
+							   -1,
+							   0);
+			TupleDescInitEntryCollation(fs->func_desc,
+										(AttrNumber) 1,
+										exprCollation(funcexpr));
+			fs->scan_desc = fs->func_desc;
 			fs->returnsTuple = false;
 		}
 		else if (functypclass == TYPEFUNC_RECORD)
 		{
-			tupdesc = BuildDescFromLists(rtfunc->funccolnames,
-										 rtfunc->funccoltypes,
-										 rtfunc->funccoltypmods,
-										 rtfunc->funccolcollations);
-
+			fs->func_desc = BuildDescFromLists(rtfunc->funccolnames,
+											   rtfunc->funccoltypes,
+											   rtfunc->funccoltypmods,
+											   rtfunc->funccolcollations);
 			/*
 			 * For RECORD results, make sure a typmod has been assigned.  (The
 			 * function should do this for itself, but let's cover things in
 			 * case it doesn't.)
 			 */
-			BlessTupleDesc(tupdesc);
+			BlessTupleDesc(fs->func_desc);
+			fs->scan_desc = fs->func_desc;
 			fs->returnsTuple = true;
 		}
 		else
@@ -377,7 +416,6 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 			elog(ERROR, "function in FROM has unsupported return type");
 		}
 
-		fs->tupdesc = tupdesc;
 		fs->colcount = colcount;
 
 		/*
@@ -387,11 +425,11 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 		 */
 		if (!scanstate->simple)
 		{
-			fs->func_slot = ExecInitExtraTupleSlot(estate);
-			ExecSetSlotDescriptor(fs->func_slot, fs->tupdesc);
+			fs->scan_slot = ExecInitExtraTupleSlot(estate);
+			ExecSetSlotDescriptor(fs->scan_slot, fs->scan_desc);
 		}
 		else
-			fs->func_slot = scanstate->ss.ss_ScanTupleSlot;
+			fs->scan_slot = scanstate->ss.ss_ScanTupleSlot;
 
 		natts += colcount;
 		i++;
@@ -406,7 +444,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 	 */
 	if (scanstate->simple)
 	{
-		scan_tupdesc = CreateTupleDescCopy(scanstate->funcstates[0].tupdesc);
+		scan_tupdesc = CreateTupleDescCopy(scanstate->funcstates[0].scan_desc);
 		scan_tupdesc->tdtypeid = RECORDOID;
 		scan_tupdesc->tdtypmod = -1;
 	}
@@ -421,7 +459,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 
 		for (i = 0; i < nfuncs; i++)
 		{
-			TupleDesc	tupdesc = scanstate->funcstates[i].tupdesc;
+			TupleDesc	tupdesc = scanstate->funcstates[i].scan_desc;
 			int			colcount = scanstate->funcstates[i].colcount;
 			int			j;
 
@@ -498,6 +536,8 @@ ExecEndFunctionScan(FunctionScanState *node)
 	{
 		FunctionScanPerFuncState *fs = &node->funcstates[i];
 
+		if (fs->scan_slot)
+			ExecClearTuple(fs->scan_slot);
 		if (fs->func_slot)
 			ExecClearTuple(fs->func_slot);
 
@@ -526,6 +566,8 @@ ExecReScanFunctionScan(FunctionScanState *node)
 	{
 		FunctionScanPerFuncState *fs = &node->funcstates[i];
 
+		if (fs->scan_slot)
+			ExecClearTuple(fs->scan_slot);
 		if (fs->func_slot)
 			ExecClearTuple(fs->func_slot);
 
@@ -570,7 +612,7 @@ ExecBeginFunctionResult(FunctionScanState *node,
 
 	callerContext = CurrentMemoryContext;
 
-	Assert(perfunc->tupdesc != NULL);
+	Assert(perfunc->scan_desc != NULL);
 
 	/*
 	 * Prepare a resultinfo node for communication.  We always do this even if
@@ -581,7 +623,7 @@ ExecBeginFunctionResult(FunctionScanState *node,
 	 */
 	perfunc->rsinfo.type = T_ReturnSetInfo;
 	perfunc->rsinfo.econtext = econtext;
-	perfunc->rsinfo.expectedDesc = perfunc->tupdesc;
+	perfunc->rsinfo.expectedDesc = perfunc->func_desc;
 	perfunc->rsinfo.allowedModes = (int) (SFRM_ValuePerCall | SFRM_Materialize);
 	perfunc->rsinfo.returnMode = SFRM_ValuePerCall;
 	/* isDone is filled below */
@@ -718,7 +760,7 @@ ExecBeginFunctionResult(FunctionScanState *node,
 		/*
 		 * Store current resultset item.
 		 */
-		if (perfunc->returnsTuple)
+		if (perfunc->returnsTuple && !perfunc->toRecord)
 		{
 			if (!perfunc->fcinfo.isnull)
 			{
@@ -744,16 +786,16 @@ ExecBeginFunctionResult(FunctionScanState *node,
 					 * for functions returning RECORD, but might as well do it
 					 * always.
 					 */
-					tupledesc_match(perfunc->tupdesc, perfunc->rsinfo.setDesc);
+					tupledesc_match(perfunc->scan_desc, perfunc->rsinfo.setDesc);
 				}
 
 				tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
 				tmptup.t_data = td;
 
-				ExecStoreTuple(&tmptup, perfunc->func_slot, InvalidBuffer, false);
+				ExecStoreTuple(&tmptup, perfunc->scan_slot, InvalidBuffer, false);
 				/* materializing handles expanded and toasted datums */
 				/* XXX: would be nice if this could be optimized away */
-				ExecMaterializeSlot(perfunc->func_slot);
+				ExecMaterializeSlot(perfunc->scan_slot);
 			}
 			else
 			{
@@ -761,7 +803,7 @@ ExecBeginFunctionResult(FunctionScanState *node,
 				 * NULL result from a tuple-returning function; expand it
 				 * to a row of all nulls.
 				 */
-				ExecStoreAllNullTuple(perfunc->func_slot);
+				ExecStoreAllNullTuple(perfunc->scan_slot);
 			}
 		}
 		else
@@ -769,13 +811,13 @@ ExecBeginFunctionResult(FunctionScanState *node,
 			/*
 			 * Scalar-type case: just store the function result
 			 */
-			ExecClearTuple(perfunc->func_slot);
-			perfunc->func_slot->tts_values[0] = result;
-			perfunc->func_slot->tts_isnull[0] = perfunc->fcinfo.isnull;
-			ExecStoreVirtualTuple(perfunc->func_slot);
+			ExecClearTuple(perfunc->scan_slot);
+			perfunc->scan_slot->tts_values[0] = result;
+			perfunc->scan_slot->tts_isnull[0] = perfunc->fcinfo.isnull;
+			ExecStoreVirtualTuple(perfunc->scan_slot);
 
 			/* materializing handles expanded and toasted datums */
-			ExecMaterializeSlot(perfunc->func_slot);
+			ExecMaterializeSlot(perfunc->scan_slot);
 		}
 	}
 	else if (perfunc->rsinfo.returnMode == SFRM_Materialize)
@@ -809,25 +851,70 @@ ExecBeginFunctionResult(FunctionScanState *node,
 			 */
 			if (perfunc->rsinfo.setDesc)
 			{
-				tupledesc_match(perfunc->tupdesc, perfunc->rsinfo.setDesc);
+				if (!perfunc->toRecord)
+					tupledesc_match(perfunc->scan_desc, perfunc->rsinfo.setDesc);
+				else if (perfunc->func_desc)
+					tupledesc_match(perfunc->func_desc, perfunc->rsinfo.setDesc);
+			}
 
-				/*
-				 * If it is a dynamically-allocated TupleDesc, free it: it is
-				 * typically allocated in a per-query context, so we must avoid
-				 * leaking it across multiple usages.
-				 */
-				if (perfunc->rsinfo.setDesc->tdrefcount == -1)
+			if (!perfunc->toRecord)
+			{
+				/* and return first row */
+				(void) tuplestore_gettupleslot(perfunc->rsinfo.setResult,
+											   ScanDirectionIsForward(direction),
+											   false,
+											   perfunc->scan_slot);
+			}
+			else
+			{
+
+				if (perfunc->func_slot == NULL)
 				{
-					FreeTupleDesc(perfunc->rsinfo.setDesc);
-					perfunc->rsinfo.setDesc = NULL;
+					MemoryContext oldcontext;
+					TupleDesc slotDesc;
+
+					oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
+
+					/* don't assume slotDesc is long-lived */
+					if (perfunc->rsinfo.setDesc)
+						slotDesc = CreateTupleDescCopy(perfunc->rsinfo.setDesc);
+					else if (perfunc->func_desc)
+						slotDesc = perfunc->func_desc;
+					else
+						Assert(false);
+
+					perfunc->func_slot = MakeSingleTupleTableSlot(slotDesc);
+					MemoryContextSwitchTo(oldcontext);
+				}
+
+				(void) tuplestore_gettupleslot(perfunc->rsinfo.setResult,
+											   ScanDirectionIsForward(direction),
+											   false,
+											   perfunc->func_slot);
+
+				ExecClearTuple(perfunc->scan_slot);
+				if (!TupIsNull(perfunc->func_slot))
+				{
+					perfunc->scan_slot->tts_values[0] =
+						ExecFetchSlotTupleDatum(perfunc->func_slot);
+					perfunc->scan_slot->tts_isnull[0] = false;
+					ExecStoreVirtualTuple(perfunc->scan_slot);
+					/* materializing handles expanded and toasted datums */
+					ExecMaterializeSlot(perfunc->scan_slot);
 				}
 			}
 
-			/* and return first row */
-			(void) tuplestore_gettupleslot(perfunc->rsinfo.setResult,
-										   ScanDirectionIsForward(direction),
-										   false,
-										   perfunc->func_slot);
+			/*
+			 * If it is a dynamically-allocated TupleDesc, free it: it is
+			 * typically allocated in a per-query context, so we want to avoid
+			 * leaking it across multiple usages.
+			 */
+			if (perfunc->rsinfo.setDesc &&
+				perfunc->rsinfo.setDesc->tdrefcount == -1)
+			{
+				FreeTupleDesc(perfunc->rsinfo.setDesc);
+				perfunc->rsinfo.setDesc = NULL;
+			}
 		}
 	}
 	else
@@ -847,9 +934,9 @@ no_function_result:
 	 */
 	perfunc->rsinfo.isDone = ExprEndResult;
 	if (returnsSet)
-		ExecClearTuple(perfunc->func_slot);
+		ExecClearTuple(perfunc->scan_slot);
 	else
-		ExecStoreAllNullTuple(perfunc->func_slot);
+		ExecStoreAllNullTuple(perfunc->scan_slot);
 done:
 	MemoryContextSwitchTo(callerContext);
 }
@@ -870,15 +957,37 @@ ExecNextFunctionResult(FunctionScanState *node,
 
 	if (perfunc->tstore)
 	{
-		(void) tuplestore_gettupleslot(perfunc->tstore,
-									   ScanDirectionIsForward(direction),
-									   false,
-									   perfunc->func_slot);
+		if (!perfunc->toRecord)
+		{
+			/* and return first row */
+			(void) tuplestore_gettupleslot(perfunc->rsinfo.setResult,
+											   ScanDirectionIsForward(direction),
+										   false,
+										   perfunc->scan_slot);
+		}
+		else
+		{
+			(void) tuplestore_gettupleslot(perfunc->rsinfo.setResult,
+										   ScanDirectionIsForward(direction),
+										   false,
+										   perfunc->func_slot);
+
+			ExecClearTuple(perfunc->scan_slot);
+			if (!TupIsNull(perfunc->func_slot))
+			{
+				perfunc->scan_slot->tts_values[0] =
+					ExecFetchSlotTupleDatum(perfunc->func_slot);
+				perfunc->scan_slot->tts_isnull[0] = false;
+				ExecStoreVirtualTuple(perfunc->scan_slot);
+				/* materializing handles expanded and toasted datums */
+				ExecMaterializeSlot(perfunc->scan_slot);
+			}
+		}
 	}
 	else if (perfunc->rsinfo.isDone == ExprSingleResult ||
 			 perfunc->rsinfo.isDone == ExprEndResult)
 	{
-		ExecClearTuple(perfunc->func_slot);
+		ExecClearTuple(perfunc->scan_slot);
 	}
 	else
 	{
@@ -910,11 +1019,11 @@ ExecNextFunctionResult(FunctionScanState *node,
 
 		if (perfunc->rsinfo.isDone == ExprEndResult)
 		{
-			ExecClearTuple(perfunc->func_slot);
+			ExecClearTuple(perfunc->scan_slot);
 			goto out;
 		}
 
-		if (perfunc->returnsTuple)
+		if (perfunc->returnsTuple && !perfunc->toRecord)
 		{
 			if (!perfunc->fcinfo.isnull)
 			{
@@ -943,7 +1052,7 @@ ExecNextFunctionResult(FunctionScanState *node,
 					 * for functions returning RECORD, but might as well do it
 					 * always.
 					 */
-					tupledesc_match(perfunc->tupdesc, perfunc->rsinfo.setDesc);
+					tupledesc_match(perfunc->scan_desc, perfunc->rsinfo.setDesc);
 				}
 
 				tupdesc = perfunc->rsinfo.setDesc;
@@ -961,26 +1070,26 @@ ExecNextFunctionResult(FunctionScanState *node,
 				tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
 				tmptup.t_data = td;
 
-				ExecStoreTuple(&tmptup, perfunc->func_slot, InvalidBuffer, false);
+				ExecStoreTuple(&tmptup, perfunc->scan_slot, InvalidBuffer, false);
 				/* materializing handles expanded and toasted datums */
 				/* XXX: would be nice if this could be optimized away */
-				ExecMaterializeSlot(perfunc->func_slot);
+				ExecMaterializeSlot(perfunc->scan_slot);
 			}
 			else
 			{
-				ExecStoreAllNullTuple(perfunc->func_slot);
+				ExecStoreAllNullTuple(perfunc->scan_slot);
 			}
 		}
 		else
 		{
 			/* Scalar-type case: just store the function result */
-			ExecClearTuple(perfunc->func_slot);
-			perfunc->func_slot->tts_values[0] = result;
-			perfunc->func_slot->tts_isnull[0] = perfunc->fcinfo.isnull;
-			ExecStoreVirtualTuple(perfunc->func_slot);
+			ExecClearTuple(perfunc->scan_slot);
+			perfunc->scan_slot->tts_values[0] = result;
+			perfunc->scan_slot->tts_isnull[0] = perfunc->fcinfo.isnull;
+			ExecStoreVirtualTuple(perfunc->scan_slot);
 
 			/* materializing handles expanded and toasted datums */
-			ExecMaterializeSlot(perfunc->func_slot);
+			ExecMaterializeSlot(perfunc->scan_slot);
 		}
 	}
 
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 1877fb4..292ab6c 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2183,6 +2183,7 @@ _copyRangeTblFunction(const RangeTblFunction *from)
 	COPY_NODE_FIELD(funccoltypmods);
 	COPY_NODE_FIELD(funccolcollations);
 	COPY_BITMAPSET_FIELD(funcparams);
+	COPY_SCALAR_FIELD(funcasrecord);
 
 	return newnode;
 }
@@ -2556,6 +2557,18 @@ _copyRangeFunction(const RangeFunction *from)
 	return newnode;
 }
 
+static RangeFunctionElem *
+_copyRangeFunctionElem(const RangeFunctionElem *from)
+{
+	RangeFunctionElem *newnode = makeNode(RangeFunctionElem);
+
+	COPY_SCALAR_FIELD(asrecord);
+	COPY_NODE_FIELD(func);
+	COPY_NODE_FIELD(coldeflist);
+
+	return newnode;
+}
+
 static RangeTableSample *
 _copyRangeTableSample(const RangeTableSample *from)
 {
@@ -5016,6 +5029,9 @@ copyObject(const void *from)
 		case T_RangeFunction:
 			retval = _copyRangeFunction(from);
 			break;
+		case T_RangeFunctionElem:
+			retval = _copyRangeFunctionElem(from);
+			break;
 		case T_RangeTableSample:
 			retval = _copyRangeTableSample(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 448e1a9..f69dc8e 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2339,6 +2339,16 @@ _equalRangeFunction(const RangeFunction *a, const RangeFunction *b)
 }
 
 static bool
+_equalRangeFunctionElem(const RangeFunctionElem *a, const RangeFunctionElem *b)
+{
+	COMPARE_NODE_FIELD(func);
+	COMPARE_NODE_FIELD(coldeflist);
+	COMPARE_SCALAR_FIELD(asrecord);
+
+	return true;
+}
+
+static bool
 _equalRangeTableSample(const RangeTableSample *a, const RangeTableSample *b)
 {
 	COMPARE_NODE_FIELD(relation);
@@ -2484,6 +2494,7 @@ _equalRangeTblFunction(const RangeTblFunction *a, const RangeTblFunction *b)
 	COMPARE_NODE_FIELD(funccoltypmods);
 	COMPARE_NODE_FIELD(funccolcollations);
 	COMPARE_BITMAPSET_FIELD(funcparams);
+	COMPARE_SCALAR_FIELD(funcasrecord);
 
 	return true;
 }
@@ -3314,6 +3325,9 @@ equal(const void *a, const void *b)
 		case T_RangeFunction:
 			retval = _equalRangeFunction(a, b);
 			break;
+		case T_RangeFunctionElem:
+			retval = _equalRangeFunctionElem(a, b);
+			break;
 		case T_RangeTableSample:
 			retval = _equalRangeTableSample(a, b);
 			break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 29b7712..bea295b 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2879,6 +2879,7 @@ _outRangeTblFunction(StringInfo str, const RangeTblFunction *node)
 	WRITE_NODE_FIELD(funccoltypmods);
 	WRITE_NODE_FIELD(funccolcollations);
 	WRITE_BITMAPSET_FIELD(funcparams);
+	WRITE_BOOL_FIELD(funcasrecord);
 }
 
 static void
@@ -3148,6 +3149,16 @@ _outRangeFunction(StringInfo str, const RangeFunction *node)
 }
 
 static void
+_outRangeFunctionElem(StringInfo str, const RangeFunctionElem *node)
+{
+	WRITE_NODE_TYPE("RANGEFUNCTIONELEM");
+
+	WRITE_NODE_FIELD(func);
+	WRITE_BOOL_FIELD(coldeflist);
+	WRITE_BOOL_FIELD(asrecord);
+}
+
+static void
 _outRangeTableSample(StringInfo str, const RangeTableSample *node)
 {
 	WRITE_NODE_TYPE("RANGETABLESAMPLE");
@@ -3840,6 +3851,9 @@ outNode(StringInfo str, const void *obj)
 			case T_RangeFunction:
 				_outRangeFunction(str, obj);
 				break;
+			case T_RangeFunctionElem:
+				_outRangeFunctionElem(str, obj);
+				break;
 			case T_RangeTableSample:
 				_outRangeTableSample(str, obj);
 				break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 6f9a81e..70bb73d 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1357,6 +1357,7 @@ _readRangeTblFunction(void)
 	READ_NODE_FIELD(funccoltypmods);
 	READ_NODE_FIELD(funccolcollations);
 	READ_BITMAPSET_FIELD(funcparams);
+	READ_BOOL_FIELD(funcasrecord);
 
 	READ_DONE();
 }
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 4496fde..3830bc9 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -4828,6 +4828,10 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
 		return NULL;
 	rtfunc = (RangeTblFunction *) linitial(rte->functions);
 
+	/* Fail if the function returns as record - we don't implement that here. */
+	if (rtfunc->funcasrecord)
+		return NULL;
+
 	if (!IsA(rtfunc->funcexpr, FuncExpr))
 		return NULL;
 	fexpr = (FuncExpr *) rtfunc->funcexpr;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index cb5cfc4..183403f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -441,11 +441,11 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound
 				columnref in_expr having_clause func_table array_expr
 				ExclusionWhereClause
-%type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
+%type <list>	rowsfrom_list
 %type <boolean> opt_ordinality
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list
-%type <node>	func_arg_expr
+%type <node>	func_arg_expr rowsfrom_item
 %type <list>	row explicit_row implicit_row type_list array_expr_list
 %type <node>	case_expr case_arg when_clause case_default
 %type <list>	when_clause_list
@@ -10980,10 +10980,17 @@ opt_repeatable_clause:
 func_table: func_expr_windowless opt_ordinality
 				{
 					RangeFunction *n = makeNode(RangeFunction);
+					RangeFunctionElem *e = makeNode(RangeFunctionElem);
+
 					n->lateral = false;
 					n->ordinality = $2;
 					n->is_rowsfrom = false;
-					n->functions = list_make1(list_make2($1, NIL));
+					n->functions = list_make1(e);
+
+					e->func = $1;
+					e->coldeflist = NIL;
+					e->asrecord = false;
+
 					/* alias and coldeflist are set by table_ref production */
 					$$ = (Node *) n;
 				}
@@ -10999,8 +11006,30 @@ func_table: func_expr_windowless opt_ordinality
 				}
 		;
 
-rowsfrom_item: func_expr_windowless opt_col_def_list
-				{ $$ = list_make2($1, $2); }
+rowsfrom_item: func_expr_windowless AS '(' TableFuncElementList ')'
+				{
+					RangeFunctionElem *n = makeNode(RangeFunctionElem);
+					n->func = $1;
+					n->coldeflist = $4;
+					n->asrecord = false;
+					$$ = (Node *) n;
+				}
+			| func_expr_windowless AS '(' ')'
+				{
+					RangeFunctionElem *n = makeNode(RangeFunctionElem);
+					n->func = $1;
+					n->coldeflist = NIL;
+					n->asrecord = true;
+					$$ = (Node *) n;
+				}
+			| func_expr_windowless
+				{
+					RangeFunctionElem *n = makeNode(RangeFunctionElem);
+					n->func = $1;
+					n->coldeflist = NIL;
+					n->asrecord = false;
+					$$ = (Node *) n;
+				}
 		;
 
 rowsfrom_list:
@@ -11008,10 +11037,6 @@ rowsfrom_list:
 			| rowsfrom_list ',' rowsfrom_item	{ $$ = lappend($1, $3); }
 		;
 
-opt_col_def_list: AS '(' TableFuncElementList ')'	{ $$ = $3; }
-			| /*EMPTY*/								{ $$ = NIL; }
-		;
-
 opt_ordinality: WITH_LA ORDINALITY					{ $$ = true; }
 			| /*EMPTY*/								{ $$ = false; }
 		;
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 9b7fcc3..e40954c 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -533,6 +533,7 @@ transformRangeFunction(ParseState *pstate, RangeFunction *r)
 	List	   *funcexprs = NIL;
 	List	   *funcnames = NIL;
 	List	   *coldeflists = NIL;
+	List	   *asrecordlist = NIL;
 	bool		is_lateral;
 	RangeTblEntry *rte;
 	ListCell   *lc;
@@ -567,14 +568,9 @@ transformRangeFunction(ParseState *pstate, RangeFunction *r)
 	 */
 	foreach(lc, r->functions)
 	{
-		List	   *pair = (List *) lfirst(lc);
-		Node	   *fexpr;
-		List	   *coldeflist;
-
-		/* Disassemble the function-call/column-def-list pairs */
-		Assert(list_length(pair) == 2);
-		fexpr = (Node *) linitial(pair);
-		coldeflist = (List *) lsecond(pair);
+		RangeFunctionElem *elem = (RangeFunctionElem *) lfirst(lc);
+		Node	   *fexpr = elem->func;
+		List	   *coldeflist = elem->coldeflist;
 
 		/*
 		 * If we find a function call unnest() with more than one argument and
@@ -630,6 +626,8 @@ transformRangeFunction(ParseState *pstate, RangeFunction *r)
 					/* coldeflist is empty, so no error is possible */
 
 					coldeflists = lappend(coldeflists, coldeflist);
+
+					asrecordlist = lappend_int(asrecordlist, false);
 				}
 				continue;		/* done with this function item */
 			}
@@ -651,6 +649,8 @@ transformRangeFunction(ParseState *pstate, RangeFunction *r)
 									 exprLocation((Node *) r->coldeflist))));
 
 		coldeflists = lappend(coldeflists, coldeflist);
+
+		asrecordlist = lappend_int(asrecordlist, elem->asrecord);
 	}
 
 	pstate->p_lateral_active = false;
@@ -713,7 +713,7 @@ transformRangeFunction(ParseState *pstate, RangeFunction *r)
 	 */
 	rte = addRangeTableEntryForFunction(pstate,
 										funcnames, funcexprs, coldeflists,
-										r, is_lateral, true);
+										asrecordlist, r, is_lateral, true);
 
 	return rte;
 }
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 1e3ecbc..0d92a00 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -1382,6 +1382,7 @@ addRangeTableEntryForFunction(ParseState *pstate,
 							  List *funcnames,
 							  List *funcexprs,
 							  List *coldeflists,
+							  List *asrecordlist,
 							  RangeFunction *rangefunc,
 							  bool lateral,
 							  bool inFromCl)
@@ -1395,7 +1396,8 @@ addRangeTableEntryForFunction(ParseState *pstate,
 	TupleDesc	tupdesc;
 	ListCell   *lc1,
 			   *lc2,
-			   *lc3;
+			   *lc3,
+			   *lc4;
 	int			i;
 	int			j;
 	int			funcno;
@@ -1429,11 +1431,12 @@ addRangeTableEntryForFunction(ParseState *pstate,
 
 	totalatts = 0;
 	funcno = 0;
-	forthree(lc1, funcexprs, lc2, funcnames, lc3, coldeflists)
+	forfour(lc1, funcexprs, lc2, funcnames, lc3, coldeflists, lc4, asrecordlist)
 	{
 		Node	   *funcexpr = (Node *) lfirst(lc1);
 		char	   *funcname = (char *) lfirst(lc2);
 		List	   *coldeflist = (List *) lfirst(lc3);
+		int			asrecord = lfirst_int(lc4);
 		RangeTblFunction *rtfunc = makeNode(RangeTblFunction);
 		TypeFuncClass functypclass;
 		Oid			funcrettype;
@@ -1445,6 +1448,7 @@ addRangeTableEntryForFunction(ParseState *pstate,
 		rtfunc->funccoltypmods = NIL;
 		rtfunc->funccolcollations = NIL;
 		rtfunc->funcparams = NULL;		/* not set until planning */
+		rtfunc->funcasrecord = asrecord;
 
 		/*
 		 * Now determine if the function returns a simple or composite type.
@@ -1453,6 +1457,15 @@ addRangeTableEntryForFunction(ParseState *pstate,
 											&funcrettype,
 											&tupdesc);
 
+		if (asrecord && functypclass != TYPEFUNC_RECORD && functypclass != TYPEFUNC_COMPOSITE)
+		{
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("only composite and \"record\" returning functions can be returned as record"),
+						 parser_errposition(pstate,
+											exprLocation(funcexpr))));
+		}
+
 		/*
 		 * A coldeflist is required if the function returns RECORD and hasn't
 		 * got a predetermined record type, and is prohibited otherwise.
@@ -1466,16 +1479,26 @@ addRangeTableEntryForFunction(ParseState *pstate,
 						 parser_errposition(pstate,
 										exprLocation((Node *) coldeflist))));
 		}
-		else
+		else if (functypclass == TYPEFUNC_RECORD && !asrecord)
 		{
-			if (functypclass == TYPEFUNC_RECORD)
 				ereport(ERROR,
 						(errcode(ERRCODE_SYNTAX_ERROR),
 						 errmsg("a column definition list is required for functions returning \"record\""),
 						 parser_errposition(pstate, exprLocation(funcexpr))));
 		}
 
-		if (functypclass == TYPEFUNC_COMPOSITE)
+		if (asrecord)
+		{
+			tupdesc = CreateTemplateTupleDesc(1, false);
+			TupleDescInitEntry(tupdesc,
+							   (AttrNumber) 1,
+							   chooseScalarFunctionAlias(funcexpr, funcname,
+														 alias, nfuncs),
+							   funcrettype,
+							   -1,
+							   0);
+		}
+		else if (functypclass == TYPEFUNC_COMPOSITE)
 		{
 			/* Composite data type, e.g. a table's row type */
 			Assert(tupdesc);
@@ -2052,7 +2075,29 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up,
 					functypclass = get_expr_result_type(rtfunc->funcexpr,
 														&funcrettype,
 														&tupdesc);
-					if (functypclass == TYPEFUNC_COMPOSITE)
+
+					if (rtfunc->funcasrecord)
+					{
+						/* Base data type, i.e. scalar */
+						if (colnames)
+							*colnames = lappend(*colnames,
+												list_nth(rte->eref->colnames,
+														 atts_done));
+
+						if (colvars)
+						{
+							Var		   *varnode;
+
+							varnode = makeVar(rtindex, atts_done + 1,
+											  funcrettype, -1,
+											  exprCollation(rtfunc->funcexpr),
+											  sublevels_up);
+							varnode->location = location;
+
+							*colvars = lappend(*colvars, varnode);
+						}
+					}
+					else if (functypclass == TYPEFUNC_COMPOSITE)
 					{
 						/* Composite data type, e.g. a table's row type */
 						Assert(tupdesc);
@@ -2585,7 +2630,14 @@ get_rte_attribute_type(RangeTblEntry *rte, AttrNumber attnum,
 															&funcrettype,
 															&tupdesc);
 
-						if (functypclass == TYPEFUNC_COMPOSITE)
+						if (rtfunc->funcasrecord)
+						{
+							/* XXX */
+							*vartype = funcrettype;
+							*vartypmod = -1;
+							*varcollid = exprCollation(rtfunc->funcexpr);
+						}
+						else if (functypclass == TYPEFUNC_COMPOSITE)
 						{
 							/* Composite data type, e.g. a table's row type */
 							Form_pg_attribute att_tup;
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 2f7efa8..7d7bd91 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -427,6 +427,7 @@ typedef enum NodeTag
 	T_WindowDef,
 	T_RangeSubselect,
 	T_RangeFunction,
+	T_RangeFunctionElem,
 	T_RangeTableSample,
 	T_TypeName,
 	T_ColumnDef,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 1481fff..f26e651 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -522,12 +522,9 @@ typedef struct RangeSubselect
 /*
  * RangeFunction - function call appearing in a FROM clause
  *
- * functions is a List because we use this to represent the construct
- * ROWS FROM(func1(...), func2(...), ...).  Each element of this list is a
- * two-element sublist, the first element being the untransformed function
- * call tree, and the second element being a possibly-empty list of ColumnDef
- * nodes representing any columndef list attached to that function within the
- * ROWS FROM() syntax.
+ * functions is a List because we use this to represent the construct ROWS
+ * FROM(func1(...), func2(...), ...).  Each element of this list a
+ * RangeFunctionElem pointer.
  *
  * alias and coldeflist represent any alias and/or columndef list attached
  * at the top level.  (We disallow coldeflist appearing both here and
@@ -546,6 +543,17 @@ typedef struct RangeFunction
 } RangeFunction;
 
 /*
+ * RangeFunctionElem - individual function call in RangeFunction
+ */
+typedef struct RangeFunctionElem
+{
+	NodeTag		type;
+	Node	   *func;			/* untransformed function call */
+	List	   *coldeflist;		/* optional coldef list inside ROWS FROM */
+	bool		asrecord;		/* record returned as one column */
+} RangeFunctionElem;
+
+/*
  * RangeTableSample - TABLESAMPLE appearing in a raw FROM clause
  *
  * This node, appearing only in raw parse trees, represents
@@ -902,6 +910,7 @@ typedef struct RangeTblFunction
 	List	   *funccolcollations;		/* OID list of column collation OIDs */
 	/* This is set during planning for use by the executor: */
 	Bitmapset  *funcparams;		/* PARAM_EXEC Param IDs affecting this func */
+	bool		funcasrecord;   /* return results as a single record column */
 } RangeTblFunction;
 
 /*
diff --git a/src/include/nodes/pg_list.h b/src/include/nodes/pg_list.h
index 77b50ff..b6b57c1 100644
--- a/src/include/nodes/pg_list.h
+++ b/src/include/nodes/pg_list.h
@@ -185,6 +185,15 @@ list_length(const List *l)
 		 (cell1) != NULL && (cell2) != NULL && (cell3) != NULL;		\
 		 (cell1) = lnext(cell1), (cell2) = lnext(cell2), (cell3) = lnext(cell3))
 
+/*
+ * fortfour -
+ *	  the same for four lists
+ */
+#define forfour(cell1, list1, cell2, list2, cell3, list3, cell4, list4)			\
+	for ((cell1) = list_head(list1), (cell2) = list_head(list2), (cell3) = list_head(list3), (cell4) = list_head(list4); \
+		 (cell1) != NULL && (cell2) != NULL && (cell3) != NULL && (cell4) != NULL;		\
+		 (cell1) = lnext(cell1), (cell2) = lnext(cell2), (cell3) = lnext(cell3), (cell4) = lnext(cell4))
+
 extern List *lappend(List *list, void *datum);
 extern List *lappend_int(List *list, int datum);
 extern List *lappend_oid(List *list, Oid datum);
diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h
index 3ef3d7b..90af146 100644
--- a/src/include/parser/parse_relation.h
+++ b/src/include/parser/parse_relation.h
@@ -80,6 +80,7 @@ extern RangeTblEntry *addRangeTableEntryForFunction(ParseState *pstate,
 							  List *funcnames,
 							  List *funcexprs,
 							  List *coldeflists,
+							  List *asrecordlist,
 							  RangeFunction *rangefunc,
 							  bool lateral,
 							  bool inFromCl);
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index a6496fc..249dc67 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -369,6 +369,10 @@ SELECT * FROM getfoo1(1) WITH ORDINALITY AS t1(v,o);
  1 | 1
 (1 row)
 
+SELECT * FROM ROWS FROM( getfoo1(1) AS ()) AS t1; -- error, not a composite / record
+ERROR:  only composite and "record" returning functions can be returned as record
+LINE 1: SELECT * FROM ROWS FROM( getfoo1(1) AS ()) AS t1;
+                                 ^
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo1(1);
 SELECT * FROM vw_getfoo;
  getfoo1 
@@ -401,6 +405,10 @@ SELECT * FROM getfoo2(1) WITH ORDINALITY AS t1(v,o);
  1 | 2
 (2 rows)
 
+SELECT * FROM ROWS FROM( getfoo2(1) AS ()) AS t1; -- error, not a composite / record
+ERROR:  only composite and "record" returning functions can be returned as record
+LINE 1: SELECT * FROM ROWS FROM( getfoo2(1) AS ()) AS t1;
+                                 ^
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo2(1);
 SELECT * FROM vw_getfoo;
  getfoo2 
@@ -435,6 +443,10 @@ SELECT * FROM getfoo3(1) WITH ORDINALITY AS t1(v,o);
  Ed  | 2
 (2 rows)
 
+SELECT * FROM ROWS FROM( getfoo3(1) AS ()) AS t1; -- error, not a composite / record
+ERROR:  only composite and "record" returning functions can be returned as record
+LINE 1: SELECT * FROM ROWS FROM( getfoo3(1) AS ()) AS t1;
+                                 ^
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo3(1);
 SELECT * FROM vw_getfoo;
  getfoo3 
@@ -461,12 +473,24 @@ SELECT * FROM getfoo4(1) AS t1;
      1 |        1 | Joe
 (1 row)
 
+SELECT * FROM ROWS FROM( getfoo4(1) AS ()) AS t1;
+    t1     
+-----------
+ (1,1,Joe)
+(1 row)
+
 SELECT * FROM getfoo4(1) WITH ORDINALITY AS t1(a,b,c,o);
  a | b |  c  | o 
 ---+---+-----+---
  1 | 1 | Joe | 1
 (1 row)
 
+SELECT * FROM ROWS FROM( getfoo4(1) AS ()) WITH ORDINALITY AS t1(abc,o);
+    abc    | o 
+-----------+---
+ (1,1,Joe) | 1
+(1 row)
+
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo4(1);
 SELECT * FROM vw_getfoo;
  fooid | foosubid | fooname 
@@ -492,6 +516,13 @@ SELECT * FROM getfoo5(1) AS t1;
      1 |        2 | Ed
 (2 rows)
 
+SELECT * FROM ROWS FROM(getfoo5(1) AS ()) AS t1;
+    t1     
+-----------
+ (1,1,Joe)
+ (1,2,Ed)
+(2 rows)
+
 SELECT * FROM getfoo5(1) WITH ORDINALITY AS t1(a,b,c,o);
  a | b |  c  | o 
 ---+---+-----+---
@@ -499,6 +530,13 @@ SELECT * FROM getfoo5(1) WITH ORDINALITY AS t1(a,b,c,o);
  1 | 2 | Ed  | 2
 (2 rows)
 
+SELECT * FROM ROWS FROM(getfoo5(1) AS ()) WITH ORDINALITY AS t1(abc,o);
+    abc    | o 
+-----------+---
+ (1,1,Joe) | 1
+ (1,2,Ed)  | 2
+(2 rows)
+
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo5(1);
 SELECT * FROM vw_getfoo;
  fooid | foosubid | fooname 
@@ -525,12 +563,24 @@ SELECT * FROM getfoo6(1) AS t1(fooid int, foosubid int, fooname text);
      1 |        1 | Joe
 (1 row)
 
+SELECT * FROM ROWS FROM( getfoo6(1) AS ()) AS t1;
+    t1     
+-----------
+ (1,1,Joe)
+(1 row)
+
 SELECT * FROM ROWS FROM( getfoo6(1) AS (fooid int, foosubid int, fooname text) ) WITH ORDINALITY;
  fooid | foosubid | fooname | ordinality 
 -------+----------+---------+------------
      1 |        1 | Joe     |          1
 (1 row)
 
+SELECT * FROM ROWS FROM( getfoo6(1) AS ()) WITH ORDINALITY;
+  getfoo6  | ordinality 
+-----------+------------
+ (1,1,Joe) |          1
+(1 row)
+
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo6(1) AS
 (fooid int, foosubid int, fooname text);
 SELECT * FROM vw_getfoo;
@@ -559,6 +609,13 @@ SELECT * FROM getfoo7(1) AS t1(fooid int, foosubid int, fooname text);
      1 |        2 | Ed
 (2 rows)
 
+SELECT * FROM ROWS FROM( getfoo7(1) AS ()) AS t1;
+    t1     
+-----------
+ (1,1,Joe)
+ (1,2,Ed)
+(2 rows)
+
 SELECT * FROM ROWS FROM( getfoo7(1) AS (fooid int, foosubid int, fooname text) ) WITH ORDINALITY;
  fooid | foosubid | fooname | ordinality 
 -------+----------+---------+------------
@@ -566,6 +623,13 @@ SELECT * FROM ROWS FROM( getfoo7(1) AS (fooid int, foosubid int, fooname text) )
      1 |        2 | Ed      |          2
 (2 rows)
 
+SELECT * FROM ROWS FROM( getfoo7(1) AS ()) WITH ORDINALITY;
+  getfoo7  | ordinality 
+-----------+------------
+ (1,1,Joe) |          1
+ (1,2,Ed)  |          2
+(2 rows)
+
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo7(1) AS
 (fooid int, foosubid int, fooname text);
 SELECT * FROM vw_getfoo;
@@ -601,6 +665,10 @@ SELECT * FROM getfoo8(1) WITH ORDINALITY AS t1(v,o);
  1 | 1
 (1 row)
 
+SELECT * FROM ROWS FROM( getfoo8(1) AS ()) AS t1; -- error, not a composite / record
+ERROR:  only composite and "record" returning functions can be returned as record
+LINE 1: SELECT * FROM ROWS FROM( getfoo8(1) AS ()) AS t1;
+                                 ^
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo8(1);
 SELECT * FROM vw_getfoo;
  getfoo8 
@@ -631,6 +699,12 @@ SELECT * FROM getfoo9(1) WITH ORDINALITY AS t1(a,b,c,o);
  1 | 1 | Joe | 1
 (1 row)
 
+SELECT * FROM ROWS FROM( getfoo9(1) AS ()) AS t1;
+    t1     
+-----------
+ (1,1,Joe)
+(1 row)
+
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo9(1);
 SELECT * FROM vw_getfoo;
  fooid | foosubid | fooname 
@@ -670,6 +744,17 @@ select * from rows from(getfoo9(1),getfoo8(1),
    |   |     |   | 1 | 2 | Ed  |   |   |     | 1 | 2 | Ed  |   |   |     | Ed  | 1 |   | 2
 (2 rows)
 
+select * from rows from(getfoo9(1),getfoo8(1),
+                    getfoo7(1) AS (fooid int, foosubid int, fooname text),
+                    getfoo6(1) AS (),
+                    getfoo5(1),getfoo4(1),getfoo3(1),getfoo2(1),getfoo1(1))
+              with ordinality as t1(a,b,c,d,e,f,g,h,k,l,m,o,p,q,r,s,t,u);
+ a | b |  c  | d | e | f |  g  |     h     | k | l |  m  | o | p |  q  |  r  | s | t | u 
+---+---+-----+---+---+---+-----+-----------+---+---+-----+---+---+-----+-----+---+---+---
+ 1 | 1 | Joe | 1 | 1 | 1 | Joe | (1,1,Joe) | 1 | 1 | Joe | 1 | 1 | Joe | Joe | 1 | 1 | 1
+   |   |     |   | 1 | 2 | Ed  |           | 1 | 2 | Ed  |   |   |     | Ed  | 1 |   | 2
+(2 rows)
+
 create temporary view vw_foo as
   select * from rows from(getfoo9(1),
                       getfoo7(1) AS (fooid int, foosubid int, fooname text),
diff --git a/src/test/regress/sql/rangefuncs.sql b/src/test/regress/sql/rangefuncs.sql
index c8edc55..b43473a 100644
--- a/src/test/regress/sql/rangefuncs.sql
+++ b/src/test/regress/sql/rangefuncs.sql
@@ -93,6 +93,7 @@ INSERT INTO foo VALUES(2,1,'Mary');
 CREATE FUNCTION getfoo1(int) RETURNS int AS 'SELECT $1;' LANGUAGE SQL;
 SELECT * FROM getfoo1(1) AS t1;
 SELECT * FROM getfoo1(1) WITH ORDINALITY AS t1(v,o);
+SELECT * FROM ROWS FROM( getfoo1(1) AS ()) AS t1; -- error, not a composite / record
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo1(1);
 SELECT * FROM vw_getfoo;
 DROP VIEW vw_getfoo;
@@ -104,6 +105,7 @@ DROP VIEW vw_getfoo;
 CREATE FUNCTION getfoo2(int) RETURNS setof int AS 'SELECT fooid FROM foo WHERE fooid = $1;' LANGUAGE SQL;
 SELECT * FROM getfoo2(1) AS t1;
 SELECT * FROM getfoo2(1) WITH ORDINALITY AS t1(v,o);
+SELECT * FROM ROWS FROM( getfoo2(1) AS ()) AS t1; -- error, not a composite / record
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo2(1);
 SELECT * FROM vw_getfoo;
 DROP VIEW vw_getfoo;
@@ -115,6 +117,7 @@ DROP VIEW vw_getfoo;
 CREATE FUNCTION getfoo3(int) RETURNS setof text AS 'SELECT fooname FROM foo WHERE fooid = $1;' LANGUAGE SQL;
 SELECT * FROM getfoo3(1) AS t1;
 SELECT * FROM getfoo3(1) WITH ORDINALITY AS t1(v,o);
+SELECT * FROM ROWS FROM( getfoo3(1) AS ()) AS t1; -- error, not a composite / record
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo3(1);
 SELECT * FROM vw_getfoo;
 DROP VIEW vw_getfoo;
@@ -125,7 +128,9 @@ DROP VIEW vw_getfoo;
 -- sql, proretset = f, prorettype = c
 CREATE FUNCTION getfoo4(int) RETURNS foo AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL;
 SELECT * FROM getfoo4(1) AS t1;
+SELECT * FROM ROWS FROM( getfoo4(1) AS ()) AS t1;
 SELECT * FROM getfoo4(1) WITH ORDINALITY AS t1(a,b,c,o);
+SELECT * FROM ROWS FROM( getfoo4(1) AS ()) WITH ORDINALITY AS t1(abc,o);
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo4(1);
 SELECT * FROM vw_getfoo;
 DROP VIEW vw_getfoo;
@@ -136,7 +141,9 @@ DROP VIEW vw_getfoo;
 -- sql, proretset = t, prorettype = c
 CREATE FUNCTION getfoo5(int) RETURNS setof foo AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL;
 SELECT * FROM getfoo5(1) AS t1;
+SELECT * FROM ROWS FROM(getfoo5(1) AS ()) AS t1;
 SELECT * FROM getfoo5(1) WITH ORDINALITY AS t1(a,b,c,o);
+SELECT * FROM ROWS FROM(getfoo5(1) AS ()) WITH ORDINALITY AS t1(abc,o);
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo5(1);
 SELECT * FROM vw_getfoo;
 DROP VIEW vw_getfoo;
@@ -147,7 +154,9 @@ DROP VIEW vw_getfoo;
 -- sql, proretset = f, prorettype = record
 CREATE FUNCTION getfoo6(int) RETURNS RECORD AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL;
 SELECT * FROM getfoo6(1) AS t1(fooid int, foosubid int, fooname text);
+SELECT * FROM ROWS FROM( getfoo6(1) AS ()) AS t1;
 SELECT * FROM ROWS FROM( getfoo6(1) AS (fooid int, foosubid int, fooname text) ) WITH ORDINALITY;
+SELECT * FROM ROWS FROM( getfoo6(1) AS ()) WITH ORDINALITY;
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo6(1) AS
 (fooid int, foosubid int, fooname text);
 SELECT * FROM vw_getfoo;
@@ -161,7 +170,9 @@ DROP VIEW vw_getfoo;
 -- sql, proretset = t, prorettype = record
 CREATE FUNCTION getfoo7(int) RETURNS setof record AS 'SELECT * FROM foo WHERE fooid = $1;' LANGUAGE SQL;
 SELECT * FROM getfoo7(1) AS t1(fooid int, foosubid int, fooname text);
+SELECT * FROM ROWS FROM( getfoo7(1) AS ()) AS t1;
 SELECT * FROM ROWS FROM( getfoo7(1) AS (fooid int, foosubid int, fooname text) ) WITH ORDINALITY;
+SELECT * FROM ROWS FROM( getfoo7(1) AS ()) WITH ORDINALITY;
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo7(1) AS
 (fooid int, foosubid int, fooname text);
 SELECT * FROM vw_getfoo;
@@ -176,6 +187,7 @@ DROP VIEW vw_getfoo;
 CREATE FUNCTION getfoo8(int) RETURNS int AS 'DECLARE fooint int; BEGIN SELECT fooid into fooint FROM foo WHERE fooid = $1; RETURN fooint; END;' LANGUAGE plpgsql;
 SELECT * FROM getfoo8(1) AS t1;
 SELECT * FROM getfoo8(1) WITH ORDINALITY AS t1(v,o);
+SELECT * FROM ROWS FROM( getfoo8(1) AS ()) AS t1; -- error, not a composite / record
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo8(1);
 SELECT * FROM vw_getfoo;
 DROP VIEW vw_getfoo;
@@ -187,6 +199,7 @@ DROP VIEW vw_getfoo;
 CREATE FUNCTION getfoo9(int) RETURNS foo AS 'DECLARE footup foo%ROWTYPE; BEGIN SELECT * into footup FROM foo WHERE fooid = $1; RETURN footup; END;' LANGUAGE plpgsql;
 SELECT * FROM getfoo9(1) AS t1;
 SELECT * FROM getfoo9(1) WITH ORDINALITY AS t1(a,b,c,o);
+SELECT * FROM ROWS FROM( getfoo9(1) AS ()) AS t1;
 CREATE VIEW vw_getfoo AS SELECT * FROM getfoo9(1);
 SELECT * FROM vw_getfoo;
 DROP VIEW vw_getfoo;
@@ -206,6 +219,11 @@ select * from rows from(getfoo9(1),getfoo8(1),
                     getfoo6(1) AS (fooid int, foosubid int, fooname text),
                     getfoo5(1),getfoo4(1),getfoo3(1),getfoo2(1),getfoo1(1))
               with ordinality as t1(a,b,c,d,e,f,g,h,i,j,k,l,m,o,p,q,r,s,t,u);
+select * from rows from(getfoo9(1),getfoo8(1),
+                    getfoo7(1) AS (fooid int, foosubid int, fooname text),
+                    getfoo6(1) AS (),
+                    getfoo5(1),getfoo4(1),getfoo3(1),getfoo2(1),getfoo1(1))
+              with ordinality as t1(a,b,c,d,e,f,g,h,k,l,m,o,p,q,r,s,t,u);
 
 create temporary view vw_foo as
   select * from rows from(getfoo9(1),
-- 
2.9.3

