From 0b92a248d722ea1aba37853131123b19bd4d1412 Mon Sep 17 00:00:00 2001
From: TatsuyaKawata <kawatatatsuya0913@gmail.com>
Date: Sat, 24 Oct 2026 23:20:58 +0900
Subject: [PATCH v6 2/2] Collect Function Scan storage statistics outside
 FunctionNext()

Based on a patch by David Rowley.  To be squashed into the previous
commit, replacing the third paragraph of its commit message with:

A function's tuplestore already holds its whole result set when it is
created, and is only destroyed by a rescan with changed parameters or at
the end of execution.  So we record the total space used by the
existing tuplestores right before a rescan destroys any of them, and
EXPLAIN looks at the ones still remaining at the end directly.  To allow
that, FunctionScanPerFuncState is moved into execnodes.h.
---
 src/backend/commands/explain.c            | 26 +++++++++
 src/backend/executor/nodeFunctionscan.c   | 64 ++++++++---------------
 src/include/nodes/execnodes.h             | 21 ++++++--
 src/test/regress/expected/planner_est.out |  3 +-
 src/test/regress/sql/planner_est.sql      |  3 +-
 5 files changed, 67 insertions(+), 50 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 1b7a3956e71..abcc47014c4 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3580,9 +3580,35 @@ show_ctescan_info(CteScanState *ctescanstate, ExplainState *es)
 static void
 show_functionscan_info(FunctionScanState *fsstate, ExplainState *es)
 {
+	bool		totalDiskUsed = false;
+	int64		totalSpaceUsed = 0;
+
 	if (!es->analyze)
 		return;
 
+	/*
+	 * Similar to the logic in ExecReScanFunctionScan(), we need to fetch the
+	 * stats from the final rescan (or the only one) and use these values if
+	 * they're higher than what's already stored in FunctionScanState.stats.
+	 */
+	for (int i = 0; i < fsstate->nfuncs; i++)
+	{
+		FunctionScanPerFuncState *fs = &fsstate->funcstates[i];
+
+		if (fs->tstore != NULL)
+		{
+			bool		diskUsed;
+			int64		spaceUsed;
+
+			tuplestore_get_stats(fs->tstore, &diskUsed, &spaceUsed);
+			totalSpaceUsed += spaceUsed;
+			totalDiskUsed |= diskUsed;
+		}
+	}
+
+	fsstate->stats.maxSpaceUsed = Max(fsstate->stats.maxSpaceUsed, totalSpaceUsed);
+	fsstate->stats.usedDisk |= totalDiskUsed;
+
 	/*
 	 * Nothing to show for the leader if execution didn't get as far as
 	 * creating any tuplestore.
diff --git a/src/backend/executor/nodeFunctionscan.c b/src/backend/executor/nodeFunctionscan.c
index 8b37a812fd0..34293612289 100644
--- a/src/backend/executor/nodeFunctionscan.c
+++ b/src/backend/executor/nodeFunctionscan.c
@@ -29,30 +29,8 @@
 #include "utils/memutils.h"
 #include "utils/tuplestore.h"
 
-
-/*
- * Runtime data for each function being scanned.
- */
-typedef struct FunctionScanPerFuncState
-{
-	SetExprState *setexpr;		/* 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 */
-	int64		rowcount;		/* # of rows in result set, -1 if not known */
-	TupleTableSlot *func_slot;	/* function result slot (or NULL) */
-
-	/*
-	 * Storage statistics of the current tuplestore, to report in EXPLAIN
-	 * ANALYZE.  Only meaningful while tstore is not NULL.
-	 */
-	bool		usedDisk;
-	int64		spaceUsed;
-} FunctionScanPerFuncState;
-
 static TupleTableSlot *FunctionNext(FunctionScanState *node);
-static void update_storage_stats(FunctionScanState *node,
-								 FunctionScanPerFuncState *fs);
+static void update_storage_stats(FunctionScanState *node);
 
 
 /* ----------------------------------------------------------------
@@ -113,10 +91,6 @@ 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]);
 		}
 
 		/*
@@ -176,10 +150,6 @@ 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);
 		}
 
 		/*
@@ -375,8 +345,6 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 		 */
 		fs->tstore = NULL;
 		fs->rowcount = -1;
-		fs->usedDisk = false;
-		fs->spaceUsed = 0;
 
 		/*
 		 * Now build a tupdesc showing the result type we expect from the
@@ -566,6 +534,9 @@ ExecEndFunctionScan(FunctionScanState *node)
 	{
 		FunctionScanInstrumentation *si;
 
+		/* Take the tuplestores still existing into account */
+		update_storage_stats(node);
+
 		Assert(ParallelWorkerNumber < node->shared_info->num_workers);
 		si = &node->shared_info->sinstrument[ParallelWorkerNumber];
 		si->maxSpaceUsed = Max(si->maxSpaceUsed, node->stats.maxSpaceUsed);
@@ -590,7 +561,7 @@ 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.
+ *		tuplestores that exist at this moment.
  *
  * A FunctionScan node has one tuplestore per function.  We report the peak
  * of the total space used by the tuplestores existing at the same time.  The
@@ -598,22 +569,25 @@ ExecEndFunctionScan(FunctionScanState *node)
  * any rescan, spilled to disk.
  */
 static void
-update_storage_stats(FunctionScanState *node, FunctionScanPerFuncState *fs)
+update_storage_stats(FunctionScanState *node)
 {
 	int64		totalSpaceUsed = 0;
 
-	tuplestore_get_stats(fs->tstore, &fs->usedDisk, &fs->spaceUsed);
-
 	for (int i = 0; i < node->nfuncs; i++)
 	{
-		FunctionScanPerFuncState *f = &node->funcstates[i];
+		FunctionScanPerFuncState *fs = &node->funcstates[i];
+		bool		usedDisk;
+		int64		spaceUsed;
 
-		if (f->tstore != NULL)
-			totalSpaceUsed += f->spaceUsed;
+		if (fs->tstore == NULL)
+			continue;
+
+		tuplestore_get_stats(fs->tstore, &usedDisk, &spaceUsed);
+		totalSpaceUsed += spaceUsed;
+		node->stats.usedDisk |= usedDisk;
 	}
 
 	node->stats.maxSpaceUsed = Max(node->stats.maxSpaceUsed, totalSpaceUsed);
-	node->stats.usedDisk |= fs->usedDisk;
 }
 
 /* ----------------------------------------------------------------
@@ -653,6 +627,14 @@ ExecReScanFunctionScan(FunctionScanState *node)
 	{
 		ListCell   *lc;
 
+		/*
+		 * Collect the storage statistics for EXPLAIN ANALYZE before any
+		 * tuplestore is destroyed.  The tuplestores of the functions that are
+		 * not recomputed exist at the same time, so they are counted too.
+		 */
+		if (node->ss.ps.instrument)
+			update_storage_stats(node);
+
 		i = 0;
 		foreach(lc, scan->functions)
 		{
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 6138ab29be2..9ee3b23cf06 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1910,6 +1910,20 @@ typedef struct SubqueryScanState
 	PlanState  *subplan;
 } SubqueryScanState;
 
+/* ----------------
+ *		Runtime data for each function being scanned by a FunctionScan node.
+ * ----------------
+ */
+typedef struct FunctionScanPerFuncState
+{
+	SetExprState *setexpr;		/* 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 */
+	int64		rowcount;		/* # of rows in result set, -1 if not known */
+	TupleTableSlot *func_slot;	/* function result slot (or NULL) */
+} FunctionScanPerFuncState;
+
 /* ----------------
  *	 FunctionScanState information
  *
@@ -1921,15 +1935,12 @@ typedef struct SubqueryScanState
  *		simple				true if we have 1 function and no ordinality
  *		ordinal				current ordinal column value
  *		nfuncs				number of functions being executed
- *		funcstates			per-function execution states (private in
- *							nodeFunctionscan.c)
+ *		funcstates			per-function execution states
  *		argcontext			memory context to evaluate function arguments in
  *		stats				storage statistics to report in EXPLAIN ANALYZE
  *		shared_info			statistics for parallel workers
  * ----------------
  */
-struct FunctionScanPerFuncState;
-
 typedef struct FunctionScanState
 {
 	ScanState	ss;				/* its first field is NodeTag */
@@ -1938,7 +1949,7 @@ typedef struct FunctionScanState
 	bool		simple;
 	int64		ordinal;
 	int			nfuncs;
-	struct FunctionScanPerFuncState *funcstates;	/* array of length nfuncs */
+	FunctionScanPerFuncState *funcstates;	/* array of length nfuncs */
 	MemoryContext argcontext;
 	FunctionScanInstrumentation stats;
 	SharedFunctionScanInfo *shared_info;
diff --git a/src/test/regress/expected/planner_est.out b/src/test/regress/expected/planner_est.out
index fbe2d5ff1a1..6a8d223831d 100644
--- a/src/test/regress/expected/planner_est.out
+++ b/src/test/regress/expected/planner_est.out
@@ -29,8 +29,7 @@ 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
+        -- ignore Storage output because the size varies depending on platform
         CONTINUE WHEN ln ~ '^\s*Storage: ';
 
         IF hide_costs = true THEN
diff --git a/src/test/regress/sql/planner_est.sql b/src/test/regress/sql/planner_est.sql
index 72aae8807ec..1da208eccce 100644
--- a/src/test/regress/sql/planner_est.sql
+++ b/src/test/regress/sql/planner_est.sql
@@ -30,8 +30,7 @@ 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
+        -- ignore Storage output because the size varies depending on platform
         CONTINUE WHEN ln ~ '^\s*Storage: ';
 
         IF hide_costs = true THEN
-- 
2.34.1

