From a0cb40cd12649e277fad0cc1ddebbd6a08cac3a6 Mon Sep 17 00:00:00 2001 From: reshke 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