From a0cb40cd12649e277fad0cc1ddebbd6a08cac3a6 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Wed, 17 Dec 2025 06:04:52 +0000
Subject: [PATCH v1] Support \portal meta command in psql.

---
 src/bin/psql/command.c                      | 36 +++++++++++++++++++++
 src/bin/psql/common.c                       |  3 +-
 src/bin/psql/settings.h                     |  2 ++
 src/bin/psql/tab-complete.in.c              |  2 +-
 src/test/regress/expected/psql.out          | 31 ++++++++++++++++++
 src/test/regress/expected/psql_pipeline.out | 21 ++++++++++++
 src/test/regress/sql/psql.sql               | 14 ++++++++
 src/test/regress/sql/psql_pipeline.sql      | 12 +++++++
 8 files changed, 119 insertions(+), 2 deletions(-)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 4a2976dddf0..759bdca772c 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -124,6 +124,8 @@ static backslashResult exec_command_print(PsqlScanState scan_state, bool active_
 static backslashResult exec_command_parse(PsqlScanState scan_state, bool active_branch,
 										  const char *cmd);
 static backslashResult exec_command_password(PsqlScanState scan_state, bool active_branch);
+static backslashResult exec_command_portal(PsqlScanState scan_state, bool active_branch,
+						const char *cmd);
 static backslashResult exec_command_prompt(PsqlScanState scan_state, bool active_branch,
 										   const char *cmd);
 static backslashResult exec_command_pset(PsqlScanState scan_state, bool active_branch);
@@ -425,6 +427,8 @@ exec_command(const char *cmd,
 		status = exec_command_parse(scan_state, active_branch, cmd);
 	else if (strcmp(cmd, "password") == 0)
 		status = exec_command_password(scan_state, active_branch);
+	else if (strcmp(cmd, "portal") == 0)
+		status = exec_command_portal(scan_state, active_branch, cmd);
 	else if (strcmp(cmd, "prompt") == 0)
 		status = exec_command_prompt(scan_state, active_branch, cmd);
 	else if (strcmp(cmd, "pset") == 0)
@@ -598,6 +602,38 @@ exec_command_bind_named(PsqlScanState scan_state, bool active_branch,
 	return status;
 }
 
+
+/*
+ * \bind_named -- set query parameters for an existing prepared statement
+ */
+static backslashResult
+exec_command_portal(PsqlScanState scan_state, bool active_branch,
+						const char *cmd)
+{
+	backslashResult status = PSQL_CMD_SKIP_LINE;
+
+	if (active_branch)
+	{
+		char	   *opt;
+
+		/* get the mandatory prepared statement name */
+		opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false);
+		if (!opt)
+		{
+			pg_log_error("\\%s: missing required argument", cmd);
+			status = PSQL_CMD_ERROR;
+		}
+		else
+		{
+			pset.portalName = opt;
+		}
+	}
+	else
+		ignore_slash_options(scan_state);
+
+	return status;
+}
+
 /*
  * \C -- override table title (formerly change HTML caption)
  */
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index a91acbf5acc..8d5d68b9501 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1596,7 +1596,7 @@ ExecQueryAndProcessResults(const char *query,
 			break;
 		case PSQL_SEND_EXTENDED_QUERY_PREPARED:
 			Assert(pset.stmtName != NULL);
-			success = PQsendQueryPrepared(pset.db, NULL, pset.stmtName,
+			success = PQsendQueryPrepared(pset.db, pset.portalName, pset.stmtName,
 										  pset.bind_nparams,
 										  (const char *const *) pset.bind_params,
 										  NULL, NULL, 0);
@@ -2688,6 +2688,7 @@ clean_extended_state(void)
 	}
 
 	pset.stmtName = NULL;
+	pset.portalName = NULL;
 	pset.send_mode = PSQL_SEND_QUERY;
 }
 
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index fd82303f776..5e186874bde 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -123,6 +123,8 @@ typedef struct _psqlSettings
 	char	  **bind_params;	/* parameters for extended query protocol call */
 	char	   *stmtName;		/* prepared statement name used for extended
 								 * query protocol commands */
+	char	   *portalName;		/* destincation portal name used for extended
+								 * query protocol commands */
 	int			piped_commands; /* number of piped commands */
 	int			piped_syncs;	/* number of piped syncs */
 	int			available_results;	/* number of results available to get */
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index b1ff6f6cd94..3f8134f3590 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1939,7 +1939,7 @@ psql_completion(const char *text, int start, int end)
 		"\\if", "\\include", "\\include_relative", "\\ir",
 		"\\list", "\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
 		"\\out",
-		"\\parse", "\\password", "\\print", "\\prompt", "\\pset",
+		"\\parse", "\\password", "\\print", "\\portal", "\\prompt", "\\pset",
 		"\\qecho", "\\quit",
 		"\\reset", "\\restrict",
 		"\\s", "\\sendpipeline", "\\set", "\\setenv", "\\sf",
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index c8f3932edf0..a9eda0ce606 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -160,6 +160,37 @@ LINE 1: SELECT $1, $2
  foo4     | bar4
 (1 row)
 
+-- Since portals do not survive transaction
+-- bound, we have to make explicit BEGIN-COMMIT
+BEGIN;
+-- \portal (extended query protocol)
+\bind_named stmt2 'foo5' \portal prtl1 \g
+ ?column? 
+----------
+ foo5
+(1 row)
+
+-- check we prepared in correct portal
+SELECT name FROM pg_cursors WHERE statement = 'SELECT $1 ';
+ name  
+-------
+ prtl1
+(1 row)
+
+\bind_named stmt3 'foo6', 'boo6' \portal prtl2 \g
+ ?column? | ?column? 
+----------+----------
+ foo6,    | boo6
+(1 row)
+
+-- check we prepared in correct portal
+SELECT name FROM pg_cursors WHERE statement = 'SELECT $1, $2 ';
+ name  
+-------
+ prtl2
+(1 row)
+
+COMMIT;
 -- \close_prepared (extended query protocol)
 \close_prepared
 \close_prepared: missing required argument
diff --git a/src/test/regress/expected/psql_pipeline.out b/src/test/regress/expected/psql_pipeline.out
index a0816fb10b6..870fbe861ca 100644
--- a/src/test/regress/expected/psql_pipeline.out
+++ b/src/test/regress/expected/psql_pipeline.out
@@ -417,6 +417,27 @@ SELECT $1 \bind 3 \sendpipeline
 (1 row)
 
 \endpipeline
+-- Test named portals
+-- Since portals do not survive transaction
+-- bound, we have to make explicit BEGIN-COMMIT
+BEGIN;
+\startpipeline
+SELECT 10 + $1 \parse s1 \bind_named s1 1 \portal p1 \sendpipeline \syncpipeline
+\syncpipeline
+\endpipeline
+ ?column? 
+----------
+       11
+(1 row)
+
+--recheck that statement was prepared in right portal
+SELECT name FROM pg_cursors WHERE statement = 'SELECT 10 + $1 ';
+ name 
+------
+ p1
+(1 row)
+
+COMMIT;
 --
 -- Pipeline errors
 --
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index dcdbd4fc020..095123ad320 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -68,6 +68,20 @@ SELECT $1, $2 \parse stmt3
 -- Multiple \g calls mean multiple executions
 \bind_named stmt2 'foo3' \g \bind_named stmt3 'foo4' 'bar4' \g
 
+-- Since portals do not survive transaction
+-- bound, we have to make explicit BEGIN-COMMIT
+BEGIN;
+-- \portal (extended query protocol)
+\bind_named stmt2 'foo5' \portal prtl1 \g
+-- check we prepared in correct portal
+SELECT name FROM pg_cursors WHERE statement = 'SELECT $1 ';
+
+\bind_named stmt3 'foo6', 'boo6' \portal prtl2 \g
+-- check we prepared in correct portal
+SELECT name FROM pg_cursors WHERE statement = 'SELECT $1, $2 ';
+
+COMMIT;
+
 -- \close_prepared (extended query protocol)
 \close_prepared
 \close_prepared ''
diff --git a/src/test/regress/sql/psql_pipeline.sql b/src/test/regress/sql/psql_pipeline.sql
index 6788dceee2e..f7f7f845439 100644
--- a/src/test/regress/sql/psql_pipeline.sql
+++ b/src/test/regress/sql/psql_pipeline.sql
@@ -200,6 +200,18 @@ SELECT $1 \bind 3 \sendpipeline
 \getresults 0
 \endpipeline
 
+-- Test named portals
+-- Since portals do not survive transaction
+-- bound, we have to make explicit BEGIN-COMMIT
+BEGIN;
+\startpipeline
+SELECT 10 + $1 \parse s1 \bind_named s1 1 \portal p1 \sendpipeline \syncpipeline
+\syncpipeline
+\endpipeline
+--recheck that statement was prepared in right portal
+SELECT name FROM pg_cursors WHERE statement = 'SELECT 10 + $1 ';
+COMMIT;
+
 --
 -- Pipeline errors
 --
-- 
2.43.0

