Add support for \aset in pgbench
authorMichael Paquier <michael@paquier.xyz>
Fri, 3 Apr 2020 02:45:15 +0000 (11:45 +0900)
committerMichael Paquier <michael@paquier.xyz>
Fri, 3 Apr 2020 02:45:15 +0000 (11:45 +0900)
This option is similar to \gset, except that it is able to store all
results from combined SQL queries into separate variables.  If a query
returns multiple rows, the last result is stored and if a query returns
no rows, nothing is stored.

While on it, add a TAP test for \gset to check for a failure when a
query returns multiple rows.

Author: Fabien Coelho
Reviewed-by: Ibrar Ahmed, Michael Paquier
Discussion: https://postgr.es/m/alpine.DEB.2.21.1904081914200.2529@lancre

doc/src/sgml/ref/pgbench.sgml
src/bin/pgbench/pgbench.c
src/bin/pgbench/t/001_pgbench_with_server.pl

index 41b3880c91f7ce16a70d2c998722b8451d922947..58a2aa3bf200af91511d69a3e2f83278504fe8ee 100644 (file)
@@ -1057,18 +1057,29 @@ pgbench <optional> <replaceable>options</replaceable> </optional> <replaceable>d
    <varlistentry id='pgbench-metacommand-gset'>
     <term>
      <literal>\gset [<replaceable>prefix</replaceable>]</literal>
+     <literal>\aset [<replaceable>prefix</replaceable>]</literal>
     </term>
 
     <listitem>
      <para>
-      This command may be used to end SQL queries, taking the place of the
+      These commands may be used to end SQL queries, taking the place of the
       terminating semicolon (<literal>;</literal>).
      </para>
 
      <para>
-      When this command is used, the preceding SQL query is expected to
-      return one row, the columns of which are stored into variables named after
-      column names, and prefixed with <replaceable>prefix</replaceable> if provided.
+      When the <literal>\gset</literal> command is used, the preceding SQL query is
+      expected to return one row, the columns of which are stored into variables
+      named after column names, and prefixed with <replaceable>prefix</replaceable>
+      if provided.
+     </para>
+
+     <para>
+      When the <literal>\aset</literal> command is used, all combined SQL queries
+      (separated by <literal>\;</literal>) have their columns stored into variables
+      named after column names, and prefixed with <replaceable>prefix</replaceable>
+      if provided. If a query returns no row, no assignment is made and the variable
+      can be tested for existence to detect this. If a query returns more than one
+      row, the last value is kept.
      </para>
 
      <para>
@@ -1077,6 +1088,8 @@ pgbench <optional> <replaceable>options</replaceable> </optional> <replaceable>d
       <replaceable>p_two</replaceable> and <replaceable>p_three</replaceable>
       with integers from the third query.
       The result of the second query is discarded.
+      The result of the two last combined queries are stored in variables
+      <replaceable>four</replaceable> and <replaceable>five</replaceable>.
 <programlisting>
 UPDATE pgbench_accounts
   SET abalance = abalance + :delta
@@ -1085,6 +1098,7 @@ UPDATE pgbench_accounts
 -- compound of two queries
 SELECT 1 \;
 SELECT 2 AS two, 3 AS three \gset p_
+SELECT 4 AS four \; SELECT 5 AS five \aset
 </programlisting>
      </para>
     </listitem>
index b8864c6ae53f056200c0fad0da128d8407f482bb..e99af8016752dccbe73a7bf427a93351c112f1e5 100644 (file)
@@ -480,6 +480,7 @@ typedef enum MetaCommand
        META_SHELL,                                     /* \shell */
        META_SLEEP,                                     /* \sleep */
        META_GSET,                                      /* \gset */
+       META_ASET,                                      /* \aset */
        META_IF,                                        /* \if */
        META_ELIF,                                      /* \elif */
        META_ELSE,                                      /* \else */
@@ -504,14 +505,16 @@ static const char *QUERYMODE[] = {"simple", "extended", "prepared"};
  *                             not applied.
  * first_line  A short, single-line extract of 'lines', for error reporting.
  * type                        SQL_COMMAND or META_COMMAND
- * meta                        The type of meta-command, or META_NONE if command is SQL
+ * meta                        The type of meta-command, with META_NONE/GSET/ASET if command
+ *                             is SQL.
  * argc                        Number of arguments of the command, 0 if not yet processed.
  * argv                        Command arguments, the first of which is the command or SQL
  *                             string itself.  For SQL commands, after post-processing
  *                             argv[0] is the same as 'lines' with variables substituted.
- * varprefix   SQL commands terminated with \gset have this set
+ * varprefix   SQL commands terminated with \gset or \aset have this set
  *                             to a non NULL value.  If nonempty, it's used to prefix the
  *                             variable name that receives the value.
+ * aset                        do gset on all possible queries of a combined query (\;).
  * expr                        Parsed expression, if needed.
  * stats               Time spent in this command.
  */
@@ -2489,6 +2492,8 @@ getMetaCommand(const char *cmd)
                mc = META_ENDIF;
        else if (pg_strcasecmp(cmd, "gset") == 0)
                mc = META_GSET;
+       else if (pg_strcasecmp(cmd, "aset") == 0)
+               mc = META_ASET;
        else
                mc = META_NONE;
        return mc;
@@ -2711,17 +2716,25 @@ sendCommand(CState *st, Command *command)
  * Process query response from the backend.
  *
  * If varprefix is not NULL, it's the variable name prefix where to store
- * the results of the *last* command.
+ * the results of the *last* command (META_GSET) or *all* commands
+ * (META_ASET).
  *
  * Returns true if everything is A-OK, false if any error occurs.
  */
 static bool
-readCommandResponse(CState *st, char *varprefix)
+readCommandResponse(CState *st, MetaCommand meta, char *varprefix)
 {
        PGresult   *res;
        PGresult   *next_res;
        int                     qrynum = 0;
 
+       /*
+        * varprefix should be set only with \gset or \aset, and SQL commands do
+        * not need it.
+        */
+       Assert((meta == META_NONE && varprefix == NULL) ||
+                  ((meta == META_GSET || meta == META_ASET) && varprefix != NULL));
+
        res = PQgetResult(st->con);
 
        while (res != NULL)
@@ -2736,7 +2749,7 @@ readCommandResponse(CState *st, char *varprefix)
                {
                        case PGRES_COMMAND_OK:  /* non-SELECT commands */
                        case PGRES_EMPTY_QUERY: /* may be used for testing no-op overhead */
-                               if (is_last && varprefix != NULL)
+                               if (is_last && meta == META_GSET)
                                {
                                        pg_log_error("client %d script %d command %d query %d: expected one row, got %d",
                                                                 st->id, st->use_file, st->command, qrynum, 0);
@@ -2745,14 +2758,22 @@ readCommandResponse(CState *st, char *varprefix)
                                break;
 
                        case PGRES_TUPLES_OK:
-                               if (is_last && varprefix != NULL)
+                               if ((is_last && meta == META_GSET) || meta == META_ASET)
                                {
-                                       if (PQntuples(res) != 1)
+                                       int                     ntuples = PQntuples(res);
+
+                                       if (meta == META_GSET && ntuples != 1)
                                        {
+                                               /* under \gset, report the error */
                                                pg_log_error("client %d script %d command %d query %d: expected one row, got %d",
                                                                         st->id, st->use_file, st->command, qrynum, PQntuples(res));
                                                goto error;
                                        }
+                                       else if (meta == META_ASET && ntuples <= 0)
+                                       {
+                                               /* coldly skip empty result under \aset */
+                                               break;
+                                       }
 
                                        /* store results into variables */
                                        for (int fld = 0; fld < PQnfields(res); fld++)
@@ -2763,9 +2784,9 @@ readCommandResponse(CState *st, char *varprefix)
                                                if (*varprefix != '\0')
                                                        varname = psprintf("%s%s", varprefix, varname);
 
-                                               /* store result as a string */
-                                               if (!putVariable(st, "gset", varname,
-                                                                                PQgetvalue(res, 0, fld)))
+                                               /* store last row result as a string */
+                                               if (!putVariable(st, meta == META_ASET ? "aset" : "gset", varname,
+                                                                                PQgetvalue(res, ntuples - 1, fld)))
                                                {
                                                        /* internal error */
                                                        pg_log_error("client %d script %d command %d query %d: error storing into variable %s",
@@ -3181,7 +3202,9 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
                                        return;         /* don't have the whole result yet */
 
                                /* store or discard the query results */
-                               if (readCommandResponse(st, sql_script[st->use_file].commands[st->command]->varprefix))
+                               if (readCommandResponse(st,
+                                                                               sql_script[st->use_file].commands[st->command]->meta,
+                                                                               sql_script[st->use_file].commands[st->command]->varprefix))
                                        st->state = CSTATE_END_COMMAND;
                                else
                                        st->state = CSTATE_ABORTED;
@@ -4660,7 +4683,7 @@ process_backslash_command(PsqlScanState sstate, const char *source)
                        syntax_error(source, lineno, my_command->first_line, my_command->argv[0],
                                                 "unexpected argument", NULL, -1);
        }
-       else if (my_command->meta == META_GSET)
+       else if (my_command->meta == META_GSET || my_command->meta == META_ASET)
        {
                if (my_command->argc > 2)
                        syntax_error(source, lineno, my_command->first_line, my_command->argv[0],
@@ -4804,10 +4827,10 @@ ParseScript(const char *script, const char *desc, int weight)
                        if (command)
                        {
                                /*
-                                * If this is gset, merge into the preceding command. (We
-                                * don't use a command slot in this case).
+                                * If this is gset or aset, merge into the preceding command.
+                                * (We don't use a command slot in this case).
                                 */
-                               if (command->meta == META_GSET)
+                               if (command->meta == META_GSET || command->meta == META_ASET)
                                {
                                        Command    *cmd;
 
@@ -4830,6 +4853,9 @@ ParseScript(const char *script, const char *desc, int weight)
                                        else
                                                cmd->varprefix = pg_strdup(command->argv[1]);
 
+                                       /* update the sql command meta */
+                                       cmd->meta = command->meta;
+
                                        /* cleanup unused command */
                                        free_command(command);
 
index b85a3ac32dd3b6b52ccc77874c34ca509b30670b..e85728c3790f5d63c256e5637060231ac7f18166 100644 (file)
@@ -699,6 +699,51 @@ SELECT 0 AS i4, 4 AS i4 \gset
 -- work on the last SQL command under \;
 \; \; SELECT 0 AS i5 \; SELECT 5 AS i5 \; \; \gset
 \set i debug(:i5)
+}
+       });
+# \gset cannot accept more than one row, causing command to fail.
+pgbench(
+       '-t 1', 2,
+       [ qr{type: .*/001_pgbench_gset_two_rows}, qr{processed: 0/1} ],
+       [qr{expected one row, got 2\b}],
+       'pgbench gset command with two rows',
+       {
+               '001_pgbench_gset_two_rows' => q{
+SELECT 5432 AS fail UNION SELECT 5433 ORDER BY 1 \gset
+}
+       });
+
+# working \aset
+# Valid cases.
+pgbench(
+       '-t 1', 0,
+       [ qr{type: .*/001_pgbench_aset}, qr{processed: 1/1} ],
+       [ qr{command=3.: int 8\b},       qr{command=4.: int 7\b} ],
+       'pgbench aset command',
+       {
+               '001_pgbench_aset' => q{
+-- test aset, which applies to a combined query
+\; SELECT 6 AS i6 \; SELECT 7 AS i7 \; \aset
+-- unless it returns more than one row, last is kept
+SELECT 8 AS i6 UNION SELECT 9 ORDER BY 1 DESC \aset
+\set i debug(:i6)
+\set i debug(:i7)
+}
+       });
+# Empty result set with \aset, causing command to fail.
+pgbench(
+       '-t 1', 2,
+       [ qr{type: .*/001_pgbench_aset_empty}, qr{processed: 0/1} ],
+       [
+               qr{undefined variable \"i8\"},
+               qr{evaluation of meta-command failed\b}
+       ],
+       'pgbench aset command with empty result',
+       {
+               '001_pgbench_aset_empty' => q{
+-- empty result
+\; SELECT 5432 AS i8 WHERE FALSE \; \aset
+\set i debug(:i8)
 }
        });