psql: Add command to use extended query protocol
authorPeter Eisentraut <peter@eisentraut.org>
Tue, 15 Nov 2022 12:50:27 +0000 (13:50 +0100)
committerPeter Eisentraut <peter@eisentraut.org>
Tue, 15 Nov 2022 13:27:46 +0000 (14:27 +0100)
This adds a new psql command \bind that sets query parameters and
causes the next query to be sent using the extended query protocol.
Example:

    SELECT $1, $2 \bind 'foo' 'bar' \g

This may be useful for psql scripting, but one of the main purposes is
also to be able to test various aspects of the extended query protocol
from psql and to write tests more easily.

Reviewed-by: Corey Huinker <corey.huinker@gmail.com>
Discussion: https://www.postgresql.org/message-id/flat/e8dd1cd5-0e04-3598-0518-a605159fe314@enterprisedb.com

doc/src/sgml/ref/psql-ref.sgml
src/bin/psql/command.c
src/bin/psql/common.c
src/bin/psql/help.c
src/bin/psql/settings.h
src/bin/psql/tab-complete.c
src/test/regress/expected/psql.out
src/test/regress/sql/psql.sql

index 9494f28063add052fd4b91fdf61f9559f7add72b..d31cf17f5def775300bd8d201b62188b4c8b8af6 100644 (file)
@@ -879,6 +879,42 @@ testdb=&gt;
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+       <term><literal>\bind</literal> [ <replaceable class="parameter">parameter</replaceable> ] ... </term>
+
+       <listitem>
+        <para>
+         Sets query parameters for the next query execution, with the
+         specified parameters passed for any parameter placeholders
+         (<literal>$1</literal> etc.).
+        </para>
+
+        <para>
+         Example:
+<programlisting>
+INSERT INTO tbl1 VALUES ($1, $2) \bind 'first value' 'second value' \g
+</programlisting>
+        </para>
+
+        <para>
+         This also works for query-execution commands besides
+         <literal>\g</literal>, such as <literal>\gx</literal> and
+         <literal>\gset</literal>.
+        </para>
+
+        <para>
+         This command causes the extended query protocol (see <xref
+         linkend="protocol-query-concepts"/>) to be used, unlike normal
+         <application>psql</application> operation, which uses the simple
+         query protocol.  So this command can be useful to test the extended
+         query protocol from psql.  (The extended query protocol is used even
+         if the query has no parameters and this command specifies zero
+         parameters.)  This command affects only the next query executed; all
+         subsequent queries will use the simple query protocol by default.
+        </para>
+       </listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><literal>\c</literal> or <literal>\connect [ -reuse-previous=<replaceable class="parameter">on|off</replaceable> ] [ <replaceable class="parameter">dbname</replaceable> [ <replaceable class="parameter">username</replaceable> ] [ <replaceable class="parameter">host</replaceable> ] [ <replaceable class="parameter">port</replaceable> ] | <replaceable class="parameter">conninfo</replaceable> ]</literal></term>
         <listitem>
index ab613dd49e0ab0555650f7e2e512a9b9e2c82ef3..3b06169ba0dcab5fb032a998759877eeb925e3a1 100644 (file)
@@ -63,6 +63,7 @@ static backslashResult exec_command(const char *cmd,
                                    PQExpBuffer query_buf,
                                    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_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,
@@ -308,6 +309,8 @@ exec_command(const char *cmd,
 
    if (strcmp(cmd, "a") == 0)
        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, "C") == 0)
        status = exec_command_C(scan_state, active_branch);
    else if (strcmp(cmd, "c") == 0 || strcmp(cmd, "connect") == 0)
@@ -453,6 +456,40 @@ exec_command_a(PsqlScanState scan_state, bool active_branch)
    return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR;
 }
 
+/*
+ * \bind -- set query parameters
+ */
+static backslashResult
+exec_command_bind(PsqlScanState scan_state, bool active_branch)
+{
+   backslashResult status = PSQL_CMD_SKIP_LINE;
+
+   if (active_branch)
+   {
+       char       *opt;
+       int         nparams = 0;
+       int         nalloc = 0;
+
+       pset.bind_params = NULL;
+
+       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] = pg_strdup(opt);
+       }
+
+       pset.bind_nparams = nparams;
+       pset.bind_flag = true;
+   }
+
+   return status;
+}
+
 /*
  * \C -- override table title (formerly change HTML caption)
  */
index 864f195992f52c0e7330fe9faa190f1576fceee6..b989d792aa753670c2f326e45a6fc023159be15b 100644 (file)
@@ -1220,6 +1220,16 @@ sendquery_cleanup:
        pset.gsavepopt = NULL;
    }
 
+   /* clean up after \bind */
+   if (pset.bind_flag)
+   {
+       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;
+   }
+
    /* reset \gset trigger */
    if (pset.gset_prefix)
    {
@@ -1397,7 +1407,10 @@ ExecQueryAndProcessResults(const char *query,
    if (timing)
        INSTR_TIME_SET_CURRENT(before);
 
-   success = PQsendQuery(pset.db, query);
+   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);
 
    if (!success)
    {
index f8ce1a07060d74a2a99b649f1e092354b2c2e5ea..b4e0ec2687fde3d1a7a37b2f8d71e5e912ea18cd 100644 (file)
@@ -189,6 +189,7 @@ slashUsage(unsigned short int pager)
    initPQExpBuffer(&buf);
 
    HELP0("General\n");
+   HELP0("  \\bind [PARAM]...       set query parameters\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");
index 2399cffa3fba86717697fdc69a4978a40c8381ff..3fce71b85fe4e680fa16ca938d277a8ad890a07d 100644 (file)
@@ -96,6 +96,9 @@ 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 */
+   int         bind_nparams;   /* number of parameters */
+   char      **bind_params;    /* parameters for extended query protocol call */
    bool        crosstab_flag;  /* one-shot request to crosstab result */
    char       *ctv_args[4];    /* \crosstabview arguments */
 
index 7b73886ce19c46f1b5f505457bf4bb9194794616..a0e26bc295894ccbb90c0c1d94e57592128a2a5c 100644 (file)
@@ -1680,6 +1680,7 @@ 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",
        "\\copyright", "\\crosstabview",
        "\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
index a7f5700edc121d2346d8c7f165ed84294083b1e3..5bdae290dcece0637815a2036b39f87c20f28df4 100644 (file)
@@ -98,6 +98,37 @@ two | 2
    1 |   2
 (1 row)
 
+-- \bind (extended query protocol)
+SELECT 1 \bind \g
+ ?column? 
+----------
+        1
+(1 row)
+
+SELECT $1 \bind 'foo' \g
+ ?column? 
+----------
+ foo
+(1 row)
+
+SELECT $1, $2 \bind 'foo' 'bar' \g
+ ?column? | ?column? 
+----------+----------
+ foo      | bar
+(1 row)
+
+-- errors
+-- parse error
+SELECT foo \bind \g
+ERROR:  column "foo" does not exist
+LINE 1: SELECT foo 
+               ^
+-- tcop error
+SELECT 1 \; SELECT 2 \bind \g
+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
 -- \gset
 select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_
 \echo :pref01_test01 :pref01_test02 :pref01_test03
index 1149c6a839ef756d6fbde6542275ffeb06e1e16c..8732017e51e9cf0509afb662b230a10360fc4a02 100644 (file)
@@ -45,6 +45,20 @@ 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)
+
+SELECT 1 \bind \g
+SELECT $1 \bind 'foo' \g
+SELECT $1, $2 \bind 'foo' 'bar' \g
+
+-- errors
+-- parse error
+SELECT foo \bind \g
+-- tcop error
+SELECT 1 \; SELECT 2 \bind \g
+-- bind error
+SELECT $1, $2 \bind 'foo' \g
+
 -- \gset
 
 select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_