From c94588fa1de8a75b2cdea78f548b82e3c55f569f Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-46-230.ec2.internal>
Date: Tue, 28 Oct 2025 17:13:41 +0000
Subject: [PATCH v17 1/1] Drop unnamed portal immediately after execution to
 completion

Previously, unnamed portals were kept until the next Bind or the end of
the transaction. This could cause temporary files to persist longer than
expected and make logging not reflect the actual SQL responsible for
the temporary file.

This patch changes exec_execute_message() to drop unnamed portals
immediately after execution to completion, ensuring temporary files are
removed promptly and statement logging correctly reflects the SQL that
created the temporary file.

The documentation is updated to accurately describe the lifetime of
unnamed portals, and test cases are updated to verify temporary file
removal and proper statement logging after unnamed portal execution.

Discussion: https://www.postgresql.org/message-id/CAA5RZ0tTrTUoEr3kDXCuKsvqYGq8OOHiBwoD-dyJocq95uEOTQ%40mail.gmail.com
---
 doc/src/sgml/protocol.sgml                     |  4 ++--
 src/backend/tcop/postgres.c                    |  7 +++++++
 .../modules/test_misc/t/009_log_temp_files.pl  | 18 +++++++-----------
 3 files changed, 16 insertions(+), 13 deletions(-)

diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 9d755232873..d1b9af11b07 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1006,8 +1006,8 @@ SELCT 1/0;<!-- this typo is intentional -->
    <para>
     If successfully created, a named portal object lasts till the end of the
     current transaction, unless explicitly destroyed.  An unnamed portal is
-    destroyed at the end of the transaction, or as soon as the next Bind
-    statement specifying the unnamed portal as destination is issued.  (Note
+    destroyed at the end of the transaction, or as soon as the statement
+    specifying the unnamed portal as destination is processed to completion.  (Note
     that a simple Query message also destroys the unnamed portal.)  Named
     portals must be explicitly closed before they can be redefined by another
     Bind message, but this is not required for the unnamed portal.
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7dd75a490aa..9a5534f8d13 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2327,6 +2327,13 @@ exec_execute_message(const char *portal_name, long max_rows)
 			 * message.  The next protocol message will start a fresh timeout.
 			 */
 			disable_statement_timeout();
+
+			/*
+			 * We completed fetching from an unnamed portal, we don't need it
+			 * beyond this point.
+			 */
+			if (portal->name[0] == '\0')
+				PortalDrop(portal, false);
 		}
 
 		/* Send appropriate CommandComplete to client */
diff --git a/src/test/modules/test_misc/t/009_log_temp_files.pl b/src/test/modules/test_misc/t/009_log_temp_files.pl
index 462a949e411..46d0c7789ca 100644
--- a/src/test/modules/test_misc/t/009_log_temp_files.pl
+++ b/src/test/modules/test_misc/t/009_log_temp_files.pl
@@ -39,7 +39,7 @@ SELECT 'unnamed portal';
 END;
 });
 ok( $node->log_contains(
-		qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+SELECT 'unnamed portal'/s,
+		qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+SELECT a FROM foo ORDER BY a OFFSET \$1/s,
 		$log_offset),
 	"unnamed portal");
 
@@ -49,10 +49,8 @@ $node->safe_psql(
 	"postgres", qq{
 SELECT a FROM foo ORDER BY a OFFSET \$1 \\bind 4991 \\g
 });
-ok( $node->log_contains(qr/LOG:\s+temporary file:/s, $log_offset),
-	"bind and implicit transaction, temporary file removed");
-ok( !$node->log_contains(qr/STATEMENT:/s, $log_offset),
-	"bind and implicit transaction, no statement logged");
+ok( $node->log_contains(qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+SELECT a FROM foo ORDER BY a OFFSET \$1/s, $log_offset),
+	"bind and implicit transaction");
 
 note "named portal: temporary file dropped under second SELECT query";
 $node->safe_psql(
@@ -64,7 +62,7 @@ SELECT 'named portal';
 END;
 });
 ok( $node->log_contains(
-		qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+SELECT 'named portal'/s,
+		qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+SELECT a FROM foo ORDER BY a OFFSET \$1/s,
 		$log_offset),
 	"named portal");
 
@@ -78,7 +76,7 @@ SELECT 'pipelined query';
 \\endpipeline
 });
 ok( $node->log_contains(
-		qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+SELECT 'pipelined query'/s,
+		qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+SELECT a FROM foo ORDER BY a OFFSET \$1/s,
 		$log_offset),
 	"pipelined query");
 
@@ -89,10 +87,8 @@ $node->safe_psql(
 SELECT a, a, a FROM foo ORDER BY a OFFSET \$1 \\parse p1
 \\bind_named p1 4993 \\g
 });
-ok($node->log_contains(qr/LOG:\s+temporary file:/s, $log_offset),
-	"parse and bind, temporary file removed");
-ok(!$node->log_contains(qr/STATEMENT:/s, $log_offset),
-	"bind and bind, no statement logged");
+ok($node->log_contains(qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+SELECT a, a, a FROM foo ORDER BY a OFFSET \$1/s, $log_offset),
+	"parse and bind");
 
 note "simple query: temporary file dropped under SELECT query";
 $log_offset = -s $node->logfile;
-- 
2.43.0

