diff --git a/doc/src/sgml/ref/pgbench.sgml b/doc/src/sgml/ref/pgbench.sgml
index b5e3a62a339..ca160881188 100644
--- a/doc/src/sgml/ref/pgbench.sgml
+++ b/doc/src/sgml/ref/pgbench.sgml
@@ -954,6 +954,91 @@ pgbench options d
+
+
+ \cset [prefix]
+
+
+
+
+ This command may be used to end SQL queries, replacing an embedded
+ semicolon (\;) within a compound SQL command.
+
+
+
+ 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 prefix if provided.
+
+
+
+ The following example sends four queries as one compound SQL command,
+ inducing one message sent at the protocol level.
+ The result of the first query is stored into variable one,
+ the results of the third query are stored into variables z_three
+ and z_four,
+ whereas the results of the other queries are discarded.
+
+-- compound of four queries
+SELECT 1 AS one \cset
+SELECT 2 AS two \;
+SELECT 3 AS three, 4 AS four \cset z_
+SELECT 5;
+
+
+
+
+
+ \cset does not work when empty SQL queries appear
+ within a compound SQL command.
+
+
+
+
+
+
+
+ \gset [prefix]
+
+
+
+
+ This command may be used to end SQL queries, replacing a final semicolon
+ (;).
+
+
+
+ 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 prefix if provided.
+
+
+
+ The following example puts the final account balance from the first query
+ into variable abalance, and fills variables
+ p_two and p_three
+ with integers from the last query.
+ The result of the second query is discarded.
+
+UPDATE pgbench_accounts
+ SET abalance = abalance + :delta
+ WHERE aid = :aid
+ RETURNING abalance \gset
+-- compound of two queries
+SELECT 1 \;
+SELECT 2 AS two, 3 AS three \gset p_
+
+
+
+
+
+ \gset does not work when empty SQL queries appear
+ within a compound SQL command.
+
+
+
+
+
\if expression
\elif expression
diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 649297ae4f0..7b5bc449fcd 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -482,6 +482,8 @@ typedef enum MetaCommand
META_SETSHELL, /* \setshell */
META_SHELL, /* \shell */
META_SLEEP, /* \sleep */
+ META_CSET, /* \cset */
+ META_GSET, /* \gset */
META_IF, /* \if */
META_ELIF, /* \elif */
META_ELSE, /* \else */
@@ -499,16 +501,39 @@ typedef enum QueryMode
static QueryMode querymode = QUERY_SIMPLE;
static const char *QUERYMODE[] = {"simple", "extended", "prepared"};
-typedef struct
+/*
+ * struct Command represents one command in a script.
+ *
+ * lines The raw, possibly multi-line command text. Variable substitution
+ * 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
+ * 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.
+ * nqueries In a multi-command SQL line, the number of queries.
+ * varprefix SQL commands terminated with \gset or \cset have this set
+ * to a non NULL value. If nonempty, it's used to prefix the
+ * variable name that receives the value.
+ * varprefix_max Allocated size of the varprefix array.
+ * expr Parsed expression, if needed.
+ * stats Time spent in this command.
+ */
+typedef struct Command
{
- char *line; /* text of command line */
- int command_num; /* unique index of this Command struct */
- int type; /* command type (SQL_COMMAND or META_COMMAND) */
- MetaCommand meta; /* meta command identifier, or META_NONE */
- int argc; /* number of command words */
- char *argv[MAX_ARGS]; /* command word list */
- PgBenchExpr *expr; /* parsed expression, if needed */
- SimpleStats stats; /* time spent in this command */
+ PQExpBufferData lines;
+ char *first_line;
+ int type;
+ MetaCommand meta;
+ int argc;
+ char *argv[MAX_ARGS];
+ int nqueries;
+ char **varprefix;
+ int varprefix_max;
+ PgBenchExpr *expr;
+ SimpleStats stats;
} Command;
typedef struct ParsedScript
@@ -521,7 +546,6 @@ typedef struct ParsedScript
static ParsedScript sql_script[MAX_SCRIPTS]; /* SQL script files */
static int num_scripts; /* number of scripts in sql_script[] */
-static int num_commands = 0; /* total number of Command structs */
static int64 total_weight = 0;
static int debug = 0; /* debug flag */
@@ -587,6 +611,7 @@ static void doLog(TState *thread, CState *st,
static void processXactStats(TState *thread, CState *st, instr_time *now,
bool skipped, StatsData *agg);
static void pgbench_error(const char *fmt,...) pg_attribute_printf(1, 2);
+static void allocate_command_varprefix(Command *cmd, int totalqueries);
static void addScript(ParsedScript script);
static void *threadRun(void *arg);
static void finishCon(CState *st);
@@ -2569,6 +2594,10 @@ getMetaCommand(const char *cmd)
mc = META_ELSE;
else if (pg_strcasecmp(cmd, "endif") == 0)
mc = META_ENDIF;
+ else if (pg_strcasecmp(cmd, "cset") == 0)
+ mc = META_CSET;
+ else if (pg_strcasecmp(cmd, "gset") == 0)
+ mc = META_GSET;
else
mc = META_NONE;
return mc;
@@ -2796,6 +2825,109 @@ sendCommand(CState *st, Command *command)
return true;
}
+/*
+ * Process query response from the backend.
+ *
+ * If varprefix is not NULL, it's the array of variable prefix names where to
+ * store the results.
+ *
+ * Returns true if everything is A-OK, false if any error occurs.
+ */
+static bool
+readCommandResponse(CState *st, char **varprefix)
+{
+ PGresult *res;
+ int qrynum = 0;
+
+ while ((res = PQgetResult(st->con)) != NULL)
+ {
+ switch (PQresultStatus(res))
+ {
+ case PGRES_COMMAND_OK: /* non-SELECT commands */
+ case PGRES_EMPTY_QUERY: /* may be used for testing no-op overhead */
+ if (varprefix && varprefix[qrynum] != NULL)
+ {
+ fprintf(stderr,
+ "client %d script %d command %d query %d: expected one row, got %d\n",
+ st->id, st->use_file, st->command, qrynum, 0);
+ st->ecnt++;
+ return false;
+ }
+ break;
+
+ case PGRES_TUPLES_OK:
+ if (varprefix && varprefix[qrynum] != NULL)
+ {
+ if (PQntuples(res) != 1)
+ {
+ fprintf(stderr,
+ "client %d script %d command %d query %d: expected one row, got %d\n",
+ st->id, st->use_file, st->command, qrynum, PQntuples(res));
+ st->ecnt++;
+ PQclear(res);
+ discard_response(st);
+ return false;
+ }
+
+ /* store results into variables */
+ for (int fld = 0; fld < PQnfields(res); fld++)
+ {
+ char *varname = PQfname(res, fld);
+
+ /* allocate varname only if necessary, freed below */
+ if (*varprefix[qrynum] != '\0')
+ varname =
+ psprintf("%s%s", varprefix[qrynum], varname);
+
+ /* store result as a string */
+ if (!putVariable(st, "gset", varname,
+ PQgetvalue(res, 0, fld)))
+ {
+ /* internal error */
+ fprintf(stderr,
+ "client %d script %d command %d query %d: error storing into variable %s\n",
+ st->id, st->use_file, st->command, qrynum,
+ varname);
+ st->ecnt++;
+ PQclear(res);
+ discard_response(st);
+ return false;
+ }
+
+ if (*varprefix[qrynum] != '\0')
+ pg_free(varname);
+ }
+ }
+ /* otherwise the result is simply thrown away by PQclear below */
+ break;
+
+ default:
+ /* anything else is unexpected */
+ fprintf(stderr,
+ "client %d script %d aborted in command %d query %d: %s",
+ st->id, st->use_file, st->command, qrynum,
+ PQerrorMessage(st->con));
+ st->ecnt++;
+ PQclear(res);
+ discard_response(st);
+ return false;
+ }
+
+ PQclear(res);
+ qrynum++;
+ }
+
+ if (qrynum == 0)
+ {
+ fprintf(stderr, "client %d command %d: no results\n", st->id, st->command);
+ st->ecnt++;
+ return false;
+ }
+
+ return true;
+}
+
+
/*
* Parse the argument to a \sleep command, and return the requested amount
* of delay, in microseconds. Returns true on success, false on error.
@@ -2862,8 +2994,6 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
*/
for (;;)
{
- PGresult *res;
-
switch (st->state)
{
/* Select transaction (script) to run. */
@@ -3141,24 +3271,11 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
if (PQisBusy(st->con))
return; /* don't have the whole result yet */
- /* Read and discard the query result */
- res = PQgetResult(st->con);
- switch (PQresultStatus(res))
- {
- case PGRES_COMMAND_OK:
- case PGRES_TUPLES_OK:
- case PGRES_EMPTY_QUERY:
- /* OK */
- PQclear(res);
- discard_response(st);
- st->state = CSTATE_END_COMMAND;
- break;
- default:
- commandFailed(st, "SQL", PQerrorMessage(st->con));
- PQclear(res);
- st->state = CSTATE_ABORTED;
- break;
- }
+ /* store or discard the query results */
+ if (readCommandResponse(st, sql_script[st->use_file].commands[st->command]->varprefix))
+ st->state = CSTATE_END_COMMAND;
+ else
+ st->state = CSTATE_ABORTED;
break;
/*
@@ -3191,7 +3308,7 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
INSTR_TIME_SET_CURRENT_LAZY(now);
- command = sql_script[st->use_file].commands[st->command];
+ command = sql_script[st->use_file].commands[st->command];
/* XXX could use a mutex here, but we choose not to */
addToSimpleStats(&command->stats,
INSTR_TIME_GET_DOUBLE(now) -
@@ -3235,9 +3352,8 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
st->state = CSTATE_CHOOSE_SCRIPT;
/*
- * Ensure that we always return on this point, so as to
- * avoid an infinite loop if the script only contains meta
- * commands.
+ * Ensure that we always return on this point, so as to avoid
+ * an infinite loop if the script only contains meta commands.
*/
return;
@@ -3976,7 +4092,7 @@ runInitSteps(const char *initialize_steps)
/*
* Replace :param with $n throughout the command's SQL text, which
- * is a modifiable string in cmd->argv[0].
+ * is a modifiable string in cmd->lines.
*/
static bool
parseQuery(Command *cmd)
@@ -3984,12 +4100,9 @@ parseQuery(Command *cmd)
char *sql,
*p;
- /* We don't want to scribble on cmd->argv[0] until done */
- sql = pg_strdup(cmd->argv[0]);
-
cmd->argc = 1;
- p = sql;
+ p = sql = pg_strdup(cmd->lines.data);
while ((p = strchr(p, ':')) != NULL)
{
char var[13];
@@ -4009,7 +4122,7 @@ parseQuery(Command *cmd)
if (cmd->argc >= MAX_ARGS)
{
fprintf(stderr, "statement has too many arguments (maximum is %d): %s\n",
- MAX_ARGS - 1, cmd->argv[0]);
+ MAX_ARGS - 1, cmd->lines.data);
pg_free(name);
return false;
}
@@ -4021,7 +4134,7 @@ parseQuery(Command *cmd)
cmd->argc++;
}
- pg_free(cmd->argv[0]);
+ Assert(cmd->argv[0] == NULL);
cmd->argv[0] = sql;
return true;
}
@@ -4081,21 +4194,16 @@ syntax_error(const char *source, int lineno,
}
/*
- * Parse a SQL command; return a Command struct, or NULL if it's a comment
- *
- * On entry, psqlscan.l has collected the command into "buf", so we don't
- * really need to do much here except check for comment and set up a
- * Command struct.
+ * Return a pointer to the start of the SQL command, after skipping over
+ * whitespace and "--" comments.
+ * If the end of the string is reached, return NULL.
*/
-static Command *
-process_sql_command(PQExpBuffer buf, const char *source)
+static char *
+skip_sql_comments(char *sql_command)
{
- Command *my_command;
- char *p;
- char *nlpos;
+ char *p = sql_command;
/* Skip any leading whitespace, as well as "--" style comments */
- p = buf->data;
for (;;)
{
if (isspace((unsigned char) *p))
@@ -4111,41 +4219,153 @@ process_sql_command(PQExpBuffer buf, const char *source)
break;
}
- /* If there's nothing but whitespace and comments, we're done */
+ /* NULL if there's nothing but whitespace and comments */
if (*p == '\0')
return NULL;
+ return p;
+}
+
+/*
+ * Parse a SQL command; return a Command struct, or NULL if it's a comment
+ *
+ * On entry, psqlscan.l has collected the command into "buf", so we don't
+ * really need to do much here except check for comments and set up a Command
+ * struct.
+ */
+static Command *
+create_sql_command(PQExpBuffer buf, const char *source, int numqueries)
+{
+ Command *my_command;
+ char *p = skip_sql_comments(buf->data);
+
+ if (p == NULL)
+ return NULL;
+
/* Allocate and initialize Command structure */
- my_command = (Command *) pg_malloc0(sizeof(Command));
- my_command->command_num = num_commands++;
+ my_command = (Command *) pg_malloc(sizeof(Command));
+ initPQExpBuffer(&my_command->lines);
+ appendPQExpBufferStr(&my_command->lines, p);
+ my_command->first_line = NULL; /* this is set later */
my_command->type = SQL_COMMAND;
my_command->meta = META_NONE;
+ my_command->argc = 0;
+ memset(my_command->argv, 0, sizeof(my_command->argv));
+ my_command->nqueries = numqueries;
+ my_command->varprefix = NULL; /* allocated later, if needed */
+ my_command->varprefix_max = 0;
+ my_command->expr = NULL;
initSimpleStats(&my_command->stats);
- /*
- * Install query text as the sole argv string. If we are using a
- * non-simple query mode, we'll extract parameters from it later.
- */
- my_command->argv[0] = pg_strdup(p);
- my_command->argc = 1;
-
- /*
- * If SQL command is multi-line, we only want to save the first line as
- * the "line" label.
- */
- nlpos = strchr(p, '\n');
- if (nlpos)
- {
- my_command->line = pg_malloc(nlpos - p + 1);
- memcpy(my_command->line, p, nlpos - p);
- my_command->line[nlpos - p] = '\0';
- }
- else
- my_command->line = pg_strdup(p);
-
return my_command;
}
+/* Free a Command structure and associated data */
+static void
+free_command(Command *command)
+{
+ termPQExpBuffer(&command->lines);
+ if (command->first_line)
+ pg_free(command->first_line);
+ if (command->argv)
+ for (int i = 0; i < command->argc; i++)
+ pg_free(command->argv[i]);
+ if (command->varprefix)
+ {
+ for (int i = 0; i < command->varprefix_max; i++)
+ if (command->varprefix[i] &&
+ command->varprefix[i][0] != '\0') /* see ParseScript */
+ pg_free(command->varprefix[i]);
+ pg_free(command->varprefix);
+ }
+ /*
+ * It should also free expr recursively, but this is currently not needed
+ * as only \{g,c}set commands (which do not have an expression) are freed.
+ */
+ pg_free(command);
+}
+
+/*
+ * append "more" text to current compound command which had been interrupted
+ * by \cset.
+ */
+static void
+append_sql_command(Command *my_command, char *more, int numqueries)
+{
+ Assert(my_command->type == SQL_COMMAND && my_command->lines.len > 0);
+
+ more = skip_sql_comments(more);
+ if (more == NULL)
+ return;
+
+ /* append command text, embedding a ';' in place of the \cset */
+ appendPQExpBuffer(&my_command->lines, ";\n%s", more);
+
+ my_command->nqueries += numqueries;
+}
+
+/*
+ * Once an SQL command is fully parsed, possibly by accumulating several
+ * parts, complete other fields of the Command structure.
+ */
+static void
+postprocess_sql_command(Command *my_command)
+{
+ char buffer[128];
+
+ Assert(my_command->type == SQL_COMMAND);
+
+ /* Save the first line for error display. */
+ strlcpy(buffer, my_command->lines.data, sizeof(buffer));
+ buffer[strcspn(buffer, "\n\r")] = '\0';
+ my_command->first_line = pg_strdup(buffer);
+
+ /* parse query if necessary */
+ switch (querymode)
+ {
+ case QUERY_SIMPLE:
+ my_command->argv[0] = my_command->lines.data;
+ my_command->argc++;
+ break;
+ case QUERY_EXTENDED:
+ case QUERY_PREPARED:
+ if (!parseQuery(my_command))
+ exit(1);
+ break;
+ default:
+ exit(1);
+ }
+}
+
+/*
+ * Determine the command's varprefix size needed and allocate memory for it
+ */
+static void
+allocate_command_varprefix(Command *cmd, int totalqueries)
+{
+ int new_max;
+
+ /* sufficient space already allocated? */
+ if (totalqueries <= cmd->varprefix_max)
+ return;
+
+ /* determine the new array size */
+ new_max = Max(cmd->varprefix_max, 2);
+ while (new_max < totalqueries)
+ new_max *= 2;
+
+ /* enlarge the array, zero-initializing the allocated space */
+ if (cmd->varprefix == NULL)
+ cmd->varprefix = pg_malloc0(sizeof(char *) * new_max);
+ else
+ {
+ cmd->varprefix = pg_realloc(cmd->varprefix, sizeof(char *) * new_max);
+ memset(cmd->varprefix + cmd->varprefix_max, 0,
+ sizeof(char *) * (new_max - cmd->varprefix_max));
+ }
+ cmd->varprefix_max = new_max;
+}
+
/*
* Parse a backslash command; return a Command struct, or NULL if comment
*
@@ -4177,7 +4397,6 @@ process_backslash_command(PsqlScanState sstate, const char *source)
/* Allocate and initialize Command structure */
my_command = (Command *) pg_malloc0(sizeof(Command));
- my_command->command_num = num_commands++;
my_command->type = META_COMMAND;
my_command->argc = 0;
initSimpleStats(&my_command->stats);
@@ -4201,7 +4420,7 @@ process_backslash_command(PsqlScanState sstate, const char *source)
if (my_command->meta == META_SET)
{
if (!expr_lex_one_word(sstate, &word_buf, &word_offset))
- syntax_error(source, lineno, my_command->line, my_command->argv[0],
+ syntax_error(source, lineno, my_command->first_line, my_command->argv[0],
"missing argument", NULL, -1);
offsets[j] = word_offset;
@@ -4222,10 +4441,11 @@ process_backslash_command(PsqlScanState sstate, const char *source)
my_command->expr = expr_parse_result;
/* Save line, trimming any trailing newline */
- my_command->line = expr_scanner_get_substring(sstate,
- start_offset,
- expr_scanner_offset(sstate),
- true);
+ my_command->first_line =
+ expr_scanner_get_substring(sstate,
+ start_offset,
+ expr_scanner_offset(sstate),
+ true);
expr_scanner_finish(yyscanner);
@@ -4238,7 +4458,7 @@ process_backslash_command(PsqlScanState sstate, const char *source)
while (expr_lex_one_word(sstate, &word_buf, &word_offset))
{
if (j >= MAX_ARGS)
- syntax_error(source, lineno, my_command->line, my_command->argv[0],
+ syntax_error(source, lineno, my_command->first_line, my_command->argv[0],
"too many arguments", NULL, -1);
offsets[j] = word_offset;
@@ -4247,19 +4467,20 @@ process_backslash_command(PsqlScanState sstate, const char *source)
}
/* Save line, trimming any trailing newline */
- my_command->line = expr_scanner_get_substring(sstate,
- start_offset,
- expr_scanner_offset(sstate),
- true);
+ my_command->first_line =
+ expr_scanner_get_substring(sstate,
+ start_offset,
+ expr_scanner_offset(sstate),
+ true);
if (my_command->meta == META_SLEEP)
{
if (my_command->argc < 2)
- syntax_error(source, lineno, my_command->line, my_command->argv[0],
+ syntax_error(source, lineno, my_command->first_line, my_command->argv[0],
"missing argument", NULL, -1);
if (my_command->argc > 3)
- syntax_error(source, lineno, my_command->line, my_command->argv[0],
+ syntax_error(source, lineno, my_command->first_line, my_command->argv[0],
"too many arguments", NULL,
offsets[3] - start_offset);
@@ -4288,7 +4509,7 @@ process_backslash_command(PsqlScanState sstate, const char *source)
if (pg_strcasecmp(my_command->argv[2], "us") != 0 &&
pg_strcasecmp(my_command->argv[2], "ms") != 0 &&
pg_strcasecmp(my_command->argv[2], "s") != 0)
- syntax_error(source, lineno, my_command->line, my_command->argv[0],
+ syntax_error(source, lineno, my_command->first_line, my_command->argv[0],
"unrecognized time unit, must be us, ms or s",
my_command->argv[2], offsets[2] - start_offset);
}
@@ -4296,25 +4517,31 @@ process_backslash_command(PsqlScanState sstate, const char *source)
else if (my_command->meta == META_SETSHELL)
{
if (my_command->argc < 3)
- syntax_error(source, lineno, my_command->line, my_command->argv[0],
+ syntax_error(source, lineno, my_command->first_line, my_command->argv[0],
"missing argument", NULL, -1);
}
else if (my_command->meta == META_SHELL)
{
if (my_command->argc < 2)
- syntax_error(source, lineno, my_command->line, my_command->argv[0],
+ syntax_error(source, lineno, my_command->first_line, my_command->argv[0],
"missing command", NULL, -1);
}
else if (my_command->meta == META_ELSE || my_command->meta == META_ENDIF)
{
if (my_command->argc != 1)
- syntax_error(source, lineno, my_command->line, my_command->argv[0],
+ syntax_error(source, lineno, my_command->first_line, my_command->argv[0],
"unexpected argument", NULL, -1);
}
+ else if (my_command->meta == META_CSET || my_command->meta == META_GSET)
+ {
+ if (my_command->argc > 2)
+ syntax_error(source, lineno, my_command->first_line, my_command->argv[0],
+ "too many arguments", NULL, -1);
+ }
else
{
/* my_command->meta == META_NONE */
- syntax_error(source, lineno, my_command->line, my_command->argv[0],
+ syntax_error(source, lineno, my_command->first_line, my_command->argv[0],
"invalid command", NULL, -1);
}
@@ -4393,6 +4620,9 @@ ParseScript(const char *script, const char *desc, int weight)
PQExpBufferData line_buf;
int alloc_num;
int index;
+ bool saw_cset = false;
+ int lineno;
+ int start_offset;
#define COMMANDS_ALLOC_NUM 128
alloc_num = COMMANDS_ALLOC_NUM;
@@ -4416,6 +4646,7 @@ ParseScript(const char *script, const char *desc, int weight)
* stdstrings should be true, which is a bit riskier.
*/
psql_scan_setup(sstate, script, strlen(script), 0, true);
+ start_offset = expr_scanner_offset(sstate) - 1;
initPQExpBuffer(&line_buf);
@@ -4425,45 +4656,115 @@ ParseScript(const char *script, const char *desc, int weight)
{
PsqlScanResult sr;
promptStatus_t prompt;
- Command *command;
+ Command *command = NULL;
+ int semicolons;
resetPQExpBuffer(&line_buf);
+ lineno = expr_scanner_get_lineno(sstate, start_offset);
sr = psql_scan(sstate, &line_buf, &prompt);
- /* If we collected a SQL command, process that */
- command = process_sql_command(&line_buf, desc);
- if (command)
- {
- ps.commands[index] = command;
- index++;
+ semicolons = psql_scan_get_escaped_semicolons(sstate);
- if (index >= alloc_num)
- {
- alloc_num += COMMANDS_ALLOC_NUM;
- ps.commands = (Command **)
- pg_realloc(ps.commands, sizeof(Command *) * alloc_num);
- }
+ if (saw_cset)
+ {
+ /* the previous multi-line command ended with \cset */
+ append_sql_command(ps.commands[index - 1], line_buf.data,
+ semicolons + 1);
+ saw_cset = false;
+ }
+ else
+ {
+ /* If we collected a new SQL command, process that */
+ command = create_sql_command(&line_buf, desc, semicolons + 1);
+
+ /* store new command */
+ if (command)
+ ps.commands[index++] = command;
}
/* If we reached a backslash, process that */
if (sr == PSCAN_BACKSLASH)
{
command = process_backslash_command(sstate, desc);
+
if (command)
{
- ps.commands[index] = command;
- index++;
-
- if (index >= alloc_num)
+ /*
+ * If this is gset/cset, merge into the preceding command. (We
+ * don't use a command slot in this case).
+ */
+ if (command->meta == META_CSET ||
+ command->meta == META_GSET)
{
- alloc_num += COMMANDS_ALLOC_NUM;
- ps.commands = (Command **)
- pg_realloc(ps.commands, sizeof(Command *) * alloc_num);
+ int cindex;
+ Command *cmd;
+
+ /*
+ * If \cset is seen, set flag to leave the command pending
+ * for the next iteration to process.
+ */
+ saw_cset = command->meta == META_CSET;
+
+ if (index == 0)
+ syntax_error(desc, lineno, NULL, NULL,
+ "\\gset/cset cannot start a script",
+ NULL, -1);
+
+ cmd = ps.commands[index - 1];
+
+ if (cmd->type != SQL_COMMAND)
+ syntax_error(desc, lineno, NULL, NULL,
+ "\\gset/cset must follow a SQL command",
+ cmd->first_line, -1);
+
+ /* this {g,c}set applies to the previous query */
+ cindex = cmd->nqueries - 1;
+
+ /*
+ * now that we know there's a {g,c}set in this command,
+ * allocate space for the variable name prefix array.
+ */
+ allocate_command_varprefix(cmd, cmd->nqueries);
+
+ /*
+ * Don't allow the previous command be a gset/cset; that
+ * would make no sense.
+ */
+ if (cmd->varprefix[cindex] != NULL)
+ syntax_error(desc, lineno, NULL, NULL,
+ "\\gset/cset cannot follow one another",
+ NULL, -1);
+
+ /* get variable prefix */
+ if (command->argc <= 1 || command->argv[1][0] == '\0')
+ cmd->varprefix[cindex] = ""; /* avoid strdup */
+ else
+ cmd->varprefix[cindex] = pg_strdup(command->argv[1]);
+
+ /* cleanup unused command */
+ free_command(command);
+
+ continue;
}
+
+ /* Attach any other backslash command as a new command */
+ ps.commands[index++] = command;
}
}
+ /*
+ * Since we used a command slot, allocate more if needed. Note we
+ * always allocate one more in order to accomodate the NULL terminator
+ * below.
+ */
+ if (index >= alloc_num)
+ {
+ alloc_num += COMMANDS_ALLOC_NUM;
+ ps.commands = (Command **)
+ pg_realloc(ps.commands, sizeof(Command *) * alloc_num);
+ }
+
/* Done if we reached EOF */
if (sr == PSCAN_INCOMPLETE || sr == PSCAN_EOL)
break;
@@ -4819,7 +5120,7 @@ printResults(TState *threads, StatsData *total, instr_time total_time,
printf(" %11.3f %s\n",
(cstats->count > 0) ?
1000.0 * cstats->sum / cstats->count : 0.0,
- (*commands)->line);
+ (*commands)->first_line);
}
}
}
@@ -5286,28 +5587,18 @@ main(int argc, char **argv)
internal_script_used = true;
}
- /* if not simple query mode, parse the script(s) to find parameters */
- if (querymode != QUERY_SIMPLE)
- {
- for (i = 0; i < num_scripts; i++)
- {
- Command **commands = sql_script[i].commands;
- int j;
-
- for (j = 0; commands[j] != NULL; j++)
- {
- if (commands[j]->type != SQL_COMMAND)
- continue;
- if (!parseQuery(commands[j]))
- exit(1);
- }
- }
- }
-
- /* compute total_weight */
+ /* complete SQL command initialization and compute total weight */
for (i = 0; i < num_scripts; i++)
+ {
+ Command **commands = sql_script[i].commands;
+
+ for (int j = 0; commands[j] != NULL; j++)
+ if (commands[j]->type == SQL_COMMAND)
+ postprocess_sql_command(commands[j]);
+
/* cannot overflow: weight is 32b, total_weight 64b */
total_weight += sql_script[i].weight;
+ }
if (total_weight == 0 && !is_init_mode)
{
diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl
index 89678e7f3f7..c87748086ab 100644
--- a/src/bin/pgbench/t/001_pgbench_with_server.pl
+++ b/src/bin/pgbench/t/001_pgbench_with_server.pl
@@ -528,6 +528,48 @@ pgbench(
}
});
+# working \gset and \cset
+pgbench(
+ '-t 1', 0,
+ [ qr{type: .*/001_pgbench_gset_and_cset}, qr{processed: 1/1} ],
+ [ qr{command=3.: int 0\b},
+ qr{command=5.: int 1\b},
+ qr{command=6.: int 2\b},
+ qr{command=8.: int 3\b},
+ qr{command=9.: int 4\b},
+ qr{command=10.: int 5\b},
+ qr{command=12.: int 6\b},
+ qr{command=13.: int 7\b},
+ qr{command=14.: int 8\b},
+ qr{command=16.: int 9\b} ],
+ 'pgbench gset and cset commands',
+ { '001_pgbench_gset_and_cset' => q{-- test gset and cset
+-- no columns
+SELECT \gset
+-- one value
+SELECT 0 AS i0 \gset
+\set i debug(:i0)
+-- two values
+SELECT 1 AS i1, 2 AS i2 \gset
+\set i debug(:i1)
+\set i debug(:i2)
+-- cset & gset to follow
+SELECT :i2 + 1 AS i3, :i2 * :i2 AS i4 \cset
+ SELECT 5 AS i5 \gset
+\set i debug(:i3)
+\set i debug(:i4)
+\set i debug(:i5)
+-- with prefix
+SELECT 6 AS i6, 7 AS i7 \cset x_
+ SELECT 8 AS i8 \gset y_
+\set i debug(:x_i6)
+\set i debug(:x_i7)
+\set i debug(:y_i8)
+-- overwrite existing variable
+SELECT 0 AS i9, 9 AS i9 \gset
+\set i debug(:i9)
+} });
+
# trigger many expression errors
my @errors = (
@@ -735,21 +777,45 @@ SELECT LEAST(:i, :i, :i, :i, :i, :i, :i, :i, :i, :i, :i);
[qr{invalid command .* "nosuchcommand"}], q{\nosuchcommand}
],
[ 'misc empty script', 1, [qr{empty command list for script}], q{} ],
- [
- 'bad boolean', 2,
- [qr{malformed variable.*trueXXX}], q{\set b :badtrue or true}
- ],);
+ [ 'bad boolean', 2,
+ [qr{malformed variable.*trueXXX}], q{\set b :badtrue or true} ],
+ # GSET & CSET
+ [ 'gset no row', 2,
+ [qr{expected one row, got 0\b}], q{SELECT WHERE FALSE \gset} ],
+ [ 'cset no row', 2,
+ [qr{expected one row, got 0\b}], q{SELECT WHERE FALSE \cset
+SELECT 1 AS i\gset}, 1 ],
+ [ 'gset alone', 1, [qr{gset/cset cannot start a script}], q{\gset} ],
+ [ 'gset no SQL', 1,
+ [qr{gset/cset must follow a SQL command}], q{\set i +1
+\gset} ],
+ [ 'gset too many arguments', 1,
+ [qr{too many arguments}], q{SELECT 1 \gset a b} ],
+ [ 'gset after gset', 1,
+ [qr{gset/cset cannot follow one another}], q{SELECT 1 AS i \gset
+\gset} ],
+ [ 'gset non SELECT', 2,
+ [qr{expected one row, got 0}],
+ q{DROP TABLE IF EXISTS no_such_table \gset} ],
+ [ 'gset bad default name', 2,
+ [qr{error storing into variable \?column\?}],
+ q{SELECT 1 \gset} ],
+ [ 'gset bad name', 2,
+ [qr{error storing into variable bad name!}],
+ q{SELECT 1 AS "bad name!" \gset} ],
+ );
for my $e (@errors)
{
- my ($name, $status, $re, $script) = @$e;
+ my ($name, $status, $re, $script, $no_prepare) = @$e;
$status != 0 or die "invalid expected status for test \"$name\"";
my $n = '001_pgbench_error_' . $name;
$n =~ s/ /_/g;
pgbench(
- '-n -t 1 -M prepared -Dfoo=bla -Dnull=null -Dtrue=true -Done=1 -Dzero=0.0 ' .
- '-Dbadtrue=trueXXX -Dmaxint=9223372036854775807 -Dminint=-9223372036854775808',
+ '-n -t 1 -Dfoo=bla -Dnull=null -Dtrue=true -Done=1 -Dzero=0.0 -Dbadtrue=trueXXX' .
+ ' -Dmaxint=9223372036854775807 -Dminint=-9223372036854775808' .
+ ($no_prepare ? '' : ' -M prepared'),
$status,
[ $status == 1 ? qr{^$} : qr{processed: 0/1} ],
$re,
diff --git a/src/fe_utils/psqlscan.l b/src/fe_utils/psqlscan.l
index 9ea32c319f1..321744cddbb 100644
--- a/src/fe_utils/psqlscan.l
+++ b/src/fe_utils/psqlscan.l
@@ -693,8 +693,15 @@ other .
* substitution. We want these before {self}, also.
*/
-"\\"[;:] {
- /* Force a semicolon or colon into the query buffer */
+"\\"; {
+ /* Count semicolons in compound commands */
+ cur_state->escaped_semicolons++;
+ /* Force a semicolon into the query buffer */
+ psqlscan_emit(cur_state, yytext + 1, 1);
+ }
+
+"\\": {
+ /* Force a colon into the query buffer */
psqlscan_emit(cur_state, yytext + 1, 1);
}
@@ -1065,6 +1072,9 @@ psql_scan(PsqlScanState state,
/* Set current output target */
state->output_buf = query_buf;
+ /* Reset number of escaped semicolons seen */
+ state->escaped_semicolons = 0;
+
/* Set input source */
if (state->buffer_stack != NULL)
yy_switch_to_buffer(state->buffer_stack->buf, state->scanner);
@@ -1208,6 +1218,16 @@ psql_scan_reset(PsqlScanState state)
state->dolqstart = NULL;
}
+/*
+ * Return the number of escaped semicolons in the lexed string seen by the
+ * previous psql_scan call.
+ */
+int
+psql_scan_get_escaped_semicolons(PsqlScanState state)
+{
+ return state->escaped_semicolons;
+}
+
/*
* Reselect this lexer (psqlscan.l) after using another one.
*
diff --git a/src/include/fe_utils/psqlscan.h b/src/include/fe_utils/psqlscan.h
index 1cf5b2e7fa4..d6fef9ff771 100644
--- a/src/include/fe_utils/psqlscan.h
+++ b/src/include/fe_utils/psqlscan.h
@@ -90,6 +90,8 @@ extern PsqlScanResult psql_scan(PsqlScanState state,
extern void psql_scan_reset(PsqlScanState state);
+extern int psql_scan_get_escaped_semicolons(PsqlScanState state);
+
extern void psql_scan_reselect_sql_lexer(PsqlScanState state);
extern bool psql_scan_in_quote(PsqlScanState state);
diff --git a/src/include/fe_utils/psqlscan_int.h b/src/include/fe_utils/psqlscan_int.h
index 42a738f4221..752cc9406a5 100644
--- a/src/include/fe_utils/psqlscan_int.h
+++ b/src/include/fe_utils/psqlscan_int.h
@@ -112,6 +112,7 @@ typedef struct PsqlScanStateData
int start_state; /* yylex's starting/finishing state */
int paren_depth; /* depth of nesting in parentheses */
int xcdepth; /* depth of nesting in slash-star comments */
+ int escaped_semicolons; /* number of embedded (\;) semicolons */
char *dolqstart; /* current $foo$ quote start string */
/*