From d55322b0da60a8798ffdb8b78ef90db0fb5be18e Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Thu, 22 Aug 2024 16:25:57 +0900 Subject: [PATCH] psql: Add more meta-commands able to use the extended protocol Currently, only unnamed prepared statement are supported by psql with the meta-command \bind. With only this command, it is not possible to test named statement creation, execution or close through the extended protocol. This commit introduces three additional commands: * \parse creates a prepared statement using the extended protocol, acting as a wrapper of libpq's PQsendPrepare(). * \bind_named binds and executes an existing prepared statement using the extended protocol, for PQsendQueryPrepared(). * \close closes an existing prepared statement using the extended protocol, for PQsendClosePrepared(). This is going to be useful to add regression tests for the extended query protocol, and I have some plans for that on separate threads. Note that \bind relies on PQsendQueryParams(). The code of psql is refactored so as bind_flag is replaced by an enum in _psqlSettings that tracks the type of libpq routine to execute, based on the meta-command involved, with the default being PQsendQuery(). This refactoring piece has been written by me, while Anthonin has implemented the rest. Author: Anthonin Bonnefoy, Michael Paquier Reviewed-by: Aleksander Alekseev, Jelte Fennema-Nio Discussion: https://postgr.es/m/CAO6_XqpSq0Q0kQcVLCbtagY94V2GxNP3zCnR6WnOM8WqXPK4nw@mail.gmail.com --- doc/src/sgml/ref/psql-ref.sgml | 90 ++++++++++++++++++++ src/bin/psql/command.c | 130 ++++++++++++++++++++++++++++- src/bin/psql/common.c | 59 ++++++++++--- src/bin/psql/help.c | 4 + src/bin/psql/settings.h | 15 +++- src/bin/psql/tab-complete.c | 6 +- src/test/regress/expected/psql.out | 55 ++++++++++++ src/test/regress/sql/psql.sql | 28 ++++++- src/tools/pgindent/typedefs.list | 1 + 9 files changed, 369 insertions(+), 19 deletions(-) diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 07419a3b92..3fd9959ed1 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -917,6 +917,36 @@ INSERT INTO tbl1 VALUES ($1, $2) \bind 'first value' 'second value' \g + + \bind_named statement_name [ parameter ] ... + + + + \bind_named is equivalent to \bind, + except that it takes the name of an existing prepared statement as + first parameter. An empty string denotes the unnamed prepared + statement. + + + + Example: + +INSERT INTO tbls1 VALUES ($1, $2) \parse stmt1 +\bind_named stmt1 'first value' 'second value' \g + + + + + This command causes the extended query protocol (see + ) to be used, unlike normal + psql operation, which uses the simple + query protocol. So this command can be useful to test the extended + query protocol from psql. + + + + + \c or \connect [ -reuse-previous=on|off ] [ dbname [ username ] [ host ] [ port ] | conninfo ] @@ -1038,6 +1068,35 @@ INSERT INTO tbl1 VALUES ($1, $2) \bind 'first value' 'second value' \g + + \close prepared_statement_name + + + + Closes the specified prepared statement. An empty string denotes the + unnamed prepared statement. If no prepared statement exists with this + name, the operation is a no-op. + + + + Example: + +SELECT $1 \parse stmt1 +\close stmt1 + + + + + This command causes the extended query protocol to be used, + unlike normal psql operation, which + uses the simple query protocol. So this command can be useful + to test the extended query protocol from + psql. + + + + + \copy { table [ ( column_list ) ] } from @@ -2780,6 +2839,37 @@ lo_import 152801 + + \parse statement_name + + + Creates a prepared statement from the current query buffer, based on + the name of a destination prepared-statement object. An empty string + denotes the unnamed prepared statement. + + + + Example: + +SELECT $1 \parse stmt1 + + + + + This command causes the extended query protocol to be used, unlike + normal psql operation, which uses the + simple query protocol. A + + message will be issued by this command so it can be useful to + test the extended query protocol from + psql. This command affects only the next + query executed; all subsequent queries will use the simple query + protocol by default. + + + + + \password [ username ] diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 180781ecd0..4dfc7b2d85 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -64,10 +64,14 @@ static backslashResult exec_command(const char *cmd, PQExpBuffer previous_buf); static backslashResult exec_command_a(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_bind(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_bind_named(PsqlScanState scan_state, bool active_branch, + const char *cmd); static backslashResult exec_command_C(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_connect(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_cd(PsqlScanState scan_state, bool active_branch, const char *cmd); +static backslashResult exec_command_close(PsqlScanState scan_state, bool active_branch, + const char *cmd); static backslashResult exec_command_conninfo(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_copy(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_copyright(PsqlScanState scan_state, bool active_branch); @@ -116,6 +120,8 @@ static backslashResult exec_command_lo(PsqlScanState scan_state, bool active_bra static backslashResult exec_command_out(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_print(PsqlScanState scan_state, bool active_branch, PQExpBuffer query_buf, PQExpBuffer previous_buf); +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_prompt(PsqlScanState scan_state, bool active_branch, const char *cmd); @@ -312,12 +318,16 @@ exec_command(const char *cmd, status = exec_command_a(scan_state, active_branch); else if (strcmp(cmd, "bind") == 0) status = exec_command_bind(scan_state, active_branch); + else if (strcmp(cmd, "bind_named") == 0) + status = exec_command_bind_named(scan_state, active_branch, cmd); else if (strcmp(cmd, "C") == 0) status = exec_command_C(scan_state, active_branch); else if (strcmp(cmd, "c") == 0 || strcmp(cmd, "connect") == 0) status = exec_command_connect(scan_state, active_branch); else if (strcmp(cmd, "cd") == 0) status = exec_command_cd(scan_state, active_branch, cmd); + else if (strcmp(cmd, "close") == 0) + status = exec_command_close(scan_state, active_branch, cmd); else if (strcmp(cmd, "conninfo") == 0) status = exec_command_conninfo(scan_state, active_branch); else if (pg_strcasecmp(cmd, "copy") == 0) @@ -379,6 +389,8 @@ exec_command(const char *cmd, else if (strcmp(cmd, "p") == 0 || strcmp(cmd, "print") == 0) status = exec_command_print(scan_state, active_branch, query_buf, previous_buf); + else if (strcmp(cmd, "parse") == 0) + 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, "prompt") == 0) @@ -472,6 +484,7 @@ exec_command_bind(PsqlScanState scan_state, bool active_branch) int nalloc = 0; pset.bind_params = NULL; + pset.stmtName = NULL; while ((opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false))) { @@ -485,7 +498,57 @@ exec_command_bind(PsqlScanState scan_state, bool active_branch) } pset.bind_nparams = nparams; - pset.bind_flag = true; + pset.send_mode = PSQL_SEND_EXTENDED_QUERY_PARAMS; + } + else + ignore_slash_options(scan_state); + + return status; +} + +/* + * \bind_named -- set query parameters for an existing prepared statement + */ +static backslashResult +exec_command_bind_named(PsqlScanState scan_state, bool active_branch, + const char *cmd) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) + { + char *opt; + int nparams = 0; + int nalloc = 0; + + pset.bind_params = NULL; + pset.stmtName = NULL; + + /* 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.stmtName = opt; + pset.send_mode = PSQL_SEND_EXTENDED_QUERY_PREPARED; + + /* set of parameters */ + while ((opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false))) + { + nparams++; + if (nparams > nalloc) + { + nalloc = nalloc ? nalloc * 2 : 1; + pset.bind_params = pg_realloc_array(pset.bind_params, char *, nalloc); + } + pset.bind_params[nparams - 1] = opt; + } + pset.bind_nparams = nparams; + } } else ignore_slash_options(scan_state); @@ -643,6 +706,38 @@ exec_command_cd(PsqlScanState scan_state, bool active_branch, const char *cmd) return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; } +/* + * \close -- close a previously prepared statement + */ +static backslashResult +exec_command_close(PsqlScanState scan_state, bool active_branch, const char *cmd) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) + { + char *opt = psql_scan_slash_option(scan_state, + OT_NORMAL, NULL, false); + + pset.stmtName = NULL; + if (!opt) + { + pg_log_error("\\%s: missing required argument", cmd); + status = PSQL_CMD_ERROR; + } + else + { + pset.stmtName = opt; + pset.send_mode = PSQL_SEND_EXTENDED_CLOSE; + status = PSQL_CMD_SEND; + } + } + else + ignore_slash_options(scan_state); + + return status; +} + /* * \conninfo -- display information about the current connection */ @@ -2096,6 +2191,39 @@ exec_command_print(PsqlScanState scan_state, bool active_branch, return PSQL_CMD_SKIP_LINE; } +/* + * \parse -- parse query + */ +static backslashResult +exec_command_parse(PsqlScanState scan_state, bool active_branch, + const char *cmd) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) + { + char *opt = psql_scan_slash_option(scan_state, + OT_NORMAL, NULL, false); + + pset.stmtName = NULL; + if (!opt) + { + pg_log_error("\\%s: missing required argument", cmd); + status = PSQL_CMD_ERROR; + } + else + { + pset.stmtName = opt; + pset.send_mode = PSQL_SEND_EXTENDED_PARSE; + status = PSQL_CMD_SEND; + } + } + else + ignore_slash_options(scan_state); + + return status; +} + /* * \password -- set user password */ diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index fe8e049c4c..be265aa05a 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -1274,15 +1274,28 @@ sendquery_cleanup: pset.gsavepopt = NULL; } - /* clean up after \bind */ - if (pset.bind_flag) + /* clean up after extended protocol queries */ + switch (pset.send_mode) { - for (i = 0; i < pset.bind_nparams; i++) - free(pset.bind_params[i]); - free(pset.bind_params); - pset.bind_params = NULL; - pset.bind_flag = false; + case PSQL_SEND_EXTENDED_CLOSE: /* \close */ + free(pset.stmtName); + break; + case PSQL_SEND_EXTENDED_PARSE: /* \parse */ + free(pset.stmtName); + break; + case PSQL_SEND_EXTENDED_QUERY_PARAMS: /* \bind */ + case PSQL_SEND_EXTENDED_QUERY_PREPARED: /* \bind_named */ + for (i = 0; i < pset.bind_nparams; i++) + free(pset.bind_params[i]); + free(pset.bind_params); + free(pset.stmtName); + pset.bind_params = NULL; + break; + case PSQL_SEND_QUERY: + break; } + pset.stmtName = NULL; + pset.send_mode = PSQL_SEND_QUERY; /* reset \gset trigger */ if (pset.gset_prefix) @@ -1456,7 +1469,7 @@ ExecQueryAndProcessResults(const char *query, const printQueryOpt *opt, FILE *printQueryFout) { bool timing = pset.timing; - bool success; + bool success = false; bool return_early = false; instr_time before, after; @@ -1469,10 +1482,32 @@ ExecQueryAndProcessResults(const char *query, else INSTR_TIME_SET_ZERO(before); - if (pset.bind_flag) - success = PQsendQueryParams(pset.db, query, pset.bind_nparams, NULL, (const char *const *) pset.bind_params, NULL, NULL, 0); - else - success = PQsendQuery(pset.db, query); + switch (pset.send_mode) + { + case PSQL_SEND_EXTENDED_CLOSE: + success = PQsendClosePrepared(pset.db, pset.stmtName); + break; + case PSQL_SEND_EXTENDED_PARSE: + success = PQsendPrepare(pset.db, pset.stmtName, query, 0, NULL); + break; + case PSQL_SEND_EXTENDED_QUERY_PARAMS: + Assert(pset.stmtName == NULL); + success = PQsendQueryParams(pset.db, query, + pset.bind_nparams, NULL, + (const char *const *) pset.bind_params, + NULL, NULL, 0); + break; + case PSQL_SEND_EXTENDED_QUERY_PREPARED: + Assert(pset.stmtName != NULL); + success = PQsendQueryPrepared(pset.db, pset.stmtName, + pset.bind_nparams, + (const char *const *) pset.bind_params, + NULL, NULL, 0); + break; + case PSQL_SEND_QUERY: + success = PQsendQuery(pset.db, query); + break; + } if (!success) { diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 6f58a11074..19d20c5878 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -165,6 +165,9 @@ slashUsage(unsigned short int pager) HELP0("General\n"); HELP0(" \\bind [PARAM]... set query parameters\n"); + HELP0(" \\bind_named STMT_NAME [PARAM]...\n" + " set query parameters for an existing prepared statement\n"); + HELP0(" \\close STMT_NAME close an existing prepared statement\n"); HELP0(" \\copyright show PostgreSQL usage and distribution terms\n"); HELP0(" \\crosstabview [COLUMNS] execute query and display result in crosstab\n"); HELP0(" \\errverbose show most recent error message at maximum verbosity\n"); @@ -312,6 +315,7 @@ slashUsage(unsigned short int pager) " connect to new database (currently no connection)\n"); HELP0(" \\conninfo display information about current connection\n"); HELP0(" \\encoding [ENCODING] show or set client encoding\n"); + HELP0(" \\parse STMT_NAME create a prepared statement\n"); HELP0(" \\password [USERNAME] securely change the password for a user\n"); HELP0("\n"); diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h index 505f99d8e4..a22de8ef78 100644 --- a/src/bin/psql/settings.h +++ b/src/bin/psql/settings.h @@ -62,6 +62,15 @@ typedef enum PSQL_COMP_CASE_LOWER, } PSQL_COMP_CASE; +typedef enum +{ + PSQL_SEND_QUERY, + PSQL_SEND_EXTENDED_CLOSE, + PSQL_SEND_EXTENDED_PARSE, + PSQL_SEND_EXTENDED_QUERY_PARAMS, + PSQL_SEND_EXTENDED_QUERY_PREPARED, +} PSQL_SEND_MODE; + typedef enum { hctl_none = 0, @@ -96,10 +105,12 @@ typedef struct _psqlSettings char *gset_prefix; /* one-shot prefix argument for \gset */ bool gdesc_flag; /* one-shot request to describe query result */ bool gexec_flag; /* one-shot request to execute query result */ - bool bind_flag; /* one-shot request to use extended query - * protocol */ + PSQL_SEND_MODE send_mode; /* one-shot request to send query with normal + * or extended query protocol */ int bind_nparams; /* number of parameters */ char **bind_params; /* parameters for extended query protocol call */ + char *stmtName; /* prepared statement name used for extended + * query protocol commands */ bool crosstab_flag; /* one-shot request to crosstab result */ char *ctv_args[4]; /* \crosstabview arguments */ diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 024469474d..0d25981253 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -1713,8 +1713,8 @@ psql_completion(const char *text, int start, int end) /* psql's backslash commands. */ static const char *const backslash_commands[] = { "\\a", - "\\bind", - "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", + "\\bind", "\\bind_named", + "\\connect", "\\conninfo", "\\C", "\\cd", "\\close", "\\copy", "\\copyright", "\\crosstabview", "\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp", "\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD", @@ -1731,7 +1731,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", - "\\password", "\\print", "\\prompt", "\\pset", + "\\parse", "\\password", "\\print", "\\prompt", "\\pset", "\\qecho", "\\quit", "\\reset", "\\s", "\\set", "\\setenv", "\\sf", "\\sv", diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 3bbe4c5f97..6aeb7cb963 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -98,6 +98,53 @@ two | 2 1 | 2 (1 row) +-- \parse (extended query protocol) +\parse +\parse: missing required argument +SELECT 1 \parse '' +SELECT 2 \parse stmt1 +SELECT $1 \parse stmt2 +SELECT $1, $2 \parse stmt3 +-- \bind_named (extended query protocol) +\bind_named +\bind_named: missing required argument +\bind_named '' \g + ?column? +---------- + 1 +(1 row) + +\bind_named stmt1 \g + ?column? +---------- + 2 +(1 row) + +\bind_named stmt2 'foo' \g + ?column? +---------- + foo +(1 row) + +\bind_named stmt3 'foo' 'bar' \g + ?column? | ?column? +----------+---------- + foo | bar +(1 row) + +-- \close (extended query protocol) +\close +\close: missing required argument +\close '' +\close stmt2 +\close stmt2 +SELECT name, statement FROM pg_prepared_statements ORDER BY name; + name | statement +-------+---------------- + stmt1 | SELECT 2 + stmt3 | SELECT $1, $2 +(2 rows) + -- \bind (extended query protocol) SELECT 1 \bind \g ?column? @@ -129,6 +176,11 @@ ERROR: cannot insert multiple commands into a prepared statement -- bind error SELECT $1, $2 \bind 'foo' \g ERROR: bind message supplies 1 parameters, but prepared statement "" requires 2 +-- bind_named error +\bind_named stmt2 'baz' \g +ERROR: prepared statement "stmt2" does not exist +\bind_named stmt3 'baz' \g +ERROR: bind message supplies 1 parameters, but prepared statement "stmt3" requires 2 -- \gset select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_ \echo :pref01_test01 :pref01_test02 :pref01_test03 @@ -4507,9 +4559,11 @@ bar 'bar' "bar" \pset fieldsep | `nosuchcommand` :foo :'foo' :"foo" \a SELECT $1 \bind 1 \g + \bind_named stmt1 1 2 \g \C arg1 \c arg1 arg2 arg3 arg4 \cd arg1 + \close stmt1 \conninfo \copy arg1 arg2 arg3 arg4 arg5 arg6 \copyright @@ -4538,6 +4592,7 @@ invalid command \lo \lo_list \o arg1 \p + SELECT 1 \parse \password arg1 \prompt arg1 arg2 \pset arg1 arg2 diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index 3b3c6f6e29..0a2f8b4692 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -45,8 +45,28 @@ SELECT 1 as one, 2 as two \g (format=csv csv_fieldsep='\t') SELECT 1 as one, 2 as two \gx (title='foo bar') \g --- \bind (extended query protocol) +-- \parse (extended query protocol) +\parse +SELECT 1 \parse '' +SELECT 2 \parse stmt1 +SELECT $1 \parse stmt2 +SELECT $1, $2 \parse stmt3 +-- \bind_named (extended query protocol) +\bind_named +\bind_named '' \g +\bind_named stmt1 \g +\bind_named stmt2 'foo' \g +\bind_named stmt3 'foo' 'bar' \g + +-- \close (extended query protocol) +\close +\close '' +\close stmt2 +\close stmt2 +SELECT name, statement FROM pg_prepared_statements ORDER BY name; + +-- \bind (extended query protocol) SELECT 1 \bind \g SELECT $1 \bind 'foo' \g SELECT $1, $2 \bind 'foo' 'bar' \g @@ -58,6 +78,9 @@ SELECT foo \bind \g SELECT 1 \; SELECT 2 \bind \g -- bind error SELECT $1, $2 \bind 'foo' \g +-- bind_named error +\bind_named stmt2 'baz' \g +\bind_named stmt3 'baz' \g -- \gset @@ -990,9 +1013,11 @@ select \if false \\ (bogus \else \\ 42 \endif \\ forty_two; \pset fieldsep | `nosuchcommand` :foo :'foo' :"foo" \a SELECT $1 \bind 1 \g + \bind_named stmt1 1 2 \g \C arg1 \c arg1 arg2 arg3 arg4 \cd arg1 + \close stmt1 \conninfo \copy arg1 arg2 arg3 arg4 arg5 arg6 \copyright @@ -1020,6 +1045,7 @@ select \if false \\ (bogus \else \\ 42 \endif \\ forty_two; \lo_list \o arg1 \p + SELECT 1 \parse \password arg1 \prompt arg1 arg2 \pset arg1 arg2 diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 6d424c8918..3f3a8f2634 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1958,6 +1958,7 @@ PSQL_COMP_CASE PSQL_ECHO PSQL_ECHO_HIDDEN PSQL_ERROR_ROLLBACK +PSQL_SEND_MODE PTEntryArray PTIterationArray PTOKEN_PRIVILEGES