From 34ed8ee00320355d920ca8e58c901df698047faf Mon Sep 17 00:00:00 2001
From: TatsuyaKawata <kawatatatsuya0913@gmail.com>
Date: Mon, 5 Oct 2026 20:15:53 +0900
Subject: [PATCH v3 1/2] Keep Function Scan tuplestore statistics across
 rescans

When a Function Scan is rescanned with changed parameters,
ExecReScanFunctionScan() calls tuplestore_end() on the tuplestore of
each affected function and lets the next FunctionNext() call build a
fresh one.  tuplestore_end() frees the Tuplestorestate together with
the maximum storage statistics recorded in it, so only the statistics
of the final tuplestore survive until EXPLAIN ANALYZE inspects the node.

Unlike WindowAgg (908a96861), we cannot simply switch to
tuplestore_clear() and reuse the tuplestore here: the tuplestore is not
created by the node but by ExecMakeTableFunctionResult(), or by the
function itself in materialize mode, and a new one is returned on every
call.

Instead, record the storage type and size of the largest tuplestore
discarded so far in FunctionScanPerFuncState, just before it is ended.
---
 src/backend/executor/nodeFunctionscan.c | 38 +++++++++++++++++++++++++
 1 file changed, 38 insertions(+)

diff --git a/src/backend/executor/nodeFunctionscan.c b/src/backend/executor/nodeFunctionscan.c
index 1416f1f09ae..bbdef76ca63 100644
--- a/src/backend/executor/nodeFunctionscan.c
+++ b/src/backend/executor/nodeFunctionscan.c
@@ -41,9 +41,17 @@ 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 discarded by a rescan so
+	 * far.
+	 */
+	char	   *savedStorageType;
+	int64		savedSpaceUsed;
 } FunctionScanPerFuncState;
 
 static TupleTableSlot *FunctionNext(FunctionScanState *node);
+static void save_tuplestore_stats(FunctionScanPerFuncState *fs);
 
 
 /* ----------------------------------------------------------------
@@ -358,6 +366,8 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags)
 		 */
 		fs->tstore = NULL;
 		fs->rowcount = -1;
+		fs->savedStorageType = NULL;
+		fs->savedSpaceUsed = -1;
 
 		/*
 		 * Now build a tupdesc showing the result type we expect from the
@@ -549,6 +559,27 @@ ExecEndFunctionScan(FunctionScanState *node)
 	}
 }
 
+/*
+ * save_tuplestore_stats
+ *		Remember the storage statistics of the function's tuplestore, which is
+ *		about to be discarded, so that EXPLAIN ANALYZE can report the maximum
+ *		across all rescans.
+ */
+static void
+save_tuplestore_stats(FunctionScanPerFuncState *fs)
+{
+	char	   *storageType;
+	int64		spaceUsed;
+
+	tuplestore_get_stats(fs->tstore, &storageType, &spaceUsed);
+
+	if (spaceUsed > fs->savedSpaceUsed)
+	{
+		fs->savedSpaceUsed = spaceUsed;
+		fs->savedStorageType = storageType;
+	}
+}
+
 /* ----------------------------------------------------------------
  *		ExecReScanFunctionScan
  *
@@ -595,6 +626,13 @@ ExecReScanFunctionScan(FunctionScanState *node)
 			{
 				if (node->funcstates[i].tstore != NULL)
 				{
+					/*
+					 * We can't just tuplestore_clear() and reuse the
+					 * tuplestore, since ExecMakeTableFunctionResult() always
+					 * returns a new one.  Remember its statistics before
+					 * they're lost.
+					 */
+					save_tuplestore_stats(&node->funcstates[i]);
 					tuplestore_end(node->funcstates[i].tstore);
 					node->funcstates[i].tstore = NULL;
 				}
-- 
2.34.1

