Hi David,

> That doesn't seem great. On checking, I see Material has the same
> issue, but that's not been highlighted by the buildfarm members
> running debug_parallel_query = regress due to lack of EXPLAIN ANALYZE
> test that has a Material node. I've just posted a patch to fix that in
> [1]. Can you follow that code and resolve this issue in your patch?

Thanks for committing the Material patch!  v4 Attached.
The main changes from v3 are:

- As is done for Material, each worker now copies its statistics into
  shared memory in ExecEndFunctionScan(), and EXPLAIN ANALYZE shows them
  per worker.

- I changed how the statistics are kept, so that the workers and EXPLAIN
  use the same values.  The tuplestore returned by
  ExecMakeTableFunctionResult() already holds the whole result set of
  the function and doesn't grow after that, so its statistics are taken
  right after it's returned, and used to update the per-function maximum
  and the node-level values.  EXPLAIN just reads the leader's values, and
  each worker copies its own into shared memory when it ends.

  This made v3-0001's saving of the statistics before a rescan discards
  the tuplestore, and moving FunctionScanPerFuncState into execnodes.h,
  unnecessary, so I've merged the patches into one.  The statistics are
  only collected for EXPLAIN ANALYZE.

- Removed max_parallel_workers_per_gather = 0 from the tests.  I've
  confirmed make check passes with debug_parallel_query = regress too.

Regards,
Tatsuya Kawata
From 193b9af6f1639295fe4dc0524b1d866728288f73 Mon Sep 17 00:00:00 2001
From: TatsuyaKawata <[email protected]>
Date: Tue, 13 Oct 2026 22:30:40 +0900
Subject: [PATCH v4] Add memory/disk usage for Function Scan nodes in EXPLAIN

Commit 1eff8279d4 added the infrastructure to report the maximum
tuplestore memory or disk usage with EXPLAIN ANALYZE, and 95d6e9af07 and
40708acd65 extended it to WindowAgg, CTE Scan, Table Function Scan and
Recursive Union nodes.  Function Scan was never covered, even though a
function in FROM always materializes its result into a tuplestore, and
that tuplestore can silently spill to disk once work_mem is exceeded.

Report the storage type and maximum storage used for Function Scan nodes
too.  With ROWS FROM there is one tuplestore per function; as for
Recursive Union, we report the storage type of the largest one and the
sum of their sizes.

ExecMakeTableFunctionResult() returns a tuplestore that already holds
the whole result set of the function, so its statistics are collected
right after that.  As a rescan with changed parameters replaces the
tuplestore, we keep the largest one seen for each function.

As is done for Material nodes, parallel workers copy their statistics
into shared memory when they end, so that EXPLAIN ANALYZE can show them
per worker.
---
 src/backend/commands/explain.c            |  45 ++++++
 src/backend/executor/execParallel.c       |  19 +++
 src/backend/executor/nodeFunctionscan.c   | 170 ++++++++++++++++++++++
 src/include/executor/instrument_node.h    |  21 +++
 src/include/executor/nodeFunctionscan.h   |   8 +
 src/include/nodes/execnodes.h             |   4 +
 src/test/regress/expected/explain.out     |  43 +++++-
 src/test/regress/expected/planner_est.out |   4 +
 src/test/regress/sql/explain.sql          |   8 +
 src/test/regress/sql/planner_est.sql      |   4 +
 src/tools/pgindent/typedefs.list          |   2 +
 11 files changed, 325 insertions(+), 3 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 4b1ad4227ac..1b7a3956e71 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -130,6 +130,8 @@ static void show_hash_info(HashState *hashstate, ExplainState *es);
 static void show_material_info(MaterialState *mstate, ExplainState *es);
 static void show_windowagg_info(WindowAggState *winstate, ExplainState *es);
 static void show_ctescan_info(CteScanState *ctescanstate, ExplainState *es);
+static void show_functionscan_info(FunctionScanState *fsstate,
+								   ExplainState *es);
 static void show_table_func_scan_info(TableFuncScanState *tscanstate,
 									  ExplainState *es);
 static void show_recursive_union_info(RecursiveUnionState *rstate,
@@ -2097,6 +2099,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
+			show_functionscan_info(castNode(FunctionScanState, planstate), es);
 			break;
 		case T_TableFuncScan:
 			if (es->verbose)
@@ -3570,6 +3573,48 @@ show_ctescan_info(CteScanState *ctescanstate, ExplainState *es)
 	show_storage_info(usedDisk, maxSpaceUsed, es);
 }
 
+/*
+ * Show information on Function Scan node, storage method and maximum
+ * memory/disk space used.
+ */
+static void
+show_functionscan_info(FunctionScanState *fsstate, ExplainState *es)
+{
+	if (!es->analyze)
+		return;
+
+	/*
+	 * Nothing to show for the leader if execution didn't get as far as
+	 * creating any tuplestore.
+	 */
+	if (fsstate->stats.maxSpaceUsed > 0)
+		show_storage_info(fsstate->stats.usedDisk,
+						  fsstate->stats.maxSpaceUsed, es);
+
+	if (fsstate->shared_info == NULL)
+		return;
+
+	/* Show details from parallel workers */
+	for (int n = 0; n < fsstate->shared_info->num_workers; n++)
+	{
+		FunctionScanInstrumentation *si;
+
+		si = &fsstate->shared_info->sinstrument[n];
+
+		/* Skip workers that didn't create a tuplestore */
+		if (si->maxSpaceUsed == 0)
+			continue;
+
+		if (es->workers_state)
+			ExplainOpenWorker(n, es);
+
+		show_storage_info(si->usedDisk, si->maxSpaceUsed, es);
+
+		if (es->workers_state)
+			ExplainCloseWorker(n, es);
+	}
+}
+
 /*
  * Show information on Table Function Scan node, storage method and maximum
  * memory/disk space used.
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index 316dd3f67c6..2fe874fc5d4 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -31,6 +31,7 @@
 #include "executor/nodeBitmapIndexscan.h"
 #include "executor/nodeCustom.h"
 #include "executor/nodeForeignscan.h"
+#include "executor/nodeFunctionscan.h"
 #include "executor/nodeHash.h"
 #include "executor/nodeHashjoin.h"
 #include "executor/nodeIncrementalSort.h"
@@ -343,6 +344,10 @@ ExecParallelEstimate(PlanState *planstate, ExecParallelEstimateContext *e)
 			/* even when not parallel-aware, for EXPLAIN ANALYZE */
 			ExecMaterialEstimate((MaterialState *) planstate, e->pcxt);
 			break;
+		case T_FunctionScanState:
+			/* even when not parallel-aware, for EXPLAIN ANALYZE */
+			ExecFunctionScanEstimate((FunctionScanState *) planstate, e->pcxt);
+			break;
 		default:
 			break;
 	}
@@ -595,6 +600,11 @@ ExecParallelInitializeDSM(PlanState *planstate,
 			/* even when not parallel-aware, for EXPLAIN ANALYZE */
 			ExecMaterialInitializeDSM((MaterialState *) planstate, d->pcxt);
 			break;
+		case T_FunctionScanState:
+			/* even when not parallel-aware, for EXPLAIN ANALYZE */
+			ExecFunctionScanInitializeDSM((FunctionScanState *) planstate,
+										  d->pcxt);
+			break;
 		default:
 			break;
 	}
@@ -1084,6 +1094,7 @@ ExecParallelReInitializeDSM(PlanState *planstate,
 		case T_IncrementalSortState:
 		case T_MemoizeState:
 		case T_MaterialState:
+		case T_FunctionScanState:
 			/* these nodes have DSM state, but no reinitialization is required */
 			break;
 
@@ -1168,6 +1179,9 @@ ExecParallelRetrieveInstrumentation(PlanState *planstate,
 		case T_MaterialState:
 			ExecMaterialRetrieveInstrumentation((MaterialState *) planstate);
 			break;
+		case T_FunctionScanState:
+			ExecFunctionScanRetrieveInstrumentation((FunctionScanState *) planstate);
+			break;
 		case T_BitmapHeapScanState:
 			ExecBitmapHeapRetrieveInstrumentation((BitmapHeapScanState *) planstate);
 			break;
@@ -1501,6 +1515,11 @@ ExecParallelInitializeWorker(PlanState *planstate, ParallelWorkerContext *pwcxt)
 			/* even when not parallel-aware, for EXPLAIN ANALYZE */
 			ExecMaterialInitializeWorker((MaterialState *) planstate, pwcxt);
 			break;
+		case T_FunctionScanState:
+			/* even when not parallel-aware, for EXPLAIN ANALYZE */
+			ExecFunctionScanInitializeWorker((FunctionScanState *) planstate,
+											 pwcxt);
+			break;
 		default:
 			break;
 	}
diff --git a/src/backend/executor/nodeFunctionscan.c b/src/backend/executor/nodeFunctionscan.c
index 1416f1f09ae..b1c78edc47d 100644
--- a/src/backend/executor/nodeFunctionscan.c
+++ b/src/backend/executor/nodeFunctionscan.c
@@ -41,9 +41,19 @@ typedef struct FunctionScanPerFuncState
 	Tuplestorestate *tstore;	/* holds the function result set */
 	int64		rowcount;		/* # of rows in result set, -1 if not known */
 	TupleTableSlot *func_slot;	/* function result slot (or NULL) */
+
+	/*
+	 * Storage statistics of the largest tuplestore created for this function
+	 * so far, to report in EXPLAIN ANALYZE.  maxSpaceUsed is 0 if none has
+	 * been created yet.
+	 */
+	bool		maxUsedDisk;
+	int64		maxSpaceUsed;
 } FunctionScanPerFuncState;
 
 static TupleTableSlot *FunctionNext(FunctionScanState *node);
+static void update_storage_stats(FunctionScanState *node,
+								 FunctionScanPerFuncState *fs);
 
 
 /* ----------------------------------------------------------------
@@ -104,6 +114,10 @@ FunctionNext(FunctionScanState *node)
 			 * call is fast, so the overhead shouldn't be an issue.
 			 */
 			tuplestore_rescan(tstore);
+
+			/* Collect its storage statistics for EXPLAIN ANALYZE */
+			if (node->ss.ps.instrument)
+				update_storage_stats(node, &node->funcstates[0]);
 		}
 
 		/*
@@ -163,6 +177,10 @@ FunctionNext(FunctionScanState *node)
 			 * call is fast, so the overhead shouldn't be an issue.
 			 */
 			tuplestore_rescan(fs->tstore);
+
+			/* Collect its storage statistics for EXPLAIN ANALYZE */
+			if (node->ss.ps.instrument)
+				update_storage_stats(node, fs);
 		}
 
 		/*
@@ -358,6 +376,8 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 		 */
 		fs->tstore = NULL;
 		fs->rowcount = -1;
+		fs->maxUsedDisk = false;
+		fs->maxSpaceUsed = 0;
 
 		/*
 		 * Now build a tupdesc showing the result type we expect from the
@@ -520,6 +540,8 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 												  "Table function arguments",
 												  ALLOCSET_DEFAULT_SIZES);
 
+	scanstate->shared_info = NULL;
+
 	return scanstate;
 }
 
@@ -534,6 +556,23 @@ ExecEndFunctionScan(FunctionScanState *node)
 {
 	int			i;
 
+	/*
+	 * When ending a parallel worker, copy the tuplestore usage details into
+	 * shared memory so that they can be picked up by the main process to
+	 * report in EXPLAIN ANALYZE.  The Gather node above us may be rescanned,
+	 * in which case a new set of workers will write to the same slots, so
+	 * merge with any existing values rather than overwriting them.
+	 */
+	if (node->shared_info != NULL && IsParallelWorker())
+	{
+		FunctionScanInstrumentation *si;
+
+		Assert(ParallelWorkerNumber < node->shared_info->num_workers);
+		si = &node->shared_info->sinstrument[ParallelWorkerNumber];
+		si->maxSpaceUsed = Max(si->maxSpaceUsed, node->stats.maxSpaceUsed);
+		si->usedDisk |= node->stats.usedDisk;
+	}
+
 	/*
 	 * Release slots and tuplestore resources
 	 */
@@ -549,6 +588,50 @@ ExecEndFunctionScan(FunctionScanState *node)
 	}
 }
 
+/*
+ * update_storage_stats
+ *		Update the storage statistics to report in EXPLAIN ANALYZE with the
+ *		function's tuplestore, which already holds the whole result set.
+ *
+ * For each function we keep the largest tuplestore seen across rescans.  As
+ * for Recursive Union, the node reports the storage type of the largest one
+ * and the sum of their sizes.
+ */
+static void
+update_storage_stats(FunctionScanState *node, FunctionScanPerFuncState *fs)
+{
+	bool		usedDisk;
+	int64		spaceUsed;
+	bool		maxUsedDisk = false;
+	int64		maxSpaceUsed = 0;
+	int64		totalSpaceUsed = 0;
+
+	tuplestore_get_stats(fs->tstore, &usedDisk, &spaceUsed);
+
+	/* Nothing changes unless it's the largest one for this function so far */
+	if (spaceUsed <= fs->maxSpaceUsed)
+		return;
+
+	fs->maxUsedDisk = usedDisk;
+	fs->maxSpaceUsed = spaceUsed;
+
+	for (int i = 0; i < node->nfuncs; i++)
+	{
+		FunctionScanPerFuncState *f = &node->funcstates[i];
+
+		totalSpaceUsed += f->maxSpaceUsed;
+
+		if (f->maxSpaceUsed > maxSpaceUsed)
+		{
+			maxSpaceUsed = f->maxSpaceUsed;
+			maxUsedDisk = f->maxUsedDisk;
+		}
+	}
+
+	node->stats.usedDisk = maxUsedDisk;
+	node->stats.maxSpaceUsed = totalSpaceUsed;
+}
+
 /* ----------------------------------------------------------------
  *		ExecReScanFunctionScan
  *
@@ -614,3 +697,90 @@ ExecReScanFunctionScan(FunctionScanState *node)
 			tuplestore_rescan(node->funcstates[i].tstore);
 	}
 }
+
+/* ----------------------------------------------------------------
+ *						Parallel Query Support
+ * ----------------------------------------------------------------
+ */
+
+/* ----------------------------------------------------------------
+ *		ExecFunctionScanEstimate
+ *
+ *		Estimate space required to propagate function scan statistics.
+ * ----------------------------------------------------------------
+ */
+void
+ExecFunctionScanEstimate(FunctionScanState *node, ParallelContext *pcxt)
+{
+	Size		size;
+
+	/* don't need this if not instrumenting or no workers */
+	if (!node->ss.ps.instrument || pcxt->nworkers == 0)
+		return;
+
+	size = mul_size(pcxt->nworkers, sizeof(FunctionScanInstrumentation));
+	size = add_size(size, offsetof(SharedFunctionScanInfo, sinstrument));
+	shm_toc_estimate_chunk(&pcxt->estimator, size);
+	shm_toc_estimate_keys(&pcxt->estimator, 1);
+}
+
+/* ----------------------------------------------------------------
+ *		ExecFunctionScanInitializeDSM
+ *
+ *		Initialize DSM space for function scan statistics.
+ * ----------------------------------------------------------------
+ */
+void
+ExecFunctionScanInitializeDSM(FunctionScanState *node, ParallelContext *pcxt)
+{
+	Size		size;
+
+	/* don't need this if not instrumenting or no workers */
+	if (!node->ss.ps.instrument || pcxt->nworkers == 0)
+		return;
+
+	size = offsetof(SharedFunctionScanInfo, sinstrument)
+		+ pcxt->nworkers * sizeof(FunctionScanInstrumentation);
+	node->shared_info = shm_toc_allocate(pcxt->toc, size);
+	/* ensure any unfilled slots will contain zeroes */
+	memset(node->shared_info, 0, size);
+	node->shared_info->num_workers = pcxt->nworkers;
+	shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id,
+				   node->shared_info);
+}
+
+/* ----------------------------------------------------------------
+ *		ExecFunctionScanInitializeWorker
+ *
+ *		Attach worker to DSM space for function scan statistics.
+ * ----------------------------------------------------------------
+ */
+void
+ExecFunctionScanInitializeWorker(FunctionScanState *node,
+								 ParallelWorkerContext *pwcxt)
+{
+	node->shared_info =
+		shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, true);
+}
+
+/* ----------------------------------------------------------------
+ *		ExecFunctionScanRetrieveInstrumentation
+ *
+ *		Transfer function scan statistics from DSM to private memory.
+ * ----------------------------------------------------------------
+ */
+void
+ExecFunctionScanRetrieveInstrumentation(FunctionScanState *node)
+{
+	Size		size;
+	SharedFunctionScanInfo *si;
+
+	if (node->shared_info == NULL)
+		return;
+
+	size = offsetof(SharedFunctionScanInfo, sinstrument)
+		+ node->shared_info->num_workers * sizeof(FunctionScanInstrumentation);
+	si = palloc(size);
+	memcpy(si, node->shared_info, size);
+	node->shared_info = si;
+}
diff --git a/src/include/executor/instrument_node.h b/src/include/executor/instrument_node.h
index 4759a628afe..f6e5263e9bf 100644
--- a/src/include/executor/instrument_node.h
+++ b/src/include/executor/instrument_node.h
@@ -194,6 +194,27 @@ typedef struct SharedMaterialInfo
 } SharedMaterialInfo;
 
 
+/* ---------------------
+ *	Instrumentation information for FunctionScan
+ * ---------------------
+ */
+typedef struct FunctionScanInstrumentation
+{
+	int64		maxSpaceUsed;	/* peak memory/disk usage in bytes, or 0 if no
+								 * tuplestore was created */
+	bool		usedDisk;		/* did the tuplestore spill to disk? */
+} FunctionScanInstrumentation;
+
+/*
+ * Shared memory container for per-worker function scan information
+ */
+typedef struct SharedFunctionScanInfo
+{
+	int			num_workers;
+	FunctionScanInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER];
+} SharedFunctionScanInfo;
+
+
 /* ---------------------
  *	Instrumentation information for Sorts.
  * ---------------------
diff --git a/src/include/executor/nodeFunctionscan.h b/src/include/executor/nodeFunctionscan.h
index e6a1a42d3f9..3cfd61e8b8d 100644
--- a/src/include/executor/nodeFunctionscan.h
+++ b/src/include/executor/nodeFunctionscan.h
@@ -14,10 +14,18 @@
 #ifndef NODEFUNCTIONSCAN_H
 #define NODEFUNCTIONSCAN_H
 
+#include "access/parallel.h"
 #include "nodes/execnodes.h"
 
 extern FunctionScanState *ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags);
 extern void ExecEndFunctionScan(FunctionScanState *node);
 extern void ExecReScanFunctionScan(FunctionScanState *node);
+extern void ExecFunctionScanEstimate(FunctionScanState *node,
+									 ParallelContext *pcxt);
+extern void ExecFunctionScanInitializeDSM(FunctionScanState *node,
+										  ParallelContext *pcxt);
+extern void ExecFunctionScanInitializeWorker(FunctionScanState *node,
+											 ParallelWorkerContext *pwcxt);
+extern void ExecFunctionScanRetrieveInstrumentation(FunctionScanState *node);
 
 #endif							/* NODEFUNCTIONSCAN_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index e3cea52e7df..4017d8b9c0f 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1924,6 +1924,8 @@ typedef struct SubqueryScanState
  *		funcstates			per-function execution states (private in
  *							nodeFunctionscan.c)
  *		argcontext			memory context to evaluate function arguments in
+ *		stats				storage statistics to report in EXPLAIN ANALYZE
+ *		shared_info			statistics for parallel workers
  * ----------------
  */
 struct FunctionScanPerFuncState;
@@ -1938,6 +1940,8 @@ typedef struct FunctionScanState
 	int			nfuncs;
 	struct FunctionScanPerFuncState *funcstates;	/* array of length nfuncs */
 	MemoryContext argcontext;
+	FunctionScanInstrumentation stats;
+	SharedFunctionScanInfo *shared_info;
 } FunctionScanState;
 
 /* ----------------
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index 74a4d87801e..1603c806876 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -799,9 +799,10 @@ select explain_filter('explain (analyze,buffers off,costs off) select sum(n) ove
    Window: w1 AS ()
    Storage: Memory  Maximum Storage: NkB
    ->  Function Scan on generate_series a (actual time=N.N..N.N rows=N.N loops=N)
+         Storage: Memory  Maximum Storage: NkB
  Planning Time: N.N ms
  Execution Time: N.N ms
-(6 rows)
+(7 rows)
 
 -- Test tuplestore storage usage in Window aggregate (disk case)
 set work_mem to 64;
@@ -812,9 +813,10 @@ select explain_filter('explain (analyze,buffers off,costs off) select sum(n) ove
    Window: w1 AS ()
    Storage: Disk  Maximum Storage: NkB
    ->  Function Scan on generate_series a (actual time=N.N..N.N rows=N.N loops=N)
+         Storage: Disk  Maximum Storage: NkB
  Planning Time: N.N ms
  Execution Time: N.N ms
-(6 rows)
+(7 rows)
 
 -- Test tuplestore storage usage in Window aggregate (memory and disk case, final result is disk)
 select explain_filter('explain (analyze,buffers off,costs off) select sum(n) over(partition by m) from (SELECT n < 3 as m, n from generate_series(1,2500) a(n))');
@@ -827,8 +829,43 @@ select explain_filter('explain (analyze,buffers off,costs off) select sum(n) ove
          Sort Key: ((a.n < N))
          Sort Method: external merge  Disk: NkB
          ->  Function Scan on generate_series a (actual time=N.N..N.N rows=N.N loops=N)
+               Storage: Disk  Maximum Storage: NkB
  Planning Time: N.N ms
  Execution Time: N.N ms
-(9 rows)
+(10 rows)
+
+-- Test tuplestore storage usage in Function Scan (memory case)
+select explain_filter('explain (analyze,buffers off,costs off) select count(*) from generate_series(1,10) a(n)');
+                                  explain_filter                                  
+----------------------------------------------------------------------------------
+ Aggregate (actual time=N.N..N.N rows=N.N loops=N)
+   ->  Function Scan on generate_series a (actual time=N.N..N.N rows=N.N loops=N)
+         Storage: Memory  Maximum Storage: NkB
+ Planning Time: N.N ms
+ Execution Time: N.N ms
+(5 rows)
+
+-- Test tuplestore storage usage in Function Scan (disk case)
+select explain_filter('explain (analyze,buffers off,costs off) select count(*) from generate_series(1,2500) a(n)');
+                                  explain_filter                                  
+----------------------------------------------------------------------------------
+ Aggregate (actual time=N.N..N.N rows=N.N loops=N)
+   ->  Function Scan on generate_series a (actual time=N.N..N.N rows=N.N loops=N)
+         Storage: Disk  Maximum Storage: NkB
+ Planning Time: N.N ms
+ Execution Time: N.N ms
+(5 rows)
+
+-- Test tuplestore storage usage in Function Scan with ROWS FROM, which uses
+-- one tuplestore per function
+select explain_filter('explain (analyze,buffers off,costs off) select count(*) from rows from (generate_series(1,2500), generate_series(1,2500)) a(n,m)');
+                          explain_filter                          
+------------------------------------------------------------------
+ Aggregate (actual time=N.N..N.N rows=N.N loops=N)
+   ->  Function Scan on a (actual time=N.N..N.N rows=N.N loops=N)
+         Storage: Disk  Maximum Storage: NkB
+ Planning Time: N.N ms
+ Execution Time: N.N ms
+(5 rows)
 
 reset work_mem;
diff --git a/src/test/regress/expected/planner_est.out b/src/test/regress/expected/planner_est.out
index 236cb274a78..fbe2d5ff1a1 100644
--- a/src/test/regress/expected/planner_est.out
+++ b/src/test/regress/expected/planner_est.out
@@ -29,6 +29,10 @@ BEGIN
         EXECUTE format('explain (analyze %s, costs on, summary off, timing off, buffers off) %s',
             analyze_str, query)
     LOOP
+        -- Ignore Storage output because the size varies depending on
+        -- the platform
+        CONTINUE WHEN ln ~ '^\s*Storage: ';
+
         IF hide_costs = true THEN
             ln := regexp_replace(ln, 'cost=\d+\.\d\d\.\.\d+\.\d\d', 'cost=N..N');
         END IF;
diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql
index 2f163c64bf6..cb18f495343 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -190,4 +190,12 @@ set work_mem to 64;
 select explain_filter('explain (analyze,buffers off,costs off) select sum(n) over() from generate_series(1,2500) a(n)');
 -- Test tuplestore storage usage in Window aggregate (memory and disk case, final result is disk)
 select explain_filter('explain (analyze,buffers off,costs off) select sum(n) over(partition by m) from (SELECT n < 3 as m, n from generate_series(1,2500) a(n))');
+
+-- Test tuplestore storage usage in Function Scan (memory case)
+select explain_filter('explain (analyze,buffers off,costs off) select count(*) from generate_series(1,10) a(n)');
+-- Test tuplestore storage usage in Function Scan (disk case)
+select explain_filter('explain (analyze,buffers off,costs off) select count(*) from generate_series(1,2500) a(n)');
+-- Test tuplestore storage usage in Function Scan with ROWS FROM, which uses
+-- one tuplestore per function
+select explain_filter('explain (analyze,buffers off,costs off) select count(*) from rows from (generate_series(1,2500), generate_series(1,2500)) a(n,m)');
 reset work_mem;
diff --git a/src/test/regress/sql/planner_est.sql b/src/test/regress/sql/planner_est.sql
index 2b696a4e4e5..72aae8807ec 100644
--- a/src/test/regress/sql/planner_est.sql
+++ b/src/test/regress/sql/planner_est.sql
@@ -30,6 +30,10 @@ BEGIN
         EXECUTE format('explain (analyze %s, costs on, summary off, timing off, buffers off) %s',
             analyze_str, query)
     LOOP
+        -- Ignore Storage output because the size varies depending on
+        -- the platform
+        CONTINUE WHEN ln ~ '^\s*Storage: ';
+
         IF hide_costs = true THEN
             ln := regexp_replace(ln, 'cost=\d+\.\d\d\.\.\d+\.\d\d', 'cost=N..N');
         END IF;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a7a8dbdd23f..0f010a68157 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1038,6 +1038,7 @@ FunctionCallInfoBaseData
 FunctionParameter
 FunctionParameterMode
 FunctionScan
+FunctionScanInstrumentation
 FunctionScanPerFuncState
 FunctionScanState
 FuzzyAttrMatchState
@@ -2850,6 +2851,7 @@ SharedDependencyObjectType
 SharedDependencyType
 SharedExecutorInstrumentation
 SharedFileSet
+SharedFunctionScanInfo
 SharedHashInfo
 SharedIncrementalSortInfo
 SharedIndexScanInstrumentation
-- 
2.34.1

Reply via email to